managarten/scripts/generate-env.mjs
Till JS 230dfd5dad chore: extract arcade into standalone repo
Arcade lives as its own pnpm workspace at ~/Documents/Code/arcade
now, with no @mana/* coupling. This drops every reference and the
games/ directory from the monorepo.

Removes:
- games/ directory (89 files: web + server + 22 HTML games + screenshots)
- @arcade/web, @arcade/server pnpm workspace entries (games/* globs)
- arcade scripts in root package.json (4 scripts)
- arcade.mana.how from mana-auth trusted origins + CORS_ORIGINS
- arcade entries in mana-apps registry, app-icons, URL overrides
- arcade.mana.how from cloudflared tunnel + prometheus blackbox probes
- arcade-web service block in docker-compose.macmini.yml
- generate-env.mjs entries for arcade server + web
- BRANDING_ONLY 'arcade' entry in registry consistency spec
- dead arcade translation keys in GuestWelcomeModal (DE+EN)
- arcade mention in CLAUDE.md, authentication guideline, MODULE_REGISTRY

Verified:
- services/mana-auth/src/auth/sso-config.spec.ts: 8/8 pass
- pnpm install regenerates lockfile cleanly (-536 lines)
- no remaining 'arcade' refs outside historical snapshot docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:40:01 +02:00

898 lines
29 KiB
JavaScript

#!/usr/bin/env node
/**
* Environment Variable Generator
*
* Reads from .env.development and generates app-specific .env files
* with the appropriate prefixes for each platform.
*
* Usage: pnpm setup:env
*/
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, '..');
const ENV_FILE = join(ROOT_DIR, '.env.development');
// Optional gitignored override for personal dev secrets. Keys defined
// here win over .env.development, so devs can keep API keys in one
// place instead of re-pasting them into per-app .env files after every
// `pnpm setup:env`. See .env.secrets.example for the template.
const SECRETS_FILE = join(ROOT_DIR, '.env.secrets');
// Parse a .env file into an object
function parseEnvFile(content) {
const env = {};
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const match = trimmed.match(/^([^=]+)=(.*)$/);
if (match) {
let [, key, value] = match;
// Handle quoted values
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
env[key.trim()] = value;
}
}
return env;
}
// Generate env file content
function generateEnvContent(vars) {
const lines = ['# Auto-generated by scripts/generate-env.mjs', '# Source: .env.development', ''];
for (const [key, value] of Object.entries(vars)) {
// Quote values that contain special characters or newlines
const needsQuotes = value.includes('\n') || value.includes(' ') || value.includes('#');
const formattedValue = needsQuotes ? `"${value}"` : value;
lines.push(`${key}=${formattedValue}`);
}
return lines.join('\n') + '\n';
}
// App configurations - maps source variables to target variables
const APP_CONFIGS = [
// Mana Auth Service (Hono + Bun)
{
path: 'services/mana-auth/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MANA_AUTH_PORT || '3001',
DATABASE_URL: (env) => env.MANA_AUTH_DATABASE_URL,
BETTER_AUTH_SECRET: (env) => env.BETTER_AUTH_SECRET || 'dev-secret-change-me',
BETTER_AUTH_URL: () => 'http://localhost:3001',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
GOOGLE_GENAI_API_KEY: (env) => env.GOOGLE_GENAI_API_KEY,
},
},
// Unified Mana API (Hono + Bun, Port 3060) — consolidates per-module servers
{
path: 'apps/api/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MANA_API_PORT || '3060',
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL || 'http://localhost:3001',
MANA_LLM_URL: (env) => env.MANA_LLM_URL || 'http://localhost:3025',
MANA_SEARCH_URL: (env) => env.MANA_SEARCH_URL || 'http://localhost:3021',
MANA_CREDITS_URL: (env) => env.MANA_CREDITS_URL || 'http://localhost:3061',
MANA_MEDIA_URL: (env) => env.MANA_MEDIA_URL || 'http://localhost:3015',
MANA_CRAWLER_URL: (env) => env.MANA_CRAWLER_URL || 'http://localhost:3014',
MANA_IMAGE_GEN_URL: (env) => env.MANA_IMAGE_GEN_URL || '',
MANA_SERVICE_KEY: (env) => env.MANA_SERVICE_KEY || 'dev-service-key',
DATABASE_URL: (env) =>
env.MANA_API_DATABASE_URL || 'postgresql://mana:devpassword@localhost:5432/mana_platform',
CORS_ORIGINS: (env) => env.CORS_ORIGINS || 'http://localhost:5173',
APP_ID: () => 'mana-api',
LOGGER_FORMAT: (env) => env.LOGGER_FORMAT || 'pretty',
// Picture module providers
OPENAI_API_KEY: (env) => env.OPENAI_API_KEY || '',
REPLICATE_API_TOKEN: (env) => env.REPLICATE_API_TOKEN || '',
// Gemini Nano Banana image edits (Wardrobe Try-On + any future
// reference-generation surface). Either key name works — we
// read both inside the route with GEMINI_API_KEY taking
// precedence, matching how mana-llm ships today.
GEMINI_API_KEY: (env) => env.GEMINI_API_KEY || '',
GOOGLE_API_KEY: (env) => env.GOOGLE_API_KEY || '',
},
},
// Mana Research Service (Hono + Bun, Port 3068)
{
path: 'services/mana-research/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MANA_RESEARCH_PORT || '3068',
DATABASE_URL: (env) =>
env.MANA_RESEARCH_DATABASE_URL ||
'postgresql://mana:devpassword@localhost:5432/mana_platform',
REDIS_URL: (env) => env.REDIS_URL || 'redis://localhost:6379',
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL || 'http://localhost:3001',
MANA_LLM_URL: (env) => env.MANA_LLM_URL || 'http://localhost:3025',
MANA_CREDITS_URL: (env) => env.MANA_CREDITS_URL || 'http://localhost:3061',
MANA_SEARCH_URL: (env) => env.MANA_SEARCH_URL || 'http://localhost:3021',
MANA_SERVICE_KEY: (env) => env.MANA_SERVICE_KEY || 'dev-service-key',
CACHE_TTL_SECONDS: (env) => env.MANA_RESEARCH_CACHE_TTL_SECONDS || '3600',
CORS_ORIGINS: (env) => env.CORS_ORIGINS || 'http://localhost:5173',
BRAVE_API_KEY: (env) => env.BRAVE_API_KEY || '',
TAVILY_API_KEY: (env) => env.TAVILY_API_KEY || '',
EXA_API_KEY: (env) => env.EXA_API_KEY || '',
SERPER_API_KEY: (env) => env.SERPER_API_KEY || '',
JINA_API_KEY: (env) => env.JINA_API_KEY || '',
FIRECRAWL_API_KEY: (env) => env.FIRECRAWL_API_KEY || '',
SCRAPINGBEE_API_KEY: (env) => env.SCRAPINGBEE_API_KEY || '',
PERPLEXITY_API_KEY: (env) => env.PERPLEXITY_API_KEY || '',
ANTHROPIC_API_KEY: (env) => env.ANTHROPIC_API_KEY || '',
OPENAI_API_KEY: (env) => env.OPENAI_API_KEY || '',
GOOGLE_GENAI_API_KEY: (env) => env.GOOGLE_GENAI_API_KEY || '',
},
},
// Mana Events Service (Hono + Bun, Port 3065)
{
path: 'services/mana-events/.env',
vars: {
PORT: (env) => env.MANA_EVENTS_PORT || '3065',
DATABASE_URL: (env) =>
env.MANA_EVENTS_DATABASE_URL ||
'postgresql://mana:devpassword@localhost:5432/mana_platform',
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL || 'http://localhost:3001',
CORS_ORIGINS: (env) => env.CORS_ORIGINS || 'http://localhost:5173',
MANA_RESEARCH_URL: (env) => env.MANA_RESEARCH_URL || 'http://localhost:3068',
MANA_LLM_URL: (env) => env.MANA_LLM_URL || 'http://localhost:3025',
MEETUP_API_KEY: (env) => env.MEETUP_API_KEY || '',
},
},
// Mana Mail Service (Hono + Bun, Port 3042)
// Stalwart proxy + 1:1 send via JMAP + broadcast (newsletter) via bulk-send.
{
path: 'services/mana-mail/.env',
vars: {
PORT: (env) => env.MANA_MAIL_PORT || '3042',
DATABASE_URL: (env) =>
env.MANA_MAIL_DATABASE_URL || 'postgresql://mana:devpassword@localhost:5432/mana_platform',
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL || 'http://localhost:3001',
MANA_SERVICE_KEY: (env) => env.MANA_SERVICE_KEY || 'dev-service-key',
BASE_URL: (env) => env.MANA_MAIL_BASE_URL || 'http://localhost:3042',
CORS_ORIGINS: (env) => env.CORS_ORIGINS || 'http://localhost:5173',
STALWART_JMAP_URL: (env) => env.STALWART_JMAP_URL || 'http://localhost:8080',
STALWART_ADMIN_USER: (env) => env.STALWART_ADMIN_USER || 'admin',
STALWART_ADMIN_PASSWORD: (env) => env.STALWART_ADMIN_PASSWORD || 'ChangeMe123!',
MAIL_DOMAIN: (env) => env.MAIL_DOMAIN || 'mana.how',
// Broadcast / newsletter config — generated secrets rotate at deploy.
BROADCAST_TRACKING_SECRET: (env) =>
env.BROADCAST_TRACKING_SECRET || 'dev-broadcast-tracking-secret-NOT-for-prod',
BROADCAST_MAX_RECIPIENTS_PER_CAMPAIGN: (env) =>
env.BROADCAST_MAX_RECIPIENTS_PER_CAMPAIGN || '5000',
BROADCAST_MAX_RECIPIENTS_PER_HOUR: (env) => env.BROADCAST_MAX_RECIPIENTS_PER_HOUR || '500',
BROADCAST_SEND_THROTTLE_MS: (env) => env.BROADCAST_SEND_THROTTLE_MS || '150',
},
},
// Chat Server (Hono/Bun)
{
path: 'apps/chat/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CHAT_BACKEND_PORT || '3002',
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
OPENROUTER_API_KEY: (env) => env.OPENROUTER_API_KEY,
OLLAMA_URL: (env) => env.OLLAMA_URL || 'http://localhost:11434',
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DATABASE_URL: (env) => env.CHAT_DATABASE_URL,
},
},
// Chat Mobile (Expo)
{
path: 'apps/chat/apps/mobile/.env',
vars: {
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
EXPO_PUBLIC_SUPABASE_URL: (env) => env.CHAT_SUPABASE_URL,
EXPO_PUBLIC_SUPABASE_ANON_KEY: (env) => env.CHAT_SUPABASE_ANON_KEY,
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CHAT_BACKEND_PORT || '3002'}`,
},
},
// Chat Web (SvelteKit)
{
path: 'apps/chat/apps/web/.env',
vars: {
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_SUPABASE_URL: (env) => env.CHAT_SUPABASE_URL,
PUBLIC_SUPABASE_ANON_KEY: (env) => env.CHAT_SUPABASE_ANON_KEY,
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CHAT_BACKEND_PORT || '3002'}`,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Mana Mobile
{
path: 'apps/mana/apps/mobile/.env',
vars: {
EXPO_PUBLIC_SUPABASE_URL: (env) => env.MANA_SUPABASE_URL,
EXPO_PUBLIC_SUPABASE_ANON_KEY: (env) => env.MANA_SUPABASE_ANON_KEY,
},
},
// Mana Web
{
path: 'apps/mana/apps/web/.env',
vars: {
PUBLIC_SUPABASE_URL: (env) => env.MANA_SUPABASE_URL,
PUBLIC_SUPABASE_ANON_KEY: (env) => env.MANA_SUPABASE_ANON_KEY,
MIDDLEWARE_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_MANA || '',
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
// Speech-to-Text proxy (server-side only, never exposed to the client)
MANA_STT_URL: (env) => env.STT_URL || 'http://localhost:3020',
MANA_STT_API_KEY: (env) => env.MANA_STT_API_KEY || '',
// LLM proxy for /api/v1/voice/parse-task and /api/v1/voice/parse-habit
// (server-side only). The fallback path inside those endpoints
// keeps voice quick-add usable when this is unset.
MANA_LLM_URL: (env) => env.MANA_LLM_URL || 'http://localhost:3025',
MANA_LLM_API_KEY: (env) => env.MANA_LLM_API_KEY || '',
},
},
// Cards Server (Hono/Bun)
{
path: 'apps/cards/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CARDS_BACKEND_PORT || '3004',
DATABASE_URL: (env) => env.CARDS_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
APP_ID: (env) => env.CARDS_APP_ID,
GOOGLE_GENAI_API_KEY: (env) => env.GOOGLE_GENAI_API_KEY,
},
},
// Cards Web
{
path: 'apps/cards/apps/web/.env',
vars: {
PUBLIC_API_URL: (env) => `http://localhost:${env.CARDS_BACKEND_PORT || '3004'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Picture Server (Hono/Bun)
{
path: 'apps/picture/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.PICTURE_BACKEND_PORT || '3006',
BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3006',
DATABASE_URL: (env) =>
env.PICTURE_DATABASE_URL || 'postgresql://mana:devpassword@localhost:5432/picture',
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
REPLICATE_API_TOKEN: (env) => env.PICTURE_REPLICATE_API_TOKEN,
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
// Storage configuration - use shared MinIO for local dev
STORAGE_MODE: (env) => env.PICTURE_STORAGE_MODE || 's3',
LOCAL_STORAGE_PATH: (env) => env.PICTURE_LOCAL_STORAGE_PATH || './uploads',
S3_ENDPOINT: (env) => env.S3_ENDPOINT || 'http://localhost:9000',
S3_REGION: (env) => env.S3_REGION || 'us-east-1',
S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY || 'minioadmin',
S3_SECRET_KEY: (env) => env.S3_SECRET_KEY || 'minioadmin',
S3_BUCKET: (env) => env.PICTURE_S3_BUCKET || 'picture-storage',
STORAGE_PUBLIC_URL: (env) =>
env.PICTURE_STORAGE_PUBLIC_URL || 'http://localhost:9000/picture-storage',
// Credit system (for staging)
APP_ID: (env) => env.PICTURE_APP_ID || 'picture-app',
MANA_SERVICE_KEY: (env) => env.PICTURE_MANA_SERVICE_KEY || '',
},
},
// Picture Mobile (Expo)
{
path: 'apps/picture/apps/mobile/.env',
vars: {
EXPO_PUBLIC_BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3003',
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Picture Web (SvelteKit) - No Supabase, uses Backend API
{
path: 'apps/picture/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3003',
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Food Server (Hono/Bun)
{
path: 'apps/food/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.FOOD_BACKEND_PORT || '3002',
DATABASE_URL: (env) => env.FOOD_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
GEMINI_API_KEY: (env) => env.FOOD_GEMINI_API_KEY,
S3_ENDPOINT: (env) => env.FOOD_S3_ENDPOINT,
S3_ACCESS_KEY_ID: (env) => env.FOOD_S3_ACCESS_KEY_ID,
S3_SECRET_ACCESS_KEY: (env) => env.FOOD_S3_SECRET_ACCESS_KEY,
S3_BUCKET_NAME: (env) => env.FOOD_S3_BUCKET_NAME,
S3_REGION: (env) => env.FOOD_S3_REGION,
S3_PUBLIC_URL: (env) => env.FOOD_S3_PUBLIC_URL,
},
},
// Food Web (SvelteKit)
{
path: 'apps/food/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.FOOD_BACKEND_PORT || '3002'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_MIDDLEWARE_APP_ID: (env) => env.FOOD_APP_ID || 'food',
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Quotes Backend: REMOVED — migrated to local-first
// Quotes Mobile (Expo)
{
path: 'apps/quotes/apps/mobile/.env',
vars: {
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.QUOTES_BACKEND_PORT || '3007'}`,
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Quotes Web (SvelteKit)
{
path: 'apps/quotes/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.QUOTES_BACKEND_PORT || '3007'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Presi Backend: REMOVED — replaced by Hono server (apps/presi/apps/server)
// Presi Web (SvelteKit)
{
path: 'apps/presi/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.PRESI_BACKEND_PORT || '3008'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// SkillTree Backend: REMOVED — migrated to local-first
// SkillTree Web (SvelteKit)
{
path: 'apps/skilltree/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.SKILLTREE_BACKEND_PORT || '3024'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Context Server (Hono/Bun)
{
path: 'apps/context/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CONTEXT_BACKEND_PORT || '3020',
DATABASE_URL: (env) => env.CONTEXT_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
AZURE_OPENAI_API_KEY: (env) => env.CONTEXT_AZURE_OPENAI_API_KEY || '',
AZURE_OPENAI_ENDPOINT: (env) => env.CONTEXT_AZURE_OPENAI_ENDPOINT || '',
GOOGLE_API_KEY: (env) => env.CONTEXT_GOOGLE_API_KEY || '',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Context Web (SvelteKit)
{
path: 'apps/context/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CONTEXT_BACKEND_PORT || '3020'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Calendar Server (Hono/Bun)
{
path: 'apps/calendar/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CALENDAR_BACKEND_PORT || '3014',
DATABASE_URL: (env) => env.CALENDAR_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Calendar Mobile (Expo)
{
path: 'apps/calendar/apps/mobile/.env',
vars: {
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CALENDAR_BACKEND_PORT || '3014'}`,
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Calendar Web (SvelteKit)
{
path: 'apps/calendar/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CALENDAR_BACKEND_PORT || '3014'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_TODO_BACKEND_URL: (env) =>
env.TODO_BACKEND_URL || `http://localhost:${env.TODO_BACKEND_PORT || '3018'}`,
// Cross-app integration: Contacts service for birthdays
PUBLIC_CONTACTS_API_URL: (env) => `http://localhost:${env.CONTACTS_BACKEND_PORT || '3015'}`,
PUBLIC_CONTACTS_WEB_URL: () => 'http://localhost:5184',
// Speech-to-Text Service
PUBLIC_STT_URL: (env) => env.STT_URL || 'http://localhost:3020',
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Contacts Server (Hono/Bun)
{
path: 'apps/contacts/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CONTACTS_BACKEND_PORT || '3015',
DATABASE_URL: (env) => env.CONTACTS_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
S3_ENDPOINT: (env) => env.S3_ENDPOINT,
S3_REGION: (env) => env.S3_REGION,
S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY,
S3_SECRET_KEY: (env) => env.S3_SECRET_KEY,
S3_BUCKET: (env) => env.CONTACTS_S3_BUCKET || 'contacts-photos',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
// Google OAuth for contacts import
GOOGLE_CLIENT_ID: (env) => env.CONTACTS_GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: (env) => env.CONTACTS_GOOGLE_CLIENT_SECRET,
GOOGLE_REDIRECT_URI: (env) => env.CONTACTS_GOOGLE_REDIRECT_URI,
},
},
// Contacts Mobile (Expo)
{
path: 'apps/contacts/apps/mobile/.env',
vars: {
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CONTACTS_BACKEND_PORT || '3015'}`,
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Contacts Web (SvelteKit)
{
path: 'apps/contacts/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CONTACTS_BACKEND_PORT || '3015'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Storage Server (Hono/Bun)
{
path: 'apps/storage/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.STORAGE_BACKEND_PORT || '3016',
DATABASE_URL: (env) => env.STORAGE_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
S3_ENDPOINT: (env) => env.S3_ENDPOINT,
S3_REGION: (env) => env.S3_REGION,
S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY,
S3_SECRET_KEY: (env) => env.S3_SECRET_KEY,
STORAGE_S3_PUBLIC_URL: (env) => env.STORAGE_S3_PUBLIC_URL,
STORAGE_MAX_FILE_SIZE: (env) => env.STORAGE_MAX_FILE_SIZE || '104857600',
STORAGE_MAX_FILES_PER_UPLOAD: (env) => env.STORAGE_MAX_FILES_PER_UPLOAD || '10',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Storage Web (SvelteKit)
{
path: 'apps/storage/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.STORAGE_BACKEND_PORT || '3016'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Clock Backend: REMOVED — migrated to local-first
// Clock Web (SvelteKit)
{
path: 'apps/clock/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CLOCK_BACKEND_PORT || '3017'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Todo Server (Hono/Bun)
{
path: 'apps/todo/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.TODO_BACKEND_PORT || '3018',
DATABASE_URL: (env) => env.TODO_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Todo Web (SvelteKit)
{
path: 'apps/todo/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.TODO_BACKEND_PORT || '3018'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Moodlit Server (Hono/Bun)
{
path: 'apps/moodlit/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MOODLIT_BACKEND_PORT || '3012',
DATABASE_URL: (env) => env.MOODLIT_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Moodlit Mobile (Expo)
{
path: 'apps/moodlit/apps/mobile/.env',
vars: {
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.MOODLIT_BACKEND_PORT || '3012'}`,
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Moodlit Web (SvelteKit)
{
path: 'apps/moodlit/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.MOODLIT_BACKEND_PORT || '3012'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Finance: REMOVED
// Worldream Web (SvelteKit)
{
path: 'games/worldream/apps/web/.env',
vars: {
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// CityCorners Backend: REMOVED — migrated to local-first
// CityCorners Web (SvelteKit)
{
path: 'apps/citycorners/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.CITYCORNERS_BACKEND_PORT || '3025'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// TechBase: REMOVED
// Traces Server (Hono/Bun)
{
path: 'apps/traces/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.TRACES_BACKEND_PORT || '3026',
DATABASE_URL: (env) => env.TRACES_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
MANA_LLM_URL: (env) => env.MANA_LLM_URL || 'http://localhost:3025',
MANA_SEARCH_URL: (env) => env.MANA_SEARCH_URL || 'http://localhost:3021',
MANA_SERVICE_KEY: (env) => env.MANA_SERVICE_KEY || '',
APP_ID: () => 'traces',
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Traces Mobile (Expo)
{
path: 'apps/traces/apps/mobile/.env',
vars: {
EXPO_PUBLIC_TRACES_BACKEND_URL: (env) =>
`http://localhost:${env.TRACES_BACKEND_PORT || '3026'}`,
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// Photos Web (SvelteKit)
{
path: 'apps/photos/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.PHOTOS_BACKEND_PORT || '3039'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Planta Web (SvelteKit)
{
path: 'apps/planta/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.PLANTA_BACKEND_PORT || '3022'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Questions Web (SvelteKit)
{
path: 'apps/questions/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.QUESTIONS_BACKEND_PORT || '3011'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// Music Server (Hono/Bun)
{
path: 'apps/mukke/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MUSIC_BACKEND_PORT || '3010',
DATABASE_URL: (env) => env.MUSIC_DATABASE_URL,
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
S3_ENDPOINT: (env) => env.S3_ENDPOINT || 'http://localhost:9000',
S3_REGION: (env) => env.S3_REGION || 'us-east-1',
S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY || 'minioadmin',
S3_SECRET_KEY: (env) => env.S3_SECRET_KEY || 'minioadmin',
S3_BUCKET: () => 'music-storage',
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Music Web (SvelteKit)
{
path: 'apps/mukke/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.MUSIC_BACKEND_PORT || '3010'}`,
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},
},
// LLM Playground (SvelteKit)
{
path: 'services/llm-playground/.env',
vars: {
PUBLIC_MANA_LLM_URL: (env) => env.MANA_LLM_URL || 'http://localhost:3025',
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL || 'http://localhost:3001',
},
},
// Quotes Telegram Bot
{
path: 'services/telegram-quotes-bot/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.QUOTES_BOT_PORT || '3303',
TELEGRAM_BOT_TOKEN: (env) => env.QUOTES_BOT_TELEGRAM_TOKEN,
DATABASE_URL: (env) => env.QUOTES_BOT_DATABASE_URL,
},
},
// Todo Telegram Bot
{
path: 'services/telegram-todo-bot/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.TODO_BOT_PORT || '3304',
TELEGRAM_BOT_TOKEN: (env) => env.TODO_BOT_TELEGRAM_TOKEN,
DATABASE_URL: (env) => env.TODO_BOT_DATABASE_URL,
TODO_API_URL: (env) => env.TODO_BOT_API_URL || 'http://localhost:3018',
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
},
},
// ==========================================================
// Landing Pages (Astro) - Umami Website IDs
// ==========================================================
// Chat Landing
{
path: 'apps/chat/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_CHAT_LANDING || '',
},
},
// Mana Landing
{
path: 'apps/mana/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_MANA_LANDING || '',
},
},
// Cards Landing
{
path: 'apps/cards/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_CARDS_LANDING || '',
},
},
// Calendar Landing
{
path: 'apps/calendar/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_CALENDAR_LANDING || '',
},
},
// Clock Landing
{
path: 'apps/clock/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_CLOCK_LANDING || '',
},
},
// Picture Landing
{
path: 'apps/picture/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_PICTURE_LANDING || '',
},
},
// Todo Landing
{
path: 'apps/todo/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_TODO_LANDING || '',
},
},
// Food Landing
{
path: 'apps/food/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_FOOD_LANDING || '',
},
},
// Presi Landing
{
path: 'apps/presi/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_PRESI_LANDING || '',
},
},
// Music Landing
{
path: 'apps/mukke/apps/landing/.env',
vars: {
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_MUSIC_LANDING || '',
},
},
];
function main() {
console.log('Generating environment files from .env.development...\n');
// Check if source file exists
if (!existsSync(ENV_FILE)) {
console.error(`Error: ${ENV_FILE} not found.`);
console.error('Please create .env.development from .env.development.example');
process.exit(1);
}
// Parse source env file
const sourceContent = readFileSync(ENV_FILE, 'utf-8');
const sourceEnv = parseEnvFile(sourceContent);
// Layer .env.secrets (gitignored) on top — only non-empty values
// override. An empty value in .env.secrets is treated as "use the
// .env.development default", so a freshly-copied .env.secrets.example
// (all keys present, all values empty) is a no-op.
let secretsLoaded = 0;
if (existsSync(SECRETS_FILE)) {
const secretsContent = readFileSync(SECRETS_FILE, 'utf-8');
const secretsEnv = parseEnvFile(secretsContent);
for (const [key, value] of Object.entries(secretsEnv)) {
if (value !== '' && value !== undefined) {
sourceEnv[key] = value;
secretsLoaded++;
}
}
console.log(
`Loaded ${secretsLoaded} secret${secretsLoaded === 1 ? '' : 's'} from .env.secrets\n`
);
}
let generated = 0;
let skipped = 0;
for (const config of APP_CONFIGS) {
const targetPath = join(ROOT_DIR, config.path);
const targetDir = dirname(targetPath);
// Check if target directory exists
if (!existsSync(targetDir)) {
console.log(` Skipping ${config.path} (directory not found)`);
skipped++;
continue;
}
// Generate variables
const targetVars = {};
for (const [key, getter] of Object.entries(config.vars)) {
const value = getter(sourceEnv);
if (value !== undefined && value !== null) {
targetVars[key] = value;
}
}
// Write file
const content = generateEnvContent(targetVars);
writeFileSync(targetPath, content);
console.log(` Generated ${config.path}`);
generated++;
}
console.log(`\nDone! Generated ${generated} files, skipped ${skipped}.`);
console.log('\nNote: Generated .env files are gitignored. Only .env.development is committed.');
}
main();