#!/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();