mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
Local dev secrets like MANA_STT_API_KEY had no persistent home — they lived only in the gitignored, generator-overwritten per-app .env files. Every `pnpm setup:env` wiped them, so devs had to re-paste keys after any env regeneration. Same recurring friction for MANA_LLM_API_KEY, MANA_AUTH_KEK, OAuth keys, etc. New layer: `.env.secrets` at the repo root. - Gitignored, optional, never required for the build to pass - Read by generate-env.mjs AFTER .env.development; non-empty values override the matching key, so the merged result drives every per-app .env the generator writes - Empty values fall through to the .env.development defaults — a freshly-copied .env.secrets.example is a no-op - One source of truth for all dev secrets, propagated to every app with one `pnpm setup:env` Files: - `.env.secrets.example` — committed template documenting all known secret keys (mana-stt, mana-llm, auth KEK, sync JWT, MinIO, third- party APIs). Devs `cp .env.secrets.example .env.secrets` and fill in. - `.gitignore` — ignores .env.secrets, allows .env.secrets.example - `scripts/generate-env.mjs` — loads .env.secrets if present, prints "Loaded N secrets from .env.secrets" so devs see the override taking effect - `scripts/setup-secrets.mjs` + `pnpm setup:secrets` — convenience script that SSHes to mana-server, greps the prod .env for the keys defined in .env.secrets.example, and writes them locally. Confirms before overwriting an existing .env.secrets unless --force is set; reports which keys couldn't be found on the remote so devs know what's left to fill manually - `docs/LOCAL_DEVELOPMENT.md` + `docs/ENVIRONMENT_VARIABLES.md` — walk-through and architecture diagram update Verified end-to-end: - `rm .env.secrets apps/mana/apps/web/.env && pnpm setup:env` → STT key empty (no regression for devs who haven't opted in) - `pnpm setup:secrets --force && pnpm setup:env` → STT key propagated, "Loaded 3 secrets from .env.secrets" in output - POST /api/v1/voice/transcribe with a real audio file → full transcript back via gpu-stt.mana.how, end-to-end working Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
824 lines
25 KiB
JavaScript
824 lines
25 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,
|
|
},
|
|
},
|
|
|
|
// 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 || '',
|
|
},
|
|
},
|
|
|
|
// Nutriphi Server (Hono/Bun)
|
|
{
|
|
path: 'apps/nutriphi/apps/server/.env',
|
|
vars: {
|
|
NODE_ENV: () => 'development',
|
|
PORT: (env) => env.NUTRIPHI_BACKEND_PORT || '3002',
|
|
DATABASE_URL: (env) => env.NUTRIPHI_DATABASE_URL,
|
|
MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
|
|
GEMINI_API_KEY: (env) => env.NUTRIPHI_GEMINI_API_KEY,
|
|
S3_ENDPOINT: (env) => env.NUTRIPHI_S3_ENDPOINT,
|
|
S3_ACCESS_KEY_ID: (env) => env.NUTRIPHI_S3_ACCESS_KEY_ID,
|
|
S3_SECRET_ACCESS_KEY: (env) => env.NUTRIPHI_S3_SECRET_ACCESS_KEY,
|
|
S3_BUCKET_NAME: (env) => env.NUTRIPHI_S3_BUCKET_NAME,
|
|
S3_REGION: (env) => env.NUTRIPHI_S3_REGION,
|
|
S3_PUBLIC_URL: (env) => env.NUTRIPHI_S3_PUBLIC_URL,
|
|
},
|
|
},
|
|
|
|
// Nutriphi Web (SvelteKit)
|
|
{
|
|
path: 'apps/nutriphi/apps/web/.env',
|
|
vars: {
|
|
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.NUTRIPHI_BACKEND_PORT || '3002'}`,
|
|
PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
|
|
PUBLIC_MIDDLEWARE_APP_ID: (env) => env.NUTRIPHI_APP_ID || 'nutriphi',
|
|
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
|
|
},
|
|
},
|
|
|
|
// Zitare Backend: REMOVED — migrated to local-first
|
|
|
|
// Zitare Mobile (Expo)
|
|
{
|
|
path: 'apps/zitare/apps/mobile/.env',
|
|
vars: {
|
|
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.ZITARE_BACKEND_PORT || '3007'}`,
|
|
EXPO_PUBLIC_MANA_AUTH_URL: (env) => env.MANA_AUTH_URL,
|
|
},
|
|
},
|
|
|
|
// Zitare Web (SvelteKit)
|
|
{
|
|
path: 'apps/zitare/apps/web/.env',
|
|
vars: {
|
|
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.ZITARE_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 || '',
|
|
},
|
|
},
|
|
|
|
// Arcade Server (Hono/Bun)
|
|
{
|
|
path: 'games/arcade/apps/server/.env',
|
|
vars: {
|
|
NODE_ENV: () => 'development',
|
|
PORT: (env) => env.MANA_GAMES_BACKEND_PORT || '3011',
|
|
// Google Gemini
|
|
GOOGLE_GENAI_API_KEY: (env) => env.MANA_GAMES_GOOGLE_GENAI_API_KEY,
|
|
// Anthropic Claude
|
|
ANTHROPIC_API_KEY: (env) => env.MANA_GAMES_ANTHROPIC_API_KEY,
|
|
// Azure OpenAI
|
|
AZURE_OPENAI_ENDPOINT: (env) => env.MANA_GAMES_AZURE_OPENAI_ENDPOINT,
|
|
AZURE_OPENAI_API_KEY: (env) => env.MANA_GAMES_AZURE_OPENAI_API_KEY,
|
|
AZURE_OPENAI_DEPLOYMENT: (env) => env.MANA_GAMES_AZURE_OPENAI_DEPLOYMENT || 'gpt-4o',
|
|
// GitHub
|
|
GITHUB_TOKEN: (env) => env.MANA_GAMES_GITHUB_TOKEN,
|
|
GITHUB_OWNER: (env) => env.MANA_GAMES_GITHUB_OWNER || 'tillschneider',
|
|
GITHUB_REPO: (env) => env.MANA_GAMES_GITHUB_REPO || 'mana-games',
|
|
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
|
|
},
|
|
},
|
|
|
|
// Arcade Web (Astro)
|
|
{
|
|
path: 'games/arcade/apps/web/.env',
|
|
vars: {
|
|
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.MANA_GAMES_BACKEND_PORT || '3011'}`,
|
|
},
|
|
},
|
|
|
|
// 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',
|
|
},
|
|
},
|
|
|
|
// Zitare Telegram Bot
|
|
{
|
|
path: 'services/telegram-zitare-bot/.env',
|
|
vars: {
|
|
NODE_ENV: () => 'development',
|
|
PORT: (env) => env.ZITARE_BOT_PORT || '3303',
|
|
TELEGRAM_BOT_TOKEN: (env) => env.ZITARE_BOT_TELEGRAM_TOKEN,
|
|
DATABASE_URL: (env) => env.ZITARE_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 || '',
|
|
},
|
|
},
|
|
|
|
// NutriPhi Landing
|
|
{
|
|
path: 'apps/nutriphi/apps/landing/.env',
|
|
vars: {
|
|
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_NUTRIPHI_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();
|