#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(scriptDir, "..", ".."); const outputPath = path.join(scriptDir, ".env.development"); const backendEnvPath = path.join(rootDir, "apps", "backend", ".env.development"); const dashboardEnvPath = path.join(rootDir, "apps", "dashboard", ".env.development"); const args = process.argv.slice(2); if (args.length > 1 || (args[0] != null && args[0] !== "--check")) { throw new Error("Usage: node docker/local-emulator/generate-env-development.mjs [--check]"); } const parseEnvFile = (filePath) => { const env = new Map(); for (const rawLine of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) { const trimmedLine = rawLine.trim(); if (trimmedLine === "" || trimmedLine.startsWith("#")) { continue; } const separatorIndex = rawLine.indexOf("="); if (separatorIndex < 0) { throw new Error(`Invalid env line in ${filePath}: ${rawLine}`); } const key = rawLine.slice(0, separatorIndex).trim(); const value = rawLine.slice(separatorIndex + 1); env.set(key, value); } return env; }; const backendEnv = parseEnvFile(backendEnvPath); const dashboardEnv = parseEnvFile(dashboardEnvPath); // Hexclave rebrand: the source env files use the canonical HEXCLAVE_* names, // but accept the legacy STACK_* spelling as a fallback. Emitted keys are // always canonical. const toCanonicalKey = (key) => key.includes("STACK_") ? key.replace("STACK_", "HEXCLAVE_") : key; const getRequiredEnvValue = (sourceName, envMap, key) => { const canonicalKey = toCanonicalKey(key); const canonicalValue = envMap.get(canonicalKey); const legacyValue = canonicalKey === key ? undefined : envMap.get(key); if (canonicalValue && legacyValue && canonicalValue !== legacyValue) { throw new Error(`${sourceName} defines both ${canonicalKey} and ${key} with different non-empty values. Remove one of them or set them to the same value.`); } const nonEmptyValue = canonicalValue || legacyValue; const value = nonEmptyValue !== undefined ? nonEmptyValue : canonicalValue ?? legacyValue; if (value == null) { throw new Error(`Missing ${canonicalKey} in ${sourceName}; update the generator or source env file.`); } return value; }; const fromSource = (sourceName, envMap, key) => ({ type: "entry", key: toCanonicalKey(key), value: getRequiredEnvValue(sourceName, envMap, key), }); const literal = (key, value) => ({ type: "entry", key: toCanonicalKey(key), value, }); const comment = (value) => ({ type: "comment", value, }); const blank = () => ({ type: "blank", }); const entries = [ comment("# Generated by docker/local-emulator/generate-env-development.mjs"), comment("# Do not edit manually; update apps/backend/.env.development, apps/dashboard/.env.development, or this generator."), blank(), comment("# Public emulator/app credentials"), literal("NEXT_PUBLIC_STACK_DOCS_BASE_URL", "https://docs.hexclave.com"), literal("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "true"), fromSource("apps/dashboard/.env.development", dashboardEnv, "NEXT_PUBLIC_STACK_PROJECT_ID"), fromSource("apps/dashboard/.env.development", dashboardEnv, "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"), fromSource("apps/dashboard/.env.development", dashboardEnv, "STACK_SECRET_SERVER_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SERVER_SECRET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_CHANGELOG_URL"), blank(), comment("# Seed/project defaults"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_ENABLE_DUMMY_PROJECT"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS"), // STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is generated per-VM at boot // by docker/local-emulator/qemu/cloud-init/emulator/user-data and injected via // /run/stack-auth/local-emulator.env. SECRET_SERVER_KEY and SUPER_SECRET_ADMIN_KEY // are intentionally omitted so the seed script leaves them null on the internal // project; per-project credentials come from /api/v1/internal/local-emulator/project. blank(), comment("# Third-party/test integrations"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SVIX_API_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_OPENAI_API_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_OPENROUTER_API_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_STRIPE_SECRET_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_STRIPE_WEBHOOK_SECRET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_RESEND_API_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_RESEND_WEBHOOK_SECRET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_API_TOKEN"), fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_ACCOUNT_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_API_BASE_URL"), fromSource("apps/backend/.env.development", backendEnv, "STACK_FREESTYLE_API_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_VERCEL_SANDBOX_TOKEN"), fromSource("apps/backend/.env.development", backendEnv, "CRON_SECRET"), blank(), comment("# Storage, queueing, and analytics"), fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_REGION"), fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_ACCESS_KEY_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_SECRET_ACCESS_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_BUCKET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_PRIVATE_BUCKET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_REGION"), fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_ACCESS_KEY_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_SECRET_ACCESS_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_TOKEN"), fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_CURRENT_SIGNING_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_NEXT_SIGNING_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_ADMIN_USER"), fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_ADMIN_PASSWORD"), fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_EXTERNAL_PASSWORD"), blank(), comment("# Email and dashboard integration"), literal("STACK_EMAIL_PORT", "2500"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_SECURE"), literal("STACK_EMAIL_USERNAME", "does-not-matter"), literal("STACK_EMAIL_PASSWORD", "does-not-matter"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_SENDER"), fromSource("apps/backend/.env.development", backendEnv, "STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_PROJECT_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_SECRET_TOKEN"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_USE_INBUCKET"), fromSource("apps/dashboard/.env.development", dashboardEnv, "STACK_FEATUREBASE_JWT_SECRET"), blank(), comment("# Mock OAuth defaults"), literal("STACK_FORWARD_MOCK_OAUTH_SERVER", "false"), fromSource("apps/backend/.env.development", backendEnv, "STACK_GITHUB_CLIENT_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_GITHUB_CLIENT_SECRET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_GOOGLE_CLIENT_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_GOOGLE_CLIENT_SECRET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_MICROSOFT_CLIENT_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_MICROSOFT_CLIENT_SECRET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SPOTIFY_CLIENT_ID"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SPOTIFY_CLIENT_SECRET"), fromSource("apps/backend/.env.development", backendEnv, "STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS"), blank(), comment("# Internal service endpoints (defaults for docker-compose; overridden in QEMU)"), literal("STACK_DATABASE_CONNECTION_STRING", "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@127.0.0.1:5432/stackframe"), fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_HOST"), literal("STACK_SVIX_SERVER_URL", "http://127.0.0.1:8071"), literal("STACK_S3_ENDPOINT", "http://127.0.0.1:9090"), literal("STACK_QSTASH_URL", "http://127.0.0.1:8080"), literal("STACK_CLICKHOUSE_URL", "http://127.0.0.1:8123"), literal("STACK_CLICKHOUSE_DATABASE", "default"), literal("STACK_EMAIL_MONITOR_INBUCKET_API_URL", "http://127.0.0.1:9001"), literal("BACKEND_PORT", "8102"), literal("DASHBOARD_PORT", "8101"), ]; const seenKeys = new Set(); for (const entry of entries) { if (entry.type !== "entry") { continue; } if (seenKeys.has(entry.key)) { throw new Error(`Duplicate env key in generator: ${entry.key}`); } seenKeys.add(entry.key); } const content = `${entries.map((entry) => { if (entry.type === "blank") { return ""; } if (entry.type === "comment") { return entry.value; } return `${entry.key}=${entry.value}`; }).join("\n")}\n`; if (args[0] === "--check") { const currentContent = fs.readFileSync(outputPath, "utf8"); if (currentContent !== content) { throw new Error(`${path.relative(rootDir, outputPath)} is out of date. Run pnpm run emulator:generate-env.`); } console.log(`${path.relative(rootDir, outputPath)} is up to date.`); } else { fs.writeFileSync(outputPath, content); console.log(`Wrote ${path.relative(rootDir, outputPath)}.`); }