everything-dev 1.52.0 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-contract.cjs +6 -1
- package/dist/api-contract.cjs.map +1 -1
- package/dist/api-contract.mjs +6 -1
- package/dist/api-contract.mjs.map +1 -1
- package/dist/cli/infra.cjs +0 -67
- package/dist/cli/infra.cjs.map +1 -1
- package/dist/cli/infra.mjs +1 -65
- package/dist/cli/infra.mjs.map +1 -1
- package/dist/config.cjs +3 -1
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -1
- package/dist/config.mjs.map +1 -1
- package/dist/contract.d.cts +8 -0
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +8 -0
- package/dist/contract.d.mts.map +1 -1
- package/dist/dag.cjs +1 -0
- package/dist/dag.cjs.map +1 -1
- package/dist/dag.d.cts.map +1 -1
- package/dist/dag.d.mts.map +1 -1
- package/dist/dag.mjs +1 -0
- package/dist/dag.mjs.map +1 -1
- package/dist/merge.cjs +1 -1
- package/dist/merge.cjs.map +1 -1
- package/dist/merge.mjs +1 -1
- package/dist/merge.mjs.map +1 -1
- package/dist/orchestrator.cjs +1 -1
- package/dist/orchestrator.cjs.map +1 -1
- package/dist/orchestrator.mjs +1 -1
- package/dist/orchestrator.mjs.map +1 -1
- package/dist/plugin.cjs +1 -12
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +10 -0
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +10 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +2 -13
- package/dist/plugin.mjs.map +1 -1
- package/dist/types.cjs +11 -3
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +13 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +13 -0
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs +11 -3
- package/dist/types.mjs.map +1 -1
- package/package.json +3 -3
package/dist/cli/infra.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"infra.cjs","names":[],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport * as p from \"@clack/prompts\";\nimport { config as loadDotenv } from \"dotenv\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst API_DATABASE_SECRET = \"API_DATABASE_URL\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst BASE_POSTGRES_PORT = 5434;\nconst BASE_REDIS_PORT = 6379;\n\nexport interface DatabaseSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n databaseName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface RedisSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport interface PortState {\n postgresPorts: Record<string, number>;\n redisPorts: Record<string, number>;\n devPorts?: DevPortState;\n}\n\nexport interface GeneratedInfraSpec {\n groups: SecretGroup[];\n databases: DatabaseSecretConfig[];\n redis: RedisSecretConfig[];\n}\n\ninterface SyncGeneratedInfraResult {\n secrets: string[];\n envExampleChanged: boolean;\n dockerComposeChanged: boolean;\n staleEnvWarnings: string[];\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return { postgresPorts: {}, redisPorts: {} };\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return { postgresPorts: {}, redisPorts: {} };\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return {\n postgresPorts: raw.postgresPorts ?? {},\n redisPorts: raw.redisPorts ?? {},\n devPorts: raw.devPorts,\n };\n } catch {\n return { postgresPorts: {}, redisPorts: {} };\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport type { DevPortState };\n\nfunction resolvePort(slug: string, portMap: Record<string, number>, basePort: number): number {\n if (portMap[slug] !== undefined) return portMap[slug];\n const assigned = Object.values(portMap);\n const maxAssigned = assigned.length > 0 ? Math.max(...assigned) : basePort - 1;\n const next = Math.max(maxAssigned + 1, basePort);\n portMap[slug] = next;\n return next;\n}\n\nexport function normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction buildGeneratedInfraSpec(\n runtimeConfig: RuntimeConfig,\n configDir?: string,\n): { spec: GeneratedInfraSpec; portState: PortState } {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : new Map();\n const portState = loadPortState(configDir);\n\n const databases = buildDatabaseConfigs(allSecrets, originMap, portState.postgresPorts);\n const redis = buildRedisConfigs(allSecrets, originMap, portState.redisPorts);\n\n return { spec: { groups, databases, redis }, portState };\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nexport function buildOriginMap(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): Map<string, string> {\n const configPath = join(configDir, \"bos.config.json\");\n\n const originMap = new Map<string, string>();\n const account = runtimeConfig.account;\n\n const resolveOrigin = (extendsRef: unknown): string | null => {\n if (typeof extendsRef === \"string\") {\n const match = extendsRef.match(/^bos:\\/\\/([^/]+)\\//);\n return match?.[1] ?? null;\n }\n return null;\n };\n\n const rawConfig = existsSync(configPath)\n ? (JSON.parse(readFileSync(configPath, \"utf-8\")) as Record<string, unknown>)\n : null;\n const rawPlugins = rawConfig?.plugins as Record<string, unknown> | undefined;\n\n for (const secret of runtimeConfig.api.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n const rawApp = rawConfig?.app as Record<string, unknown> | undefined;\n const authExtends = (rawApp?.auth as Record<string, unknown> | undefined)?.extends;\n const authOrigin = resolveOrigin(authExtends) ?? account;\n for (const secret of runtimeConfig.auth?.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, authOrigin);\n }\n\n for (const [pluginKey, pluginEntry] of Object.entries(runtimeConfig.plugins ?? {})) {\n const rawPlugin = rawPlugins?.[pluginKey];\n let pluginOrigin: string;\n if (typeof rawPlugin === \"string\") {\n pluginOrigin = resolveOrigin(rawPlugin) ?? account;\n } else if (rawPlugin && typeof rawPlugin === \"object\") {\n pluginOrigin = resolveOrigin((rawPlugin as Record<string, unknown>).extends) ?? account;\n } else {\n pluginOrigin = account;\n }\n for (const secret of pluginEntry.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, pluginOrigin);\n }\n }\n\n for (const secret of runtimeConfig.host.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n return originMap;\n}\n\nexport function buildDatabaseConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): DatabaseSecretConfig[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n const orderedSecrets = [...databaseSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeDatabaseSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeDatabaseSlug(secret);\n if (secret === API_DATABASE_SECRET) {\n portMap[slug] = 5432;\n } else if (secret === AUTH_DATABASE_SECRET) {\n portMap[slug] = 5433;\n } else {\n resolvePort(slug, portMap, BASE_POSTGRES_PORT);\n }\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_postgres_${slug}_data`\n : `postgres_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-postgres-${slug}` : `postgres-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `postgres-${slug.replace(/_/g, \"-\")}`,\n containerName,\n databaseName: `${slug}_db`,\n volumeName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db`,\n };\n });\n}\n\nexport function buildRedisConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): RedisSecretConfig[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n const orderedSecrets = [...redisSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeRedisSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeRedisSlug(secret);\n resolvePort(slug, portMap, BASE_REDIS_PORT);\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeRedisSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_redis_${slug}_data`\n : `redis_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-redis-${slug}` : `redis-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `redis-${slug.replace(/_/g, \"-\")}`,\n containerName,\n volumeName,\n url: `redis://localhost:${port}`,\n };\n });\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseSecretConfig>,\n redisConfigs: Map<string, RedisSecretConfig>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));\n const redisMap = new Map(redisConfigs.map((entry) => [entry.secret, entry]));\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of groups) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction renderDockerCompose(\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redisConfigs.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const database of databases) {\n lines.push(` ${database.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${database.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${database.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${database.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const redis of redisConfigs) {\n lines.push(` ${redis.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${redis.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${redis.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${redis.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const database of databases) {\n lines.push(` ${database.volumeName}:`);\n lines.push(` name: ${database.volumeName}`);\n }\n for (const redis of redisConfigs) {\n lines.push(` ${redis.volumeName}:`);\n lines.push(` name: ${redis.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderEnvFileFromPlan(env: Record<string, string>, devHostPort?: number): string {\n const lines: string[] = [\n \"# Generated from bos dev infra plan\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n const sortedKeys = Object.keys(env).sort();\n for (const key of sortedKeys) {\n let value = env[key];\n if (key === \"CORS_ORIGIN\" && devHostPort && !value) {\n value = `http://localhost:${devHostPort}`;\n }\n lines.push(`${key}=${value}`);\n }\n lines.push(\"\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderDockerComposeFromPlan(\n databases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n redis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redis.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const db of databases) {\n lines.push(` ${db.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${db.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${db.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${db.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${db.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const r of redis) {\n lines.push(` ${r.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${r.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${r.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${r.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const db of databases) {\n lines.push(` ${db.volumeName}:`);\n lines.push(` name: ${db.volumeName}`);\n }\n for (const r of redis) {\n lines.push(` ${r.volumeName}:`);\n lines.push(` name: ${r.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function materializeInfraPlan(\n configDir: string,\n planEnv: Record<string, string>,\n planDatabases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n planRedis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n devHostPort?: number,\n): { envExampleChanged: boolean; dockerComposeChanged: boolean } {\n const envContent = renderEnvFileFromPlan(planEnv, devHostPort);\n const dockerContent = renderDockerComposeFromPlan(planDatabases, planRedis, projectName);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n return {\n envExampleChanged: syncTextFile(envExamplePath, envContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, dockerContent),\n };\n}\n\nfunction syncTextFile(filePath: string, nextContent: string): boolean {\n if (existsSync(filePath) && readFileSync(filePath, \"utf-8\") === nextContent) {\n return false;\n }\n\n writeFileSync(filePath, nextContent);\n return true;\n}\n\nexport function writeGeneratedInfra(configDir: string, runtimeConfig: RuntimeConfig): string[] {\n const result = syncGeneratedInfra(configDir, runtimeConfig);\n\n if (result.staleEnvWarnings.length > 0) {\n p.log.warn(\n `.env has ${result.staleEnvWarnings.length} stale value(s) compared to .env.example:`,\n );\n for (const warning of result.staleEnvWarnings) {\n p.log.message(` ${warning}`);\n }\n }\n\n return result.secrets;\n}\n\nexport function syncGeneratedInfra(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): SyncGeneratedInfraResult {\n const { spec, portState } = buildGeneratedInfraSpec(runtimeConfig, configDir);\n const secrets = spec.groups.flatMap((group) => group.secrets);\n const envOptions: { forExample: true; devHostPort?: number } = { forExample: true };\n if (runtimeConfig.env === \"development\") {\n envOptions.devHostPort = resolveDevHostPort(runtimeConfig);\n }\n const newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, envOptions);\n const newDockerContent = renderDockerCompose(spec.databases, spec.redis, runtimeConfig.account);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n const staleWarnings = checkEnvStaleness(configDir, spec.databases, spec.redis);\n\n if (configDir) {\n savePortState(configDir, portState);\n }\n\n return {\n secrets,\n envExampleChanged: syncTextFile(envExamplePath, newEnvContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent),\n staleEnvWarnings: staleWarnings,\n };\n}\n\nfunction checkEnvStaleness(\n configDir: string,\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n): string[] {\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return [];\n\n const existingEnv = readFileSync(envPath, \"utf-8\");\n const envMap = new Map<string, string>();\n for (const line of existingEnv.split(\"\\n\")) {\n const match = line.match(/^([A-Z_]+)=(.*)$/);\n if (match) envMap.set(match[1], match[2]);\n }\n\n const stale: string[] = [];\n\n for (const db of databases) {\n const existing = envMap.get(db.secret);\n if (existing && existing !== db.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${db.secret}: port ${oldPort} → ${db.port}`);\n }\n }\n\n for (const redis of redisConfigs) {\n const existing = envMap.get(redis.secret);\n if (existing && existing !== redis.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${redis.secret}: port ${oldPort} → ${redis.port}`);\n }\n }\n\n return stale;\n}\n\nexport function ensureEnvFile(configDir: string): void {\n const envPath = join(configDir, \".env\");\n const examplePath = join(configDir, \".env.example\");\n\n if (existsSync(envPath) || !existsSync(examplePath)) return;\n\n const content = readFileSync(examplePath, \"utf-8\");\n const lines = content.split(\"\\n\");\n const secret = randomBytes(32).toString(\"base64url\");\n const updated = lines\n .map((line) => {\n if (/^BETTER_AUTH_SECRET=/.test(line)) {\n return `BETTER_AUTH_SECRET=${secret}`;\n }\n return line;\n })\n .join(\"\\n\");\n\n writeFileSync(envPath, updated);\n p.log.info(\"Created .env from generated .env.example with generated BETTER_AUTH_SECRET\");\n}\n\nlet envLoadedDir: string | null = null;\n\nexport function loadProjectEnv(configDir: string): void {\n if (envLoadedDir === configDir) return;\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return;\n\n loadDotenv({ path: envPath, processEnv: process.env, quiet: true });\n envLoadedDir = configDir;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"infra.cjs","names":[],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport * as p from \"@clack/prompts\";\nimport { config as loadDotenv } from \"dotenv\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst API_DATABASE_SECRET = \"API_DATABASE_URL\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst BASE_POSTGRES_PORT = 5434;\nconst BASE_REDIS_PORT = 6379;\n\nexport interface DatabaseSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n databaseName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface RedisSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport interface PortState {\n postgresPorts: Record<string, number>;\n redisPorts: Record<string, number>;\n devPorts?: DevPortState;\n}\n\nexport interface GeneratedInfraSpec {\n groups: SecretGroup[];\n databases: DatabaseSecretConfig[];\n redis: RedisSecretConfig[];\n}\n\ninterface SyncGeneratedInfraResult {\n secrets: string[];\n envExampleChanged: boolean;\n dockerComposeChanged: boolean;\n staleEnvWarnings: string[];\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return { postgresPorts: {}, redisPorts: {} };\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return { postgresPorts: {}, redisPorts: {} };\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return {\n postgresPorts: raw.postgresPorts ?? {},\n redisPorts: raw.redisPorts ?? {},\n devPorts: raw.devPorts,\n };\n } catch {\n return { postgresPorts: {}, redisPorts: {} };\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport type { DevPortState };\n\nfunction resolvePort(slug: string, portMap: Record<string, number>, basePort: number): number {\n if (portMap[slug] !== undefined) return portMap[slug];\n const assigned = Object.values(portMap);\n const maxAssigned = assigned.length > 0 ? Math.max(...assigned) : basePort - 1;\n const next = Math.max(maxAssigned + 1, basePort);\n portMap[slug] = next;\n return next;\n}\n\nexport function normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction buildGeneratedInfraSpec(\n runtimeConfig: RuntimeConfig,\n configDir?: string,\n): { spec: GeneratedInfraSpec; portState: PortState } {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : new Map();\n const portState = loadPortState(configDir);\n\n const databases = buildDatabaseConfigs(allSecrets, originMap, portState.postgresPorts);\n const redis = buildRedisConfigs(allSecrets, originMap, portState.redisPorts);\n\n return { spec: { groups, databases, redis }, portState };\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nexport function buildOriginMap(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): Map<string, string> {\n const configPath = join(configDir, \"bos.config.json\");\n\n const originMap = new Map<string, string>();\n const account = runtimeConfig.account;\n\n const resolveOrigin = (extendsRef: unknown): string | null => {\n if (typeof extendsRef === \"string\") {\n const match = extendsRef.match(/^bos:\\/\\/([^/]+)\\//);\n return match?.[1] ?? null;\n }\n return null;\n };\n\n const rawConfig = existsSync(configPath)\n ? (JSON.parse(readFileSync(configPath, \"utf-8\")) as Record<string, unknown>)\n : null;\n const rawPlugins = rawConfig?.plugins as Record<string, unknown> | undefined;\n\n for (const secret of runtimeConfig.api.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n const rawApp = rawConfig?.app as Record<string, unknown> | undefined;\n const authExtends = (rawApp?.auth as Record<string, unknown> | undefined)?.extends;\n const authOrigin = resolveOrigin(authExtends) ?? account;\n for (const secret of runtimeConfig.auth?.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, authOrigin);\n }\n\n for (const [pluginKey, pluginEntry] of Object.entries(runtimeConfig.plugins ?? {})) {\n const rawPlugin = rawPlugins?.[pluginKey];\n let pluginOrigin: string;\n if (typeof rawPlugin === \"string\") {\n pluginOrigin = resolveOrigin(rawPlugin) ?? account;\n } else if (rawPlugin && typeof rawPlugin === \"object\") {\n pluginOrigin = resolveOrigin((rawPlugin as Record<string, unknown>).extends) ?? account;\n } else {\n pluginOrigin = account;\n }\n for (const secret of pluginEntry.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, pluginOrigin);\n }\n }\n\n for (const secret of runtimeConfig.host.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n return originMap;\n}\n\nexport function buildDatabaseConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): DatabaseSecretConfig[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n const orderedSecrets = [...databaseSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeDatabaseSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeDatabaseSlug(secret);\n if (secret === API_DATABASE_SECRET) {\n portMap[slug] = 5432;\n } else if (secret === AUTH_DATABASE_SECRET) {\n portMap[slug] = 5433;\n } else {\n resolvePort(slug, portMap, BASE_POSTGRES_PORT);\n }\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_postgres_${slug}_data`\n : `postgres_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-postgres-${slug}` : `postgres-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `postgres-${slug.replace(/_/g, \"-\")}`,\n containerName,\n databaseName: `${slug}_db`,\n volumeName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db`,\n };\n });\n}\n\nexport function buildRedisConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): RedisSecretConfig[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n const orderedSecrets = [...redisSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeRedisSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeRedisSlug(secret);\n resolvePort(slug, portMap, BASE_REDIS_PORT);\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeRedisSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_redis_${slug}_data`\n : `redis_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-redis-${slug}` : `redis-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `redis-${slug.replace(/_/g, \"-\")}`,\n containerName,\n volumeName,\n url: `redis://localhost:${port}`,\n };\n });\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseSecretConfig>,\n redisConfigs: Map<string, RedisSecretConfig>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));\n const redisMap = new Map(redisConfigs.map((entry) => [entry.secret, entry]));\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of groups) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction renderDockerCompose(\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redisConfigs.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const database of databases) {\n lines.push(` ${database.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${database.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${database.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${database.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const redis of redisConfigs) {\n lines.push(` ${redis.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${redis.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${redis.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${redis.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const database of databases) {\n lines.push(` ${database.volumeName}:`);\n lines.push(` name: ${database.volumeName}`);\n }\n for (const redis of redisConfigs) {\n lines.push(` ${redis.volumeName}:`);\n lines.push(` name: ${redis.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderEnvFileFromPlan(env: Record<string, string>, devHostPort?: number): string {\n const lines: string[] = [\n \"# Generated from bos dev infra plan\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n const sortedKeys = Object.keys(env).sort();\n for (const key of sortedKeys) {\n let value = env[key];\n if (key === \"CORS_ORIGIN\" && devHostPort && !value) {\n value = `http://localhost:${devHostPort}`;\n }\n lines.push(`${key}=${value}`);\n }\n lines.push(\"\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderDockerComposeFromPlan(\n databases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n redis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redis.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const db of databases) {\n lines.push(` ${db.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${db.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${db.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${db.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${db.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const r of redis) {\n lines.push(` ${r.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${r.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${r.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${r.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const db of databases) {\n lines.push(` ${db.volumeName}:`);\n lines.push(` name: ${db.volumeName}`);\n }\n for (const r of redis) {\n lines.push(` ${r.volumeName}:`);\n lines.push(` name: ${r.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function materializeInfraPlan(\n configDir: string,\n planEnv: Record<string, string>,\n planDatabases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n planRedis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n devHostPort?: number,\n): { envExampleChanged: boolean; dockerComposeChanged: boolean } {\n const envContent = renderEnvFileFromPlan(planEnv, devHostPort);\n const dockerContent = renderDockerComposeFromPlan(planDatabases, planRedis, projectName);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n return {\n envExampleChanged: syncTextFile(envExamplePath, envContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, dockerContent),\n };\n}\n\nfunction syncTextFile(filePath: string, nextContent: string): boolean {\n if (existsSync(filePath) && readFileSync(filePath, \"utf-8\") === nextContent) {\n return false;\n }\n\n writeFileSync(filePath, nextContent);\n return true;\n}\n\nexport function writeGeneratedInfra(configDir: string, runtimeConfig: RuntimeConfig): string[] {\n const result = syncGeneratedInfra(configDir, runtimeConfig);\n\n if (result.staleEnvWarnings.length > 0) {\n p.log.warn(\n `.env has ${result.staleEnvWarnings.length} stale value(s) compared to .env.example:`,\n );\n for (const warning of result.staleEnvWarnings) {\n p.log.message(` ${warning}`);\n }\n }\n\n return result.secrets;\n}\n\nexport function syncGeneratedInfra(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): SyncGeneratedInfraResult {\n const { spec, portState } = buildGeneratedInfraSpec(runtimeConfig, configDir);\n const secrets = spec.groups.flatMap((group) => group.secrets);\n const envOptions: { forExample: true; devHostPort?: number } = { forExample: true };\n if (runtimeConfig.env === \"development\") {\n envOptions.devHostPort = resolveDevHostPort(runtimeConfig);\n }\n const newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, envOptions);\n const newDockerContent = renderDockerCompose(spec.databases, spec.redis, runtimeConfig.account);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n const staleWarnings = checkEnvStaleness(configDir, spec.databases, spec.redis);\n\n if (configDir) {\n savePortState(configDir, portState);\n }\n\n return {\n secrets,\n envExampleChanged: syncTextFile(envExamplePath, newEnvContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent),\n staleEnvWarnings: staleWarnings,\n };\n}\n\nfunction checkEnvStaleness(\n configDir: string,\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n): string[] {\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return [];\n\n const existingEnv = readFileSync(envPath, \"utf-8\");\n const envMap = new Map<string, string>();\n for (const line of existingEnv.split(\"\\n\")) {\n const match = line.match(/^([A-Z_]+)=(.*)$/);\n if (match) envMap.set(match[1], match[2]);\n }\n\n const stale: string[] = [];\n\n for (const db of databases) {\n const existing = envMap.get(db.secret);\n if (existing && existing !== db.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${db.secret}: port ${oldPort} → ${db.port}`);\n }\n }\n\n for (const redis of redisConfigs) {\n const existing = envMap.get(redis.secret);\n if (existing && existing !== redis.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${redis.secret}: port ${oldPort} → ${redis.port}`);\n }\n }\n\n return stale;\n}\n\nexport function ensureEnvFile(configDir: string): void {\n const envPath = join(configDir, \".env\");\n const examplePath = join(configDir, \".env.example\");\n\n if (existsSync(envPath) || !existsSync(examplePath)) return;\n\n const content = readFileSync(examplePath, \"utf-8\");\n const lines = content.split(\"\\n\");\n const secret = randomBytes(32).toString(\"base64url\");\n const updated = lines\n .map((line) => {\n if (/^BETTER_AUTH_SECRET=/.test(line)) {\n return `BETTER_AUTH_SECRET=${secret}`;\n }\n return line;\n })\n .join(\"\\n\");\n\n writeFileSync(envPath, updated);\n p.log.info(\"Created .env from generated .env.example with generated BETTER_AUTH_SECRET\");\n}\n\nlet envLoadedDir: string | null = null;\n\nexport function loadProjectEnv(configDir: string): void {\n if (envLoadedDir === configDir) return;\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return;\n\n loadDotenv({ path: envPath, processEnv: process.env, quiet: true });\n envLoadedDir = configDir;\n}\n"],"mappings":"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"}
|
package/dist/cli/infra.mjs
CHANGED
|
@@ -254,70 +254,6 @@ function renderDockerCompose(databases, redisConfigs, projectName) {
|
|
|
254
254
|
}
|
|
255
255
|
return `${lines.join("\n")}\n`;
|
|
256
256
|
}
|
|
257
|
-
function renderEnvFileFromPlan(env, devHostPort) {
|
|
258
|
-
const lines = [
|
|
259
|
-
"# Generated from bos dev infra plan",
|
|
260
|
-
"# Update values as needed for your local environment",
|
|
261
|
-
""
|
|
262
|
-
];
|
|
263
|
-
const sortedKeys = Object.keys(env).sort();
|
|
264
|
-
for (const key of sortedKeys) {
|
|
265
|
-
let value = env[key];
|
|
266
|
-
if (key === "CORS_ORIGIN" && devHostPort && !value) value = `http://localhost:${devHostPort}`;
|
|
267
|
-
lines.push(`${key}=${value}`);
|
|
268
|
-
}
|
|
269
|
-
lines.push("");
|
|
270
|
-
return `${lines.join("\n")}\n`;
|
|
271
|
-
}
|
|
272
|
-
function renderDockerComposeFromPlan(databases, redis, projectName) {
|
|
273
|
-
const lines = [`name: ${projectName}`, ""];
|
|
274
|
-
if (databases.length > 0) lines.push("x-pg-common: &pg-common", " image: postgres:17-alpine", " environment: &pg-env", ` POSTGRES_USER: ${POSTGRES_USER}`, ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`, " healthcheck:", " test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]", " interval: 3s", " timeout: 3s", " retries: 5", "");
|
|
275
|
-
if (redis.length > 0) lines.push("x-redis-common: &redis-common", " image: redis:7-alpine", " command: redis-server --appendonly yes", " healthcheck:", " test: [\"CMD\", \"redis-cli\", \"ping\"]", " interval: 3s", " timeout: 3s", " retries: 5", "");
|
|
276
|
-
lines.push("services:");
|
|
277
|
-
for (const db of databases) {
|
|
278
|
-
lines.push(` ${db.serviceName}:`);
|
|
279
|
-
lines.push(" <<: *pg-common");
|
|
280
|
-
lines.push(` container_name: ${db.containerName}`);
|
|
281
|
-
lines.push(" environment:");
|
|
282
|
-
lines.push(" <<: *pg-env");
|
|
283
|
-
lines.push(` POSTGRES_DB: ${db.databaseName}`);
|
|
284
|
-
lines.push(" ports:");
|
|
285
|
-
lines.push(` - "${db.port}:5432"`);
|
|
286
|
-
lines.push(" volumes:");
|
|
287
|
-
lines.push(` - ${db.volumeName}:/var/lib/postgresql/data`);
|
|
288
|
-
lines.push("");
|
|
289
|
-
}
|
|
290
|
-
for (const r of redis) {
|
|
291
|
-
lines.push(` ${r.serviceName}:`);
|
|
292
|
-
lines.push(" <<: *redis-common");
|
|
293
|
-
lines.push(` container_name: ${r.containerName}`);
|
|
294
|
-
lines.push(" ports:");
|
|
295
|
-
lines.push(` - "${r.port}:6379"`);
|
|
296
|
-
lines.push(" volumes:");
|
|
297
|
-
lines.push(` - ${r.volumeName}:/data`);
|
|
298
|
-
lines.push("");
|
|
299
|
-
}
|
|
300
|
-
lines.push("volumes:");
|
|
301
|
-
for (const db of databases) {
|
|
302
|
-
lines.push(` ${db.volumeName}:`);
|
|
303
|
-
lines.push(` name: ${db.volumeName}`);
|
|
304
|
-
}
|
|
305
|
-
for (const r of redis) {
|
|
306
|
-
lines.push(` ${r.volumeName}:`);
|
|
307
|
-
lines.push(` name: ${r.volumeName}`);
|
|
308
|
-
}
|
|
309
|
-
return `${lines.join("\n")}\n`;
|
|
310
|
-
}
|
|
311
|
-
function materializeInfraPlan(configDir, planEnv, planDatabases, planRedis, projectName, devHostPort) {
|
|
312
|
-
const envContent = renderEnvFileFromPlan(planEnv, devHostPort);
|
|
313
|
-
const dockerContent = renderDockerComposeFromPlan(planDatabases, planRedis, projectName);
|
|
314
|
-
const envExamplePath = join(configDir, ".env.example");
|
|
315
|
-
const dockerComposePath = join(configDir, "docker-compose.yml");
|
|
316
|
-
return {
|
|
317
|
-
envExampleChanged: syncTextFile(envExamplePath, envContent),
|
|
318
|
-
dockerComposeChanged: syncTextFile(dockerComposePath, dockerContent)
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
257
|
function syncTextFile(filePath, nextContent) {
|
|
322
258
|
if (existsSync(filePath) && readFileSync(filePath, "utf-8") === nextContent) return false;
|
|
323
259
|
writeFileSync(filePath, nextContent);
|
|
@@ -401,5 +337,5 @@ function loadProjectEnv(configDir) {
|
|
|
401
337
|
}
|
|
402
338
|
|
|
403
339
|
//#endregion
|
|
404
|
-
export { buildDatabaseConfigs, buildOriginMap, buildRedisConfigs, ensureEnvFile, getSecretGroups, loadPortState, loadProjectEnv,
|
|
340
|
+
export { buildDatabaseConfigs, buildOriginMap, buildRedisConfigs, ensureEnvFile, getSecretGroups, loadPortState, loadProjectEnv, normalizeDatabaseSlug, normalizeRedisSlug, savePortState, syncGeneratedInfra, writeGeneratedInfra };
|
|
405
341
|
//# sourceMappingURL=infra.mjs.map
|
package/dist/cli/infra.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"infra.mjs","names":[],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport * as p from \"@clack/prompts\";\nimport { config as loadDotenv } from \"dotenv\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst API_DATABASE_SECRET = \"API_DATABASE_URL\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst BASE_POSTGRES_PORT = 5434;\nconst BASE_REDIS_PORT = 6379;\n\nexport interface DatabaseSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n databaseName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface RedisSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport interface PortState {\n postgresPorts: Record<string, number>;\n redisPorts: Record<string, number>;\n devPorts?: DevPortState;\n}\n\nexport interface GeneratedInfraSpec {\n groups: SecretGroup[];\n databases: DatabaseSecretConfig[];\n redis: RedisSecretConfig[];\n}\n\ninterface SyncGeneratedInfraResult {\n secrets: string[];\n envExampleChanged: boolean;\n dockerComposeChanged: boolean;\n staleEnvWarnings: string[];\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return { postgresPorts: {}, redisPorts: {} };\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return { postgresPorts: {}, redisPorts: {} };\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return {\n postgresPorts: raw.postgresPorts ?? {},\n redisPorts: raw.redisPorts ?? {},\n devPorts: raw.devPorts,\n };\n } catch {\n return { postgresPorts: {}, redisPorts: {} };\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport type { DevPortState };\n\nfunction resolvePort(slug: string, portMap: Record<string, number>, basePort: number): number {\n if (portMap[slug] !== undefined) return portMap[slug];\n const assigned = Object.values(portMap);\n const maxAssigned = assigned.length > 0 ? Math.max(...assigned) : basePort - 1;\n const next = Math.max(maxAssigned + 1, basePort);\n portMap[slug] = next;\n return next;\n}\n\nexport function normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction buildGeneratedInfraSpec(\n runtimeConfig: RuntimeConfig,\n configDir?: string,\n): { spec: GeneratedInfraSpec; portState: PortState } {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : new Map();\n const portState = loadPortState(configDir);\n\n const databases = buildDatabaseConfigs(allSecrets, originMap, portState.postgresPorts);\n const redis = buildRedisConfigs(allSecrets, originMap, portState.redisPorts);\n\n return { spec: { groups, databases, redis }, portState };\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nexport function buildOriginMap(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): Map<string, string> {\n const configPath = join(configDir, \"bos.config.json\");\n\n const originMap = new Map<string, string>();\n const account = runtimeConfig.account;\n\n const resolveOrigin = (extendsRef: unknown): string | null => {\n if (typeof extendsRef === \"string\") {\n const match = extendsRef.match(/^bos:\\/\\/([^/]+)\\//);\n return match?.[1] ?? null;\n }\n return null;\n };\n\n const rawConfig = existsSync(configPath)\n ? (JSON.parse(readFileSync(configPath, \"utf-8\")) as Record<string, unknown>)\n : null;\n const rawPlugins = rawConfig?.plugins as Record<string, unknown> | undefined;\n\n for (const secret of runtimeConfig.api.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n const rawApp = rawConfig?.app as Record<string, unknown> | undefined;\n const authExtends = (rawApp?.auth as Record<string, unknown> | undefined)?.extends;\n const authOrigin = resolveOrigin(authExtends) ?? account;\n for (const secret of runtimeConfig.auth?.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, authOrigin);\n }\n\n for (const [pluginKey, pluginEntry] of Object.entries(runtimeConfig.plugins ?? {})) {\n const rawPlugin = rawPlugins?.[pluginKey];\n let pluginOrigin: string;\n if (typeof rawPlugin === \"string\") {\n pluginOrigin = resolveOrigin(rawPlugin) ?? account;\n } else if (rawPlugin && typeof rawPlugin === \"object\") {\n pluginOrigin = resolveOrigin((rawPlugin as Record<string, unknown>).extends) ?? account;\n } else {\n pluginOrigin = account;\n }\n for (const secret of pluginEntry.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, pluginOrigin);\n }\n }\n\n for (const secret of runtimeConfig.host.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n return originMap;\n}\n\nexport function buildDatabaseConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): DatabaseSecretConfig[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n const orderedSecrets = [...databaseSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeDatabaseSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeDatabaseSlug(secret);\n if (secret === API_DATABASE_SECRET) {\n portMap[slug] = 5432;\n } else if (secret === AUTH_DATABASE_SECRET) {\n portMap[slug] = 5433;\n } else {\n resolvePort(slug, portMap, BASE_POSTGRES_PORT);\n }\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_postgres_${slug}_data`\n : `postgres_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-postgres-${slug}` : `postgres-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `postgres-${slug.replace(/_/g, \"-\")}`,\n containerName,\n databaseName: `${slug}_db`,\n volumeName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db`,\n };\n });\n}\n\nexport function buildRedisConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): RedisSecretConfig[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n const orderedSecrets = [...redisSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeRedisSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeRedisSlug(secret);\n resolvePort(slug, portMap, BASE_REDIS_PORT);\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeRedisSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_redis_${slug}_data`\n : `redis_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-redis-${slug}` : `redis-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `redis-${slug.replace(/_/g, \"-\")}`,\n containerName,\n volumeName,\n url: `redis://localhost:${port}`,\n };\n });\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseSecretConfig>,\n redisConfigs: Map<string, RedisSecretConfig>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));\n const redisMap = new Map(redisConfigs.map((entry) => [entry.secret, entry]));\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of groups) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction renderDockerCompose(\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redisConfigs.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const database of databases) {\n lines.push(` ${database.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${database.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${database.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${database.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const redis of redisConfigs) {\n lines.push(` ${redis.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${redis.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${redis.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${redis.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const database of databases) {\n lines.push(` ${database.volumeName}:`);\n lines.push(` name: ${database.volumeName}`);\n }\n for (const redis of redisConfigs) {\n lines.push(` ${redis.volumeName}:`);\n lines.push(` name: ${redis.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderEnvFileFromPlan(env: Record<string, string>, devHostPort?: number): string {\n const lines: string[] = [\n \"# Generated from bos dev infra plan\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n const sortedKeys = Object.keys(env).sort();\n for (const key of sortedKeys) {\n let value = env[key];\n if (key === \"CORS_ORIGIN\" && devHostPort && !value) {\n value = `http://localhost:${devHostPort}`;\n }\n lines.push(`${key}=${value}`);\n }\n lines.push(\"\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderDockerComposeFromPlan(\n databases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n redis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redis.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const db of databases) {\n lines.push(` ${db.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${db.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${db.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${db.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${db.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const r of redis) {\n lines.push(` ${r.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${r.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${r.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${r.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const db of databases) {\n lines.push(` ${db.volumeName}:`);\n lines.push(` name: ${db.volumeName}`);\n }\n for (const r of redis) {\n lines.push(` ${r.volumeName}:`);\n lines.push(` name: ${r.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function materializeInfraPlan(\n configDir: string,\n planEnv: Record<string, string>,\n planDatabases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n planRedis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n devHostPort?: number,\n): { envExampleChanged: boolean; dockerComposeChanged: boolean } {\n const envContent = renderEnvFileFromPlan(planEnv, devHostPort);\n const dockerContent = renderDockerComposeFromPlan(planDatabases, planRedis, projectName);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n return {\n envExampleChanged: syncTextFile(envExamplePath, envContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, dockerContent),\n };\n}\n\nfunction syncTextFile(filePath: string, nextContent: string): boolean {\n if (existsSync(filePath) && readFileSync(filePath, \"utf-8\") === nextContent) {\n return false;\n }\n\n writeFileSync(filePath, nextContent);\n return true;\n}\n\nexport function writeGeneratedInfra(configDir: string, runtimeConfig: RuntimeConfig): string[] {\n const result = syncGeneratedInfra(configDir, runtimeConfig);\n\n if (result.staleEnvWarnings.length > 0) {\n p.log.warn(\n `.env has ${result.staleEnvWarnings.length} stale value(s) compared to .env.example:`,\n );\n for (const warning of result.staleEnvWarnings) {\n p.log.message(` ${warning}`);\n }\n }\n\n return result.secrets;\n}\n\nexport function syncGeneratedInfra(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): SyncGeneratedInfraResult {\n const { spec, portState } = buildGeneratedInfraSpec(runtimeConfig, configDir);\n const secrets = spec.groups.flatMap((group) => group.secrets);\n const envOptions: { forExample: true; devHostPort?: number } = { forExample: true };\n if (runtimeConfig.env === \"development\") {\n envOptions.devHostPort = resolveDevHostPort(runtimeConfig);\n }\n const newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, envOptions);\n const newDockerContent = renderDockerCompose(spec.databases, spec.redis, runtimeConfig.account);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n const staleWarnings = checkEnvStaleness(configDir, spec.databases, spec.redis);\n\n if (configDir) {\n savePortState(configDir, portState);\n }\n\n return {\n secrets,\n envExampleChanged: syncTextFile(envExamplePath, newEnvContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent),\n staleEnvWarnings: staleWarnings,\n };\n}\n\nfunction checkEnvStaleness(\n configDir: string,\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n): string[] {\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return [];\n\n const existingEnv = readFileSync(envPath, \"utf-8\");\n const envMap = new Map<string, string>();\n for (const line of existingEnv.split(\"\\n\")) {\n const match = line.match(/^([A-Z_]+)=(.*)$/);\n if (match) envMap.set(match[1], match[2]);\n }\n\n const stale: string[] = [];\n\n for (const db of databases) {\n const existing = envMap.get(db.secret);\n if (existing && existing !== db.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${db.secret}: port ${oldPort} → ${db.port}`);\n }\n }\n\n for (const redis of redisConfigs) {\n const existing = envMap.get(redis.secret);\n if (existing && existing !== redis.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${redis.secret}: port ${oldPort} → ${redis.port}`);\n }\n }\n\n return stale;\n}\n\nexport function ensureEnvFile(configDir: string): void {\n const envPath = join(configDir, \".env\");\n const examplePath = join(configDir, \".env.example\");\n\n if (existsSync(envPath) || !existsSync(examplePath)) return;\n\n const content = readFileSync(examplePath, \"utf-8\");\n const lines = content.split(\"\\n\");\n const secret = randomBytes(32).toString(\"base64url\");\n const updated = lines\n .map((line) => {\n if (/^BETTER_AUTH_SECRET=/.test(line)) {\n return `BETTER_AUTH_SECRET=${secret}`;\n }\n return line;\n })\n .join(\"\\n\");\n\n writeFileSync(envPath, updated);\n p.log.info(\"Created .env from generated .env.example with generated BETTER_AUTH_SECRET\");\n}\n\nlet envLoadedDir: string | null = null;\n\nexport function loadProjectEnv(configDir: string): void {\n if (envLoadedDir === configDir) return;\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return;\n\n loadDotenv({ path: envPath, processEnv: process.env, quiet: true });\n envLoadedDir = configDir;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"infra.mjs","names":[],"sources":["../../src/cli/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport * as p from \"@clack/prompts\";\nimport { config as loadDotenv } from \"dotenv\";\nimport type { RuntimeConfig } from \"../types\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst API_DATABASE_SECRET = \"API_DATABASE_URL\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst BASE_POSTGRES_PORT = 5434;\nconst BASE_REDIS_PORT = 6379;\n\nexport interface DatabaseSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n databaseName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface RedisSecretConfig {\n secret: string;\n slug: string;\n fromKey: string;\n port: number;\n serviceName: string;\n containerName: string;\n volumeName: string;\n url: string;\n}\n\nexport interface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface DevPortState {\n host?: number;\n api?: number;\n ui?: number;\n auth?: number;\n pluginPortStart?: number;\n}\n\nexport interface PortState {\n postgresPorts: Record<string, number>;\n redisPorts: Record<string, number>;\n devPorts?: DevPortState;\n}\n\nexport interface GeneratedInfraSpec {\n groups: SecretGroup[];\n databases: DatabaseSecretConfig[];\n redis: RedisSecretConfig[];\n}\n\ninterface SyncGeneratedInfraResult {\n secrets: string[];\n envExampleChanged: boolean;\n dockerComposeChanged: boolean;\n staleEnvWarnings: string[];\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n const secrets: string[] = [];\n const seen = new Set<string>();\n\n for (const value of values) {\n if (!value || seen.has(value)) continue;\n seen.add(value);\n secrets.push(value);\n }\n\n return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n if (!configDir) return { postgresPorts: {}, redisPorts: {} };\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n if (!existsSync(statePath)) return { postgresPorts: {}, redisPorts: {} };\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n return {\n postgresPorts: raw.postgresPorts ?? {},\n redisPorts: raw.redisPorts ?? {},\n devPorts: raw.devPorts,\n };\n } catch {\n return { postgresPorts: {}, redisPorts: {} };\n }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n const statePath = join(configDir, \".bos\", \"infra-state.json\");\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport type { DevPortState };\n\nfunction resolvePort(slug: string, portMap: Record<string, number>, basePort: number): number {\n if (portMap[slug] !== undefined) return portMap[slug];\n const assigned = Object.values(portMap);\n const maxAssigned = assigned.length > 0 ? Math.max(...assigned) : basePort - 1;\n const next = Math.max(maxAssigned + 1, basePort);\n portMap[slug] = next;\n return next;\n}\n\nexport function normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n const groups: SecretGroup[] = [];\n const seen = new Set<string>();\n\n const addGroup = (section: string, secrets: string[]) => {\n const filtered = secrets.filter((s) => {\n if (seen.has(s)) return false;\n seen.add(s);\n return true;\n });\n if (filtered.length > 0) {\n groups.push({ section, secrets: filtered });\n }\n };\n\n addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n if (runtimeConfig.auth) {\n addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n if (plugin.secrets && plugin.secrets.length > 0) {\n addGroup(`plugins.${pluginKey}`, plugin.secrets);\n }\n }\n }\n\n return groups;\n}\n\nfunction buildGeneratedInfraSpec(\n runtimeConfig: RuntimeConfig,\n configDir?: string,\n): { spec: GeneratedInfraSpec; portState: PortState } {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : new Map();\n const portState = loadPortState(configDir);\n\n const databases = buildDatabaseConfigs(allSecrets, originMap, portState.postgresPorts);\n const redis = buildRedisConfigs(allSecrets, originMap, portState.redisPorts);\n\n return { spec: { groups, databases, redis }, portState };\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nexport function buildOriginMap(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): Map<string, string> {\n const configPath = join(configDir, \"bos.config.json\");\n\n const originMap = new Map<string, string>();\n const account = runtimeConfig.account;\n\n const resolveOrigin = (extendsRef: unknown): string | null => {\n if (typeof extendsRef === \"string\") {\n const match = extendsRef.match(/^bos:\\/\\/([^/]+)\\//);\n return match?.[1] ?? null;\n }\n return null;\n };\n\n const rawConfig = existsSync(configPath)\n ? (JSON.parse(readFileSync(configPath, \"utf-8\")) as Record<string, unknown>)\n : null;\n const rawPlugins = rawConfig?.plugins as Record<string, unknown> | undefined;\n\n for (const secret of runtimeConfig.api.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n const rawApp = rawConfig?.app as Record<string, unknown> | undefined;\n const authExtends = (rawApp?.auth as Record<string, unknown> | undefined)?.extends;\n const authOrigin = resolveOrigin(authExtends) ?? account;\n for (const secret of runtimeConfig.auth?.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, authOrigin);\n }\n\n for (const [pluginKey, pluginEntry] of Object.entries(runtimeConfig.plugins ?? {})) {\n const rawPlugin = rawPlugins?.[pluginKey];\n let pluginOrigin: string;\n if (typeof rawPlugin === \"string\") {\n pluginOrigin = resolveOrigin(rawPlugin) ?? account;\n } else if (rawPlugin && typeof rawPlugin === \"object\") {\n pluginOrigin = resolveOrigin((rawPlugin as Record<string, unknown>).extends) ?? account;\n } else {\n pluginOrigin = account;\n }\n for (const secret of pluginEntry.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, pluginOrigin);\n }\n }\n\n for (const secret of runtimeConfig.host.secrets ?? []) {\n if (!originMap.has(secret)) originMap.set(secret, account);\n }\n\n return originMap;\n}\n\nexport function buildDatabaseConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): DatabaseSecretConfig[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n const orderedSecrets = [...databaseSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeDatabaseSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeDatabaseSlug(secret);\n if (secret === API_DATABASE_SECRET) {\n portMap[slug] = 5432;\n } else if (secret === AUTH_DATABASE_SECRET) {\n portMap[slug] = 5433;\n } else {\n resolvePort(slug, portMap, BASE_POSTGRES_PORT);\n }\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeDatabaseSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_postgres_${slug}_data`\n : `postgres_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-postgres-${slug}` : `postgres-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `postgres-${slug.replace(/_/g, \"-\")}`,\n containerName,\n databaseName: `${slug}_db`,\n volumeName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db`,\n };\n });\n}\n\nexport function buildRedisConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n portMap: Record<string, number>,\n): RedisSecretConfig[] {\n const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n const orderedSecrets = [...redisSecrets];\n\n // Prune stale entries from removed plugins\n const currentSlugs = new Set(orderedSecrets.map(normalizeRedisSlug));\n for (const slug of Object.keys(portMap)) {\n if (!currentSlugs.has(slug)) delete portMap[slug];\n }\n\n // Sort by slug for deterministic assignment order\n orderedSecrets.sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)));\n\n for (const secret of orderedSecrets) {\n const slug = normalizeRedisSlug(secret);\n resolvePort(slug, portMap, BASE_REDIS_PORT);\n }\n\n return orderedSecrets.map((secret) => {\n const slug = normalizeRedisSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port = portMap[slug];\n\n const volumeName = fromKey\n ? `${fromKey.replace(/\\./g, \"_\")}_redis_${slug}_data`\n : `redis_${slug}_data`;\n\n const containerName = fromKey ? `${fromKey}-redis-${slug}` : `redis-${slug}`;\n\n return {\n secret,\n slug,\n fromKey,\n port,\n serviceName: `redis-${slug.replace(/_/g, \"-\")}`,\n containerName,\n volumeName,\n url: `redis://localhost:${port}`,\n };\n });\n}\n\nfunction extractPortFromUrl(url: string): string | null {\n const match = url.match(/:(\\d{4,5})(?:\\/|$)/);\n return match?.[1] ?? null;\n}\n\nfunction resolveDevHostPort(runtimeConfig: RuntimeConfig): number {\n if (typeof runtimeConfig.host?.port === \"number\") return runtimeConfig.host.port;\n const fromUrl = runtimeConfig.host?.url ? extractPortFromUrl(runtimeConfig.host.url) : null;\n if (fromUrl) {\n const parsed = Number.parseInt(fromUrl, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n return 3000;\n}\n\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseSecretConfig>,\n redisConfigs: Map<string, RedisSecretConfig>,\n options: { forExample: boolean; devHostPort?: number },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n if (typeof options.devHostPort === \"number\") {\n return `http://localhost:${options.devHostPort}`;\n }\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n options: { forExample: boolean; devHostPort?: number },\n): string {\n const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));\n const redisMap = new Map(redisConfigs.map((entry) => [entry.secret, entry]));\n const lines: string[] = [\n \"# Generated from configured bos secrets\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n\n for (const group of groups) {\n lines.push(`# ${group.section}`);\n for (const secret of group.secrets) {\n lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction renderDockerCompose(\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redisConfigs.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const database of databases) {\n lines.push(` ${database.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${database.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${database.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${database.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const redis of redisConfigs) {\n lines.push(` ${redis.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${redis.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${redis.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${redis.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const database of databases) {\n lines.push(` ${database.volumeName}:`);\n lines.push(` name: ${database.volumeName}`);\n }\n for (const redis of redisConfigs) {\n lines.push(` ${redis.volumeName}:`);\n lines.push(` name: ${redis.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderEnvFileFromPlan(env: Record<string, string>, devHostPort?: number): string {\n const lines: string[] = [\n \"# Generated from bos dev infra plan\",\n \"# Update values as needed for your local environment\",\n \"\",\n ];\n const sortedKeys = Object.keys(env).sort();\n for (const key of sortedKeys) {\n let value = env[key];\n if (key === \"CORS_ORIGIN\" && devHostPort && !value) {\n value = `http://localhost:${devHostPort}`;\n }\n lines.push(`${key}=${value}`);\n }\n lines.push(\"\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function renderDockerComposeFromPlan(\n databases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n redis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n): string {\n const lines = [`name: ${projectName}`, \"\"];\n\n if (databases.length > 0) {\n lines.push(\n \"x-pg-common: &pg-common\",\n \" image: postgres:17-alpine\",\n \" environment: &pg-env\",\n ` POSTGRES_USER: ${POSTGRES_USER}`,\n ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,\n \" healthcheck:\",\n ' test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n if (redis.length > 0) {\n lines.push(\n \"x-redis-common: &redis-common\",\n \" image: redis:7-alpine\",\n \" command: redis-server --appendonly yes\",\n \" healthcheck:\",\n ' test: [\"CMD\", \"redis-cli\", \"ping\"]',\n \" interval: 3s\",\n \" timeout: 3s\",\n \" retries: 5\",\n \"\",\n );\n }\n\n lines.push(\"services:\");\n\n for (const db of databases) {\n lines.push(` ${db.serviceName}:`);\n lines.push(\" <<: *pg-common\");\n lines.push(` container_name: ${db.containerName}`);\n lines.push(\" environment:\");\n lines.push(\" <<: *pg-env\");\n lines.push(` POSTGRES_DB: ${db.databaseName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${db.port}:5432\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${db.volumeName}:/var/lib/postgresql/data`);\n lines.push(\"\");\n }\n\n for (const r of redis) {\n lines.push(` ${r.serviceName}:`);\n lines.push(\" <<: *redis-common\");\n lines.push(` container_name: ${r.containerName}`);\n lines.push(\" ports:\");\n lines.push(` - \"${r.port}:6379\"`);\n lines.push(\" volumes:\");\n lines.push(` - ${r.volumeName}:/data`);\n lines.push(\"\");\n }\n\n lines.push(\"volumes:\");\n for (const db of databases) {\n lines.push(` ${db.volumeName}:`);\n lines.push(` name: ${db.volumeName}`);\n }\n for (const r of redis) {\n lines.push(` ${r.volumeName}:`);\n lines.push(` name: ${r.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function materializeInfraPlan(\n configDir: string,\n planEnv: Record<string, string>,\n planDatabases: {\n serviceName: string;\n containerName: string;\n port: number;\n volumeName: string;\n databaseName: string;\n }[],\n planRedis: { serviceName: string; containerName: string; port: number; volumeName: string }[],\n projectName: string,\n devHostPort?: number,\n): { envExampleChanged: boolean; dockerComposeChanged: boolean } {\n const envContent = renderEnvFileFromPlan(planEnv, devHostPort);\n const dockerContent = renderDockerComposeFromPlan(planDatabases, planRedis, projectName);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n return {\n envExampleChanged: syncTextFile(envExamplePath, envContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, dockerContent),\n };\n}\n\nfunction syncTextFile(filePath: string, nextContent: string): boolean {\n if (existsSync(filePath) && readFileSync(filePath, \"utf-8\") === nextContent) {\n return false;\n }\n\n writeFileSync(filePath, nextContent);\n return true;\n}\n\nexport function writeGeneratedInfra(configDir: string, runtimeConfig: RuntimeConfig): string[] {\n const result = syncGeneratedInfra(configDir, runtimeConfig);\n\n if (result.staleEnvWarnings.length > 0) {\n p.log.warn(\n `.env has ${result.staleEnvWarnings.length} stale value(s) compared to .env.example:`,\n );\n for (const warning of result.staleEnvWarnings) {\n p.log.message(` ${warning}`);\n }\n }\n\n return result.secrets;\n}\n\nexport function syncGeneratedInfra(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): SyncGeneratedInfraResult {\n const { spec, portState } = buildGeneratedInfraSpec(runtimeConfig, configDir);\n const secrets = spec.groups.flatMap((group) => group.secrets);\n const envOptions: { forExample: true; devHostPort?: number } = { forExample: true };\n if (runtimeConfig.env === \"development\") {\n envOptions.devHostPort = resolveDevHostPort(runtimeConfig);\n }\n const newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, envOptions);\n const newDockerContent = renderDockerCompose(spec.databases, spec.redis, runtimeConfig.account);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n const staleWarnings = checkEnvStaleness(configDir, spec.databases, spec.redis);\n\n if (configDir) {\n savePortState(configDir, portState);\n }\n\n return {\n secrets,\n envExampleChanged: syncTextFile(envExamplePath, newEnvContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent),\n staleEnvWarnings: staleWarnings,\n };\n}\n\nfunction checkEnvStaleness(\n configDir: string,\n databases: DatabaseSecretConfig[],\n redisConfigs: RedisSecretConfig[],\n): string[] {\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return [];\n\n const existingEnv = readFileSync(envPath, \"utf-8\");\n const envMap = new Map<string, string>();\n for (const line of existingEnv.split(\"\\n\")) {\n const match = line.match(/^([A-Z_]+)=(.*)$/);\n if (match) envMap.set(match[1], match[2]);\n }\n\n const stale: string[] = [];\n\n for (const db of databases) {\n const existing = envMap.get(db.secret);\n if (existing && existing !== db.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${db.secret}: port ${oldPort} → ${db.port}`);\n }\n }\n\n for (const redis of redisConfigs) {\n const existing = envMap.get(redis.secret);\n if (existing && existing !== redis.url) {\n const oldPort = extractPortFromUrl(existing) ?? \"?\";\n stale.push(`${redis.secret}: port ${oldPort} → ${redis.port}`);\n }\n }\n\n return stale;\n}\n\nexport function ensureEnvFile(configDir: string): void {\n const envPath = join(configDir, \".env\");\n const examplePath = join(configDir, \".env.example\");\n\n if (existsSync(envPath) || !existsSync(examplePath)) return;\n\n const content = readFileSync(examplePath, \"utf-8\");\n const lines = content.split(\"\\n\");\n const secret = randomBytes(32).toString(\"base64url\");\n const updated = lines\n .map((line) => {\n if (/^BETTER_AUTH_SECRET=/.test(line)) {\n return `BETTER_AUTH_SECRET=${secret}`;\n }\n return line;\n })\n .join(\"\\n\");\n\n writeFileSync(envPath, updated);\n p.log.info(\"Created .env from generated .env.example with generated BETTER_AUTH_SECRET\");\n}\n\nlet envLoadedDir: string | null = null;\n\nexport function loadProjectEnv(configDir: string): void {\n if (envLoadedDir === configDir) return;\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return;\n\n loadDotenv({ path: envPath, processEnv: process.env, quiet: true });\n envLoadedDir = configDir;\n}\n"],"mappings":"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"}
|
package/dist/config.cjs
CHANGED
|
@@ -402,7 +402,8 @@ async function buildRuntimeConfig(config, baseDir, env, options) {
|
|
|
402
402
|
variables: apiConfig.variables,
|
|
403
403
|
secrets: apiConfig.secrets,
|
|
404
404
|
integrity: apiIsRemote ? apiConfig.integrity : void 0,
|
|
405
|
-
shared: apiConfig.shared
|
|
405
|
+
shared: apiConfig.shared,
|
|
406
|
+
dependsOn: apiConfig.dependsOn ? normalizeStringArray(apiConfig.dependsOn) : void 0
|
|
406
407
|
},
|
|
407
408
|
auth: (() => {
|
|
408
409
|
if (!authConfig || !authRuntime) return void 0;
|
|
@@ -534,6 +535,7 @@ function buildRuntimePluginConfig(pluginId, env, resolved, forceSource) {
|
|
|
534
535
|
variables: normalizeJsonRecord(source.variables),
|
|
535
536
|
secrets: normalizeStringArray(source.secrets),
|
|
536
537
|
shared: source.shared,
|
|
538
|
+
connectSrc: normalizeStringArray(source.connectSrc),
|
|
537
539
|
integrity: runtimeTarget.source === "remote" ? source.integrity : void 0,
|
|
538
540
|
ui: uiRuntime ? {
|
|
539
541
|
name: typeof uiConfig?.name === "string" ? uiConfig.name : `${apiName}-ui`,
|