everything-dev 1.37.0 → 1.39.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/cli/infra.cjs +145 -36
- package/dist/cli/infra.cjs.map +1 -1
- package/dist/cli/infra.mjs +147 -38
- package/dist/cli/infra.mjs.map +1 -1
- package/dist/config.cjs +6 -6
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +1 -0
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +1 -0
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +6 -6
- package/dist/config.mjs.map +1 -1
- package/dist/contract.cjs +1 -0
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +2 -0
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +2 -0
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.meta.cjs +2 -1
- package/dist/contract.meta.cjs.map +1 -1
- package/dist/contract.meta.d.cts +5 -0
- package/dist/contract.meta.d.mts +5 -0
- package/dist/contract.meta.mjs +2 -1
- package/dist/contract.meta.mjs.map +1 -1
- package/dist/contract.mjs +1 -0
- package/dist/contract.mjs.map +1 -1
- package/dist/plugin.cjs +4 -1
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +1 -0
- package/dist/plugin.d.mts +1 -0
- package/dist/plugin.mjs +4 -1
- package/dist/plugin.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/infra.cjs
CHANGED
|
@@ -12,7 +12,8 @@ const POSTGRES_PASSWORD = "everythingdev";
|
|
|
12
12
|
const API_DATABASE_SECRET = "API_DATABASE_URL";
|
|
13
13
|
const AUTH_DATABASE_SECRET = "AUTH_DATABASE_URL";
|
|
14
14
|
const HOST_SECRET = "CORS_ORIGIN";
|
|
15
|
-
const
|
|
15
|
+
const BASE_POSTGRES_PORT = 5434;
|
|
16
|
+
const BASE_REDIS_PORT = 6379;
|
|
16
17
|
function uniqueSecrets(values) {
|
|
17
18
|
const secrets = [];
|
|
18
19
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -23,6 +24,44 @@ function uniqueSecrets(values) {
|
|
|
23
24
|
}
|
|
24
25
|
return secrets;
|
|
25
26
|
}
|
|
27
|
+
function loadPortState(configDir) {
|
|
28
|
+
if (!configDir) return {
|
|
29
|
+
postgresPorts: {},
|
|
30
|
+
redisPorts: {}
|
|
31
|
+
};
|
|
32
|
+
const statePath = (0, node_path.join)(configDir, ".bos", "infra-state.json");
|
|
33
|
+
if (!(0, node_fs.existsSync)(statePath)) return {
|
|
34
|
+
postgresPorts: {},
|
|
35
|
+
redisPorts: {}
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse((0, node_fs.readFileSync)(statePath, "utf-8"));
|
|
39
|
+
return {
|
|
40
|
+
postgresPorts: raw.postgresPorts ?? {},
|
|
41
|
+
redisPorts: raw.redisPorts ?? {}
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
return {
|
|
45
|
+
postgresPorts: {},
|
|
46
|
+
redisPorts: {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function savePortState(configDir, state) {
|
|
51
|
+
const statePath = (0, node_path.join)(configDir, ".bos", "infra-state.json");
|
|
52
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(statePath), { recursive: true });
|
|
53
|
+
(0, node_fs.writeFileSync)(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
54
|
+
}
|
|
55
|
+
function resolvePort(slug, portMap, basePort) {
|
|
56
|
+
if (portMap[slug] !== void 0) return portMap[slug];
|
|
57
|
+
const assigned = Object.values(portMap);
|
|
58
|
+
const next = assigned.length > 0 ? Math.max(...assigned) + 1 : basePort;
|
|
59
|
+
portMap[slug] = next;
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
function normalizeRedisSlug(secret) {
|
|
63
|
+
return secret.replace(/_REDIS_URL$/, "").toLowerCase();
|
|
64
|
+
}
|
|
26
65
|
function getSecretGroups(runtimeConfig) {
|
|
27
66
|
const groups = [];
|
|
28
67
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -47,10 +86,16 @@ function getSecretGroups(runtimeConfig) {
|
|
|
47
86
|
}
|
|
48
87
|
function buildGeneratedInfraSpec(runtimeConfig, configDir) {
|
|
49
88
|
const groups = getSecretGroups(runtimeConfig);
|
|
89
|
+
const allSecrets = groups.flatMap((group) => group.secrets);
|
|
50
90
|
const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : /* @__PURE__ */ new Map();
|
|
91
|
+
const portState = loadPortState(configDir);
|
|
51
92
|
return {
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
spec: {
|
|
94
|
+
groups,
|
|
95
|
+
databases: buildDatabaseConfigs(allSecrets, originMap, portState.postgresPorts),
|
|
96
|
+
redis: buildRedisConfigs(allSecrets, originMap, portState.redisPorts)
|
|
97
|
+
},
|
|
98
|
+
portState
|
|
54
99
|
};
|
|
55
100
|
}
|
|
56
101
|
function normalizeDatabaseSlug(secret) {
|
|
@@ -81,15 +126,18 @@ function buildOriginMap(configDir, runtimeConfig) {
|
|
|
81
126
|
for (const secret of runtimeConfig.host.secrets ?? []) if (!originMap.has(secret)) originMap.set(secret, account);
|
|
82
127
|
return originMap;
|
|
83
128
|
}
|
|
84
|
-
function buildDatabaseConfigs(secrets, originMap) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
function buildDatabaseConfigs(secrets, originMap, portMap) {
|
|
130
|
+
const orderedSecrets = [...uniqueSecrets(secrets.filter((secret) => secret.endsWith("_DATABASE_URL")))];
|
|
131
|
+
for (const secret of orderedSecrets) {
|
|
132
|
+
const slug = normalizeDatabaseSlug(secret);
|
|
133
|
+
if (secret === API_DATABASE_SECRET) portMap[slug] = 5432;
|
|
134
|
+
else if (secret === AUTH_DATABASE_SECRET) portMap[slug] = 5433;
|
|
135
|
+
else resolvePort(slug, portMap, BASE_POSTGRES_PORT);
|
|
136
|
+
}
|
|
137
|
+
return orderedSecrets.map((secret) => {
|
|
90
138
|
const slug = normalizeDatabaseSlug(secret);
|
|
91
139
|
const fromKey = originMap.get(secret) ?? "";
|
|
92
|
-
const port =
|
|
140
|
+
const port = portMap[slug];
|
|
93
141
|
const volumeName = fromKey ? `${fromKey.replace(/\./g, "_")}_postgres_${slug}_data` : `postgres_${slug}_data`;
|
|
94
142
|
const containerName = fromKey ? `${fromKey}-postgres-${slug}` : `postgres-${slug}`;
|
|
95
143
|
return {
|
|
@@ -105,13 +153,38 @@ function buildDatabaseConfigs(secrets, originMap) {
|
|
|
105
153
|
};
|
|
106
154
|
});
|
|
107
155
|
}
|
|
108
|
-
function
|
|
156
|
+
function buildRedisConfigs(secrets, originMap, portMap) {
|
|
157
|
+
const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith("_REDIS_URL")));
|
|
158
|
+
for (const secret of redisSecrets) resolvePort(normalizeRedisSlug(secret), portMap, BASE_REDIS_PORT);
|
|
159
|
+
return redisSecrets.map((secret) => {
|
|
160
|
+
const slug = normalizeRedisSlug(secret);
|
|
161
|
+
const fromKey = originMap.get(secret) ?? "";
|
|
162
|
+
const port = portMap[slug];
|
|
163
|
+
const volumeName = fromKey ? `${fromKey.replace(/\./g, "_")}_redis_${slug}_data` : `redis_${slug}_data`;
|
|
164
|
+
const containerName = fromKey ? `${fromKey}-redis-${slug}` : `redis-${slug}`;
|
|
165
|
+
return {
|
|
166
|
+
secret,
|
|
167
|
+
slug,
|
|
168
|
+
fromKey,
|
|
169
|
+
port,
|
|
170
|
+
serviceName: `redis-${slug.replace(/_/g, "-")}`,
|
|
171
|
+
containerName,
|
|
172
|
+
volumeName,
|
|
173
|
+
url: `redis://localhost:${port}`
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function extractPortFromUrl(url) {
|
|
178
|
+
return url.match(/:(\d{4,5})(?:\/|$)/)?.[1] ?? null;
|
|
179
|
+
}
|
|
180
|
+
function defaultSecretValue(secret, databases, redisConfigs, options) {
|
|
109
181
|
if (secret === "BETTER_AUTH_SECRET") return options.forExample ? "" : (0, node_crypto.randomBytes)(32).toString("base64url");
|
|
110
182
|
if (secret === "CORS_ORIGIN") return "http://localhost:3000";
|
|
111
|
-
return databases.get(secret)?.url ?? "";
|
|
183
|
+
return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? "";
|
|
112
184
|
}
|
|
113
|
-
function renderEnvFile(groups, databases, options) {
|
|
185
|
+
function renderEnvFile(groups, databases, redisConfigs, options) {
|
|
114
186
|
const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));
|
|
187
|
+
const redisMap = new Map(redisConfigs.map((entry) => [entry.secret, entry]));
|
|
115
188
|
const lines = [
|
|
116
189
|
"# Generated from configured bos secrets",
|
|
117
190
|
"# Update values as needed for your local environment",
|
|
@@ -119,28 +192,16 @@ function renderEnvFile(groups, databases, options) {
|
|
|
119
192
|
];
|
|
120
193
|
for (const group of groups) {
|
|
121
194
|
lines.push(`# ${group.section}`);
|
|
122
|
-
for (const secret of group.secrets) lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, options)}`);
|
|
195
|
+
for (const secret of group.secrets) lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);
|
|
123
196
|
lines.push("");
|
|
124
197
|
}
|
|
125
198
|
return `${lines.join("\n")}\n`;
|
|
126
199
|
}
|
|
127
|
-
function renderDockerCompose(databases, projectName) {
|
|
128
|
-
const lines = [
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
" image: postgres:17-alpine",
|
|
133
|
-
" environment: &pg-env",
|
|
134
|
-
` POSTGRES_USER: ${POSTGRES_USER}`,
|
|
135
|
-
` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,
|
|
136
|
-
" healthcheck:",
|
|
137
|
-
" test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]",
|
|
138
|
-
" interval: 3s",
|
|
139
|
-
" timeout: 3s",
|
|
140
|
-
" retries: 5",
|
|
141
|
-
"",
|
|
142
|
-
"services:"
|
|
143
|
-
];
|
|
200
|
+
function renderDockerCompose(databases, redisConfigs, projectName) {
|
|
201
|
+
const lines = [`name: ${projectName}`, ""];
|
|
202
|
+
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", "");
|
|
203
|
+
if (redisConfigs.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", "");
|
|
204
|
+
lines.push("services:");
|
|
144
205
|
for (const database of databases) {
|
|
145
206
|
lines.push(` ${database.serviceName}:`);
|
|
146
207
|
lines.push(" <<: *pg-common");
|
|
@@ -154,11 +215,25 @@ function renderDockerCompose(databases, projectName) {
|
|
|
154
215
|
lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);
|
|
155
216
|
lines.push("");
|
|
156
217
|
}
|
|
218
|
+
for (const redis of redisConfigs) {
|
|
219
|
+
lines.push(` ${redis.serviceName}:`);
|
|
220
|
+
lines.push(" <<: *redis-common");
|
|
221
|
+
lines.push(` container_name: ${redis.containerName}`);
|
|
222
|
+
lines.push(" ports:");
|
|
223
|
+
lines.push(` - "${redis.port}:6379"`);
|
|
224
|
+
lines.push(" volumes:");
|
|
225
|
+
lines.push(` - ${redis.volumeName}:/data`);
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
157
228
|
lines.push("volumes:");
|
|
158
229
|
for (const database of databases) {
|
|
159
230
|
lines.push(` ${database.volumeName}:`);
|
|
160
231
|
lines.push(` name: ${database.volumeName}`);
|
|
161
232
|
}
|
|
233
|
+
for (const redis of redisConfigs) {
|
|
234
|
+
lines.push(` ${redis.volumeName}:`);
|
|
235
|
+
lines.push(` name: ${redis.volumeName}`);
|
|
236
|
+
}
|
|
162
237
|
return `${lines.join("\n")}\n`;
|
|
163
238
|
}
|
|
164
239
|
function syncTextFile(filePath, nextContent) {
|
|
@@ -167,21 +242,55 @@ function syncTextFile(filePath, nextContent) {
|
|
|
167
242
|
return true;
|
|
168
243
|
}
|
|
169
244
|
function writeGeneratedInfra(configDir, runtimeConfig) {
|
|
170
|
-
|
|
245
|
+
const result = syncGeneratedInfra(configDir, runtimeConfig);
|
|
246
|
+
if (result.staleEnvWarnings.length > 0) {
|
|
247
|
+
_clack_prompts.log.warn(`.env has ${result.staleEnvWarnings.length} stale value(s) compared to .env.example:`);
|
|
248
|
+
for (const warning of result.staleEnvWarnings) _clack_prompts.log.message(` ${warning}`);
|
|
249
|
+
}
|
|
250
|
+
return result.secrets;
|
|
171
251
|
}
|
|
172
252
|
function syncGeneratedInfra(configDir, runtimeConfig) {
|
|
173
|
-
const spec = buildGeneratedInfraSpec(runtimeConfig, configDir);
|
|
253
|
+
const { spec, portState } = buildGeneratedInfraSpec(runtimeConfig, configDir);
|
|
174
254
|
const secrets = spec.groups.flatMap((group) => group.secrets);
|
|
175
|
-
const newEnvContent = renderEnvFile(spec.groups, spec.databases, { forExample: true });
|
|
176
|
-
const newDockerContent = renderDockerCompose(spec.databases, runtimeConfig.account);
|
|
255
|
+
const newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, { forExample: true });
|
|
256
|
+
const newDockerContent = renderDockerCompose(spec.databases, spec.redis, runtimeConfig.account);
|
|
177
257
|
const envExamplePath = (0, node_path.join)(configDir, ".env.example");
|
|
178
258
|
const dockerComposePath = (0, node_path.join)(configDir, "docker-compose.yml");
|
|
259
|
+
const staleWarnings = checkEnvStaleness(configDir, spec.databases, spec.redis);
|
|
260
|
+
if (configDir) savePortState(configDir, portState);
|
|
179
261
|
return {
|
|
180
262
|
secrets,
|
|
181
263
|
envExampleChanged: syncTextFile(envExamplePath, newEnvContent),
|
|
182
|
-
dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent)
|
|
264
|
+
dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent),
|
|
265
|
+
staleEnvWarnings: staleWarnings
|
|
183
266
|
};
|
|
184
267
|
}
|
|
268
|
+
function checkEnvStaleness(configDir, databases, redisConfigs) {
|
|
269
|
+
const envPath = (0, node_path.join)(configDir, ".env");
|
|
270
|
+
if (!(0, node_fs.existsSync)(envPath)) return [];
|
|
271
|
+
const existingEnv = (0, node_fs.readFileSync)(envPath, "utf-8");
|
|
272
|
+
const envMap = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const line of existingEnv.split("\n")) {
|
|
274
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/);
|
|
275
|
+
if (match) envMap.set(match[1], match[2]);
|
|
276
|
+
}
|
|
277
|
+
const stale = [];
|
|
278
|
+
for (const db of databases) {
|
|
279
|
+
const existing = envMap.get(db.secret);
|
|
280
|
+
if (existing && existing !== db.url) {
|
|
281
|
+
const oldPort = extractPortFromUrl(existing) ?? "?";
|
|
282
|
+
stale.push(`${db.secret}: port ${oldPort} → ${db.port}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const redis of redisConfigs) {
|
|
286
|
+
const existing = envMap.get(redis.secret);
|
|
287
|
+
if (existing && existing !== redis.url) {
|
|
288
|
+
const oldPort = extractPortFromUrl(existing) ?? "?";
|
|
289
|
+
stale.push(`${redis.secret}: port ${oldPort} → ${redis.port}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return stale;
|
|
293
|
+
}
|
|
185
294
|
function ensureEnvFile(configDir) {
|
|
186
295
|
const envPath = (0, node_path.join)(configDir, ".env");
|
|
187
296
|
const examplePath = (0, node_path.join)(configDir, ".env.example");
|
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, readFileSync, writeFileSync } from \"node:fs\";\nimport { 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_DATABASE_PORT = 5434;\n\ninterface 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\ninterface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface GeneratedInfraSpec {\n groups: SecretGroup[];\n databases: DatabaseSecretConfig[];\n}\n\ninterface SyncGeneratedInfraResult {\n secrets: string[];\n envExampleChanged: boolean;\n dockerComposeChanged: boolean;\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\nfunction 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): GeneratedInfraSpec {\n const groups = getSecretGroups(runtimeConfig);\n const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : new Map();\n const databases = buildDatabaseConfigs(\n groups.flatMap((group) => group.secrets),\n originMap,\n );\n return { groups, databases };\n}\n\nfunction normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nfunction buildOriginMap(configDir: string, runtimeConfig: RuntimeConfig): 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\nfunction buildDatabaseConfigs(\n secrets: string[],\n originMap: Map<string, string>,\n): DatabaseSecretConfig[] {\n const databaseSecrets = uniqueSecrets(\n secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n );\n\n const additionalSecrets = databaseSecrets\n .filter((secret) => secret !== API_DATABASE_SECRET && secret !== AUTH_DATABASE_SECRET)\n .sort((a, b) => a.localeCompare(b));\n\n const orderedSecrets = [API_DATABASE_SECRET, AUTH_DATABASE_SECRET, ...additionalSecrets];\n\n return orderedSecrets.map((secret, index) => {\n const slug = normalizeDatabaseSlug(secret);\n const fromKey = originMap.get(secret) ?? \"\";\n const port =\n secret === API_DATABASE_SECRET\n ? 5432\n : secret === AUTH_DATABASE_SECRET\n ? 5433\n : BASE_DATABASE_PORT + index - 2;\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\nfunction defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseSecretConfig>,\n options: { forExample: boolean },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\n return \"http://localhost:3000\";\n }\n\n return databases.get(secret)?.url ?? \"\";\n}\n\nfunction renderEnvFile(\n groups: SecretGroup[],\n databases: DatabaseSecretConfig[],\n options: { forExample: boolean },\n): string {\n const databaseMap = new Map(databases.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, options)}`);\n }\n lines.push(\"\");\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction renderDockerCompose(databases: DatabaseSecretConfig[], projectName: string): string {\n const lines = [\n `name: ${projectName}`,\n \"\",\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 \"services:\",\n ];\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 lines.push(\"volumes:\");\n for (const database of databases) {\n lines.push(` ${database.volumeName}:`);\n lines.push(` name: ${database.volumeName}`);\n }\n\n return `${lines.join(\"\\n\")}\\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 return syncGeneratedInfra(configDir, runtimeConfig).secrets;\n}\n\nexport function syncGeneratedInfra(\n configDir: string,\n runtimeConfig: RuntimeConfig,\n): SyncGeneratedInfraResult {\n const spec = buildGeneratedInfraSpec(runtimeConfig, configDir);\n const secrets = spec.groups.flatMap((group) => group.secrets);\n const newEnvContent = renderEnvFile(spec.groups, spec.databases, { forExample: true });\n const newDockerContent = renderDockerCompose(spec.databases, runtimeConfig.account);\n\n const envExamplePath = join(configDir, \".env.example\");\n const dockerComposePath = join(configDir, \"docker-compose.yml\");\n\n return {\n secrets,\n envExampleChanged: syncTextFile(envExamplePath, newEnvContent),\n dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent),\n };\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\nexport function loadProjectEnv(configDir: string): void {\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return;\n\n loadDotenv({ path: envPath, processEnv: process.env, quiet: true });\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\ninterface 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\ninterface 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\ninterface SecretGroup {\n section: string;\n secrets: string[];\n}\n\ninterface PortState {\n postgresPorts: Record<string, number>;\n redisPorts: Record<string, number>;\n}\n\ninterface 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\nfunction 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 };\n } catch {\n return { postgresPorts: {}, redisPorts: {} };\n }\n}\n\nfunction 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\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 next = assigned.length > 0 ? Math.max(...assigned) + 1 : basePort;\n portMap[slug] = next;\n return next;\n}\n\nfunction normalizeRedisSlug(secret: string): string {\n return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nfunction 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\nfunction normalizeDatabaseSlug(secret: string): string {\n return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nfunction buildOriginMap(configDir: string, runtimeConfig: RuntimeConfig): 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\nfunction 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 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\nfunction 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\n for (const secret of redisSecrets) {\n const slug = normalizeRedisSlug(secret);\n resolvePort(slug, portMap, BASE_REDIS_PORT);\n }\n\n return redisSecrets.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 defaultSecretValue(\n secret: string,\n databases: Map<string, DatabaseSecretConfig>,\n redisConfigs: Map<string, RedisSecretConfig>,\n options: { forExample: boolean },\n): string {\n if (secret === \"BETTER_AUTH_SECRET\") {\n return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n }\n\n if (secret === \"CORS_ORIGIN\") {\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 },\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\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 newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, {\n forExample: true,\n });\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\nexport function loadProjectEnv(configDir: string): void {\n const envPath = join(configDir, \".env\");\n if (!existsSync(envPath)) return;\n\n loadDotenv({ path: envPath, processEnv: process.env, quiet: true });\n}\n"],"mappings":"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"}
|
package/dist/cli/infra.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import { config } from "dotenv";
|
|
@@ -10,7 +10,8 @@ const POSTGRES_PASSWORD = "everythingdev";
|
|
|
10
10
|
const API_DATABASE_SECRET = "API_DATABASE_URL";
|
|
11
11
|
const AUTH_DATABASE_SECRET = "AUTH_DATABASE_URL";
|
|
12
12
|
const HOST_SECRET = "CORS_ORIGIN";
|
|
13
|
-
const
|
|
13
|
+
const BASE_POSTGRES_PORT = 5434;
|
|
14
|
+
const BASE_REDIS_PORT = 6379;
|
|
14
15
|
function uniqueSecrets(values) {
|
|
15
16
|
const secrets = [];
|
|
16
17
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -21,6 +22,44 @@ function uniqueSecrets(values) {
|
|
|
21
22
|
}
|
|
22
23
|
return secrets;
|
|
23
24
|
}
|
|
25
|
+
function loadPortState(configDir) {
|
|
26
|
+
if (!configDir) return {
|
|
27
|
+
postgresPorts: {},
|
|
28
|
+
redisPorts: {}
|
|
29
|
+
};
|
|
30
|
+
const statePath = join(configDir, ".bos", "infra-state.json");
|
|
31
|
+
if (!existsSync(statePath)) return {
|
|
32
|
+
postgresPorts: {},
|
|
33
|
+
redisPorts: {}
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
37
|
+
return {
|
|
38
|
+
postgresPorts: raw.postgresPorts ?? {},
|
|
39
|
+
redisPorts: raw.redisPorts ?? {}
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
return {
|
|
43
|
+
postgresPorts: {},
|
|
44
|
+
redisPorts: {}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function savePortState(configDir, state) {
|
|
49
|
+
const statePath = join(configDir, ".bos", "infra-state.json");
|
|
50
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
51
|
+
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
52
|
+
}
|
|
53
|
+
function resolvePort(slug, portMap, basePort) {
|
|
54
|
+
if (portMap[slug] !== void 0) return portMap[slug];
|
|
55
|
+
const assigned = Object.values(portMap);
|
|
56
|
+
const next = assigned.length > 0 ? Math.max(...assigned) + 1 : basePort;
|
|
57
|
+
portMap[slug] = next;
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
function normalizeRedisSlug(secret) {
|
|
61
|
+
return secret.replace(/_REDIS_URL$/, "").toLowerCase();
|
|
62
|
+
}
|
|
24
63
|
function getSecretGroups(runtimeConfig) {
|
|
25
64
|
const groups = [];
|
|
26
65
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -45,10 +84,16 @@ function getSecretGroups(runtimeConfig) {
|
|
|
45
84
|
}
|
|
46
85
|
function buildGeneratedInfraSpec(runtimeConfig, configDir) {
|
|
47
86
|
const groups = getSecretGroups(runtimeConfig);
|
|
87
|
+
const allSecrets = groups.flatMap((group) => group.secrets);
|
|
48
88
|
const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : /* @__PURE__ */ new Map();
|
|
89
|
+
const portState = loadPortState(configDir);
|
|
49
90
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
91
|
+
spec: {
|
|
92
|
+
groups,
|
|
93
|
+
databases: buildDatabaseConfigs(allSecrets, originMap, portState.postgresPorts),
|
|
94
|
+
redis: buildRedisConfigs(allSecrets, originMap, portState.redisPorts)
|
|
95
|
+
},
|
|
96
|
+
portState
|
|
52
97
|
};
|
|
53
98
|
}
|
|
54
99
|
function normalizeDatabaseSlug(secret) {
|
|
@@ -79,15 +124,18 @@ function buildOriginMap(configDir, runtimeConfig) {
|
|
|
79
124
|
for (const secret of runtimeConfig.host.secrets ?? []) if (!originMap.has(secret)) originMap.set(secret, account);
|
|
80
125
|
return originMap;
|
|
81
126
|
}
|
|
82
|
-
function buildDatabaseConfigs(secrets, originMap) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
127
|
+
function buildDatabaseConfigs(secrets, originMap, portMap) {
|
|
128
|
+
const orderedSecrets = [...uniqueSecrets(secrets.filter((secret) => secret.endsWith("_DATABASE_URL")))];
|
|
129
|
+
for (const secret of orderedSecrets) {
|
|
130
|
+
const slug = normalizeDatabaseSlug(secret);
|
|
131
|
+
if (secret === API_DATABASE_SECRET) portMap[slug] = 5432;
|
|
132
|
+
else if (secret === AUTH_DATABASE_SECRET) portMap[slug] = 5433;
|
|
133
|
+
else resolvePort(slug, portMap, BASE_POSTGRES_PORT);
|
|
134
|
+
}
|
|
135
|
+
return orderedSecrets.map((secret) => {
|
|
88
136
|
const slug = normalizeDatabaseSlug(secret);
|
|
89
137
|
const fromKey = originMap.get(secret) ?? "";
|
|
90
|
-
const port =
|
|
138
|
+
const port = portMap[slug];
|
|
91
139
|
const volumeName = fromKey ? `${fromKey.replace(/\./g, "_")}_postgres_${slug}_data` : `postgres_${slug}_data`;
|
|
92
140
|
const containerName = fromKey ? `${fromKey}-postgres-${slug}` : `postgres-${slug}`;
|
|
93
141
|
return {
|
|
@@ -103,13 +151,38 @@ function buildDatabaseConfigs(secrets, originMap) {
|
|
|
103
151
|
};
|
|
104
152
|
});
|
|
105
153
|
}
|
|
106
|
-
function
|
|
154
|
+
function buildRedisConfigs(secrets, originMap, portMap) {
|
|
155
|
+
const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith("_REDIS_URL")));
|
|
156
|
+
for (const secret of redisSecrets) resolvePort(normalizeRedisSlug(secret), portMap, BASE_REDIS_PORT);
|
|
157
|
+
return redisSecrets.map((secret) => {
|
|
158
|
+
const slug = normalizeRedisSlug(secret);
|
|
159
|
+
const fromKey = originMap.get(secret) ?? "";
|
|
160
|
+
const port = portMap[slug];
|
|
161
|
+
const volumeName = fromKey ? `${fromKey.replace(/\./g, "_")}_redis_${slug}_data` : `redis_${slug}_data`;
|
|
162
|
+
const containerName = fromKey ? `${fromKey}-redis-${slug}` : `redis-${slug}`;
|
|
163
|
+
return {
|
|
164
|
+
secret,
|
|
165
|
+
slug,
|
|
166
|
+
fromKey,
|
|
167
|
+
port,
|
|
168
|
+
serviceName: `redis-${slug.replace(/_/g, "-")}`,
|
|
169
|
+
containerName,
|
|
170
|
+
volumeName,
|
|
171
|
+
url: `redis://localhost:${port}`
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function extractPortFromUrl(url) {
|
|
176
|
+
return url.match(/:(\d{4,5})(?:\/|$)/)?.[1] ?? null;
|
|
177
|
+
}
|
|
178
|
+
function defaultSecretValue(secret, databases, redisConfigs, options) {
|
|
107
179
|
if (secret === "BETTER_AUTH_SECRET") return options.forExample ? "" : randomBytes(32).toString("base64url");
|
|
108
180
|
if (secret === "CORS_ORIGIN") return "http://localhost:3000";
|
|
109
|
-
return databases.get(secret)?.url ?? "";
|
|
181
|
+
return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? "";
|
|
110
182
|
}
|
|
111
|
-
function renderEnvFile(groups, databases, options) {
|
|
183
|
+
function renderEnvFile(groups, databases, redisConfigs, options) {
|
|
112
184
|
const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));
|
|
185
|
+
const redisMap = new Map(redisConfigs.map((entry) => [entry.secret, entry]));
|
|
113
186
|
const lines = [
|
|
114
187
|
"# Generated from configured bos secrets",
|
|
115
188
|
"# Update values as needed for your local environment",
|
|
@@ -117,28 +190,16 @@ function renderEnvFile(groups, databases, options) {
|
|
|
117
190
|
];
|
|
118
191
|
for (const group of groups) {
|
|
119
192
|
lines.push(`# ${group.section}`);
|
|
120
|
-
for (const secret of group.secrets) lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, options)}`);
|
|
193
|
+
for (const secret of group.secrets) lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);
|
|
121
194
|
lines.push("");
|
|
122
195
|
}
|
|
123
196
|
return `${lines.join("\n")}\n`;
|
|
124
197
|
}
|
|
125
|
-
function renderDockerCompose(databases, projectName) {
|
|
126
|
-
const lines = [
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
" image: postgres:17-alpine",
|
|
131
|
-
" environment: &pg-env",
|
|
132
|
-
` POSTGRES_USER: ${POSTGRES_USER}`,
|
|
133
|
-
` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,
|
|
134
|
-
" healthcheck:",
|
|
135
|
-
" test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]",
|
|
136
|
-
" interval: 3s",
|
|
137
|
-
" timeout: 3s",
|
|
138
|
-
" retries: 5",
|
|
139
|
-
"",
|
|
140
|
-
"services:"
|
|
141
|
-
];
|
|
198
|
+
function renderDockerCompose(databases, redisConfigs, projectName) {
|
|
199
|
+
const lines = [`name: ${projectName}`, ""];
|
|
200
|
+
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", "");
|
|
201
|
+
if (redisConfigs.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", "");
|
|
202
|
+
lines.push("services:");
|
|
142
203
|
for (const database of databases) {
|
|
143
204
|
lines.push(` ${database.serviceName}:`);
|
|
144
205
|
lines.push(" <<: *pg-common");
|
|
@@ -152,11 +213,25 @@ function renderDockerCompose(databases, projectName) {
|
|
|
152
213
|
lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);
|
|
153
214
|
lines.push("");
|
|
154
215
|
}
|
|
216
|
+
for (const redis of redisConfigs) {
|
|
217
|
+
lines.push(` ${redis.serviceName}:`);
|
|
218
|
+
lines.push(" <<: *redis-common");
|
|
219
|
+
lines.push(` container_name: ${redis.containerName}`);
|
|
220
|
+
lines.push(" ports:");
|
|
221
|
+
lines.push(` - "${redis.port}:6379"`);
|
|
222
|
+
lines.push(" volumes:");
|
|
223
|
+
lines.push(` - ${redis.volumeName}:/data`);
|
|
224
|
+
lines.push("");
|
|
225
|
+
}
|
|
155
226
|
lines.push("volumes:");
|
|
156
227
|
for (const database of databases) {
|
|
157
228
|
lines.push(` ${database.volumeName}:`);
|
|
158
229
|
lines.push(` name: ${database.volumeName}`);
|
|
159
230
|
}
|
|
231
|
+
for (const redis of redisConfigs) {
|
|
232
|
+
lines.push(` ${redis.volumeName}:`);
|
|
233
|
+
lines.push(` name: ${redis.volumeName}`);
|
|
234
|
+
}
|
|
160
235
|
return `${lines.join("\n")}\n`;
|
|
161
236
|
}
|
|
162
237
|
function syncTextFile(filePath, nextContent) {
|
|
@@ -165,21 +240,55 @@ function syncTextFile(filePath, nextContent) {
|
|
|
165
240
|
return true;
|
|
166
241
|
}
|
|
167
242
|
function writeGeneratedInfra(configDir, runtimeConfig) {
|
|
168
|
-
|
|
243
|
+
const result = syncGeneratedInfra(configDir, runtimeConfig);
|
|
244
|
+
if (result.staleEnvWarnings.length > 0) {
|
|
245
|
+
p.log.warn(`.env has ${result.staleEnvWarnings.length} stale value(s) compared to .env.example:`);
|
|
246
|
+
for (const warning of result.staleEnvWarnings) p.log.message(` ${warning}`);
|
|
247
|
+
}
|
|
248
|
+
return result.secrets;
|
|
169
249
|
}
|
|
170
250
|
function syncGeneratedInfra(configDir, runtimeConfig) {
|
|
171
|
-
const spec = buildGeneratedInfraSpec(runtimeConfig, configDir);
|
|
251
|
+
const { spec, portState } = buildGeneratedInfraSpec(runtimeConfig, configDir);
|
|
172
252
|
const secrets = spec.groups.flatMap((group) => group.secrets);
|
|
173
|
-
const newEnvContent = renderEnvFile(spec.groups, spec.databases, { forExample: true });
|
|
174
|
-
const newDockerContent = renderDockerCompose(spec.databases, runtimeConfig.account);
|
|
253
|
+
const newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, { forExample: true });
|
|
254
|
+
const newDockerContent = renderDockerCompose(spec.databases, spec.redis, runtimeConfig.account);
|
|
175
255
|
const envExamplePath = join(configDir, ".env.example");
|
|
176
256
|
const dockerComposePath = join(configDir, "docker-compose.yml");
|
|
257
|
+
const staleWarnings = checkEnvStaleness(configDir, spec.databases, spec.redis);
|
|
258
|
+
if (configDir) savePortState(configDir, portState);
|
|
177
259
|
return {
|
|
178
260
|
secrets,
|
|
179
261
|
envExampleChanged: syncTextFile(envExamplePath, newEnvContent),
|
|
180
|
-
dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent)
|
|
262
|
+
dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent),
|
|
263
|
+
staleEnvWarnings: staleWarnings
|
|
181
264
|
};
|
|
182
265
|
}
|
|
266
|
+
function checkEnvStaleness(configDir, databases, redisConfigs) {
|
|
267
|
+
const envPath = join(configDir, ".env");
|
|
268
|
+
if (!existsSync(envPath)) return [];
|
|
269
|
+
const existingEnv = readFileSync(envPath, "utf-8");
|
|
270
|
+
const envMap = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const line of existingEnv.split("\n")) {
|
|
272
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/);
|
|
273
|
+
if (match) envMap.set(match[1], match[2]);
|
|
274
|
+
}
|
|
275
|
+
const stale = [];
|
|
276
|
+
for (const db of databases) {
|
|
277
|
+
const existing = envMap.get(db.secret);
|
|
278
|
+
if (existing && existing !== db.url) {
|
|
279
|
+
const oldPort = extractPortFromUrl(existing) ?? "?";
|
|
280
|
+
stale.push(`${db.secret}: port ${oldPort} → ${db.port}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const redis of redisConfigs) {
|
|
284
|
+
const existing = envMap.get(redis.secret);
|
|
285
|
+
if (existing && existing !== redis.url) {
|
|
286
|
+
const oldPort = extractPortFromUrl(existing) ?? "?";
|
|
287
|
+
stale.push(`${redis.secret}: port ${oldPort} → ${redis.port}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return stale;
|
|
291
|
+
}
|
|
183
292
|
function ensureEnvFile(configDir) {
|
|
184
293
|
const envPath = join(configDir, ".env");
|
|
185
294
|
const examplePath = join(configDir, ".env.example");
|