@geekmidas/cli 10.0.0-alpha.25 → 10.0.0-alpha.27

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.
Files changed (59) hide show
  1. package/dist/{config-Dn9DW2Fz.cjs → config-7R_-WyHq.cjs} +24 -3
  2. package/dist/{config-Dn9DW2Fz.cjs.map → config-7R_-WyHq.cjs.map} +1 -1
  3. package/dist/{config-DBGpRTUy.mjs → config-DXrrYvob.mjs} +19 -4
  4. package/dist/{config-DBGpRTUy.mjs.map → config-DXrrYvob.mjs.map} +1 -1
  5. package/dist/config.cjs +3 -2
  6. package/dist/config.d.cts +12 -1
  7. package/dist/config.d.cts.map +1 -1
  8. package/dist/config.d.mts +12 -1
  9. package/dist/config.d.mts.map +1 -1
  10. package/dist/config.mjs +3 -3
  11. package/dist/{github-BOaa3kQR.mjs → github-Ba5kXEJo.mjs} +4 -4
  12. package/dist/{github-BOaa3kQR.mjs.map → github-Ba5kXEJo.mjs.map} +1 -1
  13. package/dist/{github-CUjW3G91.cjs → github-CQflOKKV.cjs} +4 -4
  14. package/dist/{github-CUjW3G91.cjs.map → github-CQflOKKV.cjs.map} +1 -1
  15. package/dist/{index-RCUizGxL.d.cts → index-BPN7kghQ.d.cts} +2 -12
  16. package/dist/{index-B4H4XF7A.d.mts.map → index-BPN7kghQ.d.cts.map} +1 -1
  17. package/dist/{index-B4H4XF7A.d.mts → index-BqWKWRm0.d.mts} +2 -12
  18. package/dist/{index-RCUizGxL.d.cts.map → index-BqWKWRm0.d.mts.map} +1 -1
  19. package/dist/index.cjs +170 -169
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +123 -122
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{openapi-DHQlstv6.mjs → openapi-3NLVvtz-.mjs} +2 -2
  24. package/dist/{openapi-DHQlstv6.mjs.map → openapi-3NLVvtz-.mjs.map} +1 -1
  25. package/dist/{openapi-BAKQLho-.cjs → openapi-rhJdy2GC.cjs} +2 -2
  26. package/dist/{openapi-BAKQLho-.cjs.map → openapi-rhJdy2GC.cjs.map} +1 -1
  27. package/dist/openapi.cjs +1 -1
  28. package/dist/openapi.mjs +1 -1
  29. package/dist/{generator-LfZU7NlU.mjs → reconcile-DhbCvh4K.mjs} +111 -2
  30. package/dist/reconcile-DhbCvh4K.mjs.map +1 -0
  31. package/dist/{generator-Wdfp0jCS.cjs → reconcile-szmJw_Sb.cjs} +130 -3
  32. package/dist/reconcile-szmJw_Sb.cjs.map +1 -0
  33. package/dist/{transfer-B88fTvnh.mjs → transfer-BV00TxZv.mjs} +4 -4
  34. package/dist/{transfer-B88fTvnh.mjs.map → transfer-BV00TxZv.mjs.map} +1 -1
  35. package/dist/{transfer-D59M4aaR.cjs → transfer-BYIn7z8m.cjs} +4 -4
  36. package/dist/{transfer-D59M4aaR.cjs.map → transfer-BYIn7z8m.cjs.map} +1 -1
  37. package/dist/workspace/index.cjs +1 -2
  38. package/dist/workspace/index.d.cts +2 -2
  39. package/dist/workspace/index.d.mts +2 -2
  40. package/dist/workspace/index.mjs +2 -2
  41. package/dist/{workspace-CRD9YinV.cjs → workspace-Bav3TVrP.cjs} +47 -78
  42. package/dist/workspace-Bav3TVrP.cjs.map +1 -0
  43. package/dist/{credentials-BpoOUa74.mjs → workspace-CUmR_k4J.mjs} +228 -492
  44. package/dist/workspace-CUmR_k4J.mjs.map +1 -0
  45. package/dist/{credentials-Dbyccl3A.cjs → workspace-DgeVgxmm.cjs} +229 -517
  46. package/dist/workspace-DgeVgxmm.cjs.map +1 -0
  47. package/dist/{workspace-Bu3xNCej.mjs → workspace-T4IVGSoB.mjs} +48 -73
  48. package/dist/workspace-T4IVGSoB.mjs.map +1 -0
  49. package/package.json +14 -14
  50. package/dist/credentials-BpoOUa74.mjs.map +0 -1
  51. package/dist/credentials-Dbyccl3A.cjs.map +0 -1
  52. package/dist/generator-LfZU7NlU.mjs.map +0 -1
  53. package/dist/generator-Wdfp0jCS.cjs.map +0 -1
  54. package/dist/reconcile-gb1lz1Dk.mjs +0 -113
  55. package/dist/reconcile-gb1lz1Dk.mjs.map +0 -1
  56. package/dist/reconcile-hx_vBvux.cjs +0 -136
  57. package/dist/reconcile-hx_vBvux.cjs.map +0 -1
  58. package/dist/workspace-Bu3xNCej.mjs.map +0 -1
  59. package/dist/workspace-CRD9YinV.cjs.map +0 -1
@@ -1,23 +1,242 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
2
  import { a as DEFAULT_EVENTS$1, i as DEFAULT_CACHE, o as DEFAULT_STORAGE, t as discover } from "./discover-0fhCbnk0.mjs";
3
- import { a as loadWorkspaceAppInfo, c as appKey, n as getAppNameFromCwd, o as loadWorkspaceConfig } from "./config-DBGpRTUy.mjs";
4
- import { s as getDependencyEnvVars, t as allConstructGlobs, w as TEST_STAGE } from "./workspace-Bu3xNCej.mjs";
5
- import { r as generatePgBossUrl } from "./generator-LfZU7NlU.mjs";
3
+ import { l as appKey, o as loadWorkspaceAppInfo, r as getAppNameFromCwd, s as loadWorkspaceConfig } from "./config-DXrrYvob.mjs";
4
+ import { E as TEST_STAGE, t as allConstructGlobs } from "./workspace-T4IVGSoB.mjs";
6
5
  import { n as readStageSecrets, o as toEmbeddableSecrets, r as secretsExist } from "./storage-TP0x118M.mjs";
7
- import { existsSync, readFileSync } from "node:fs";
6
+ import { existsSync } from "node:fs";
8
7
  import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
9
8
  import { dirname, join, resolve } from "node:path";
10
- import { execFile, execSync } from "node:child_process";
9
+ import { execFile } from "node:child_process";
11
10
  import { DEFAULT_POSTGRES_VERSION, cacheTable, cookieDomain, dependentsOf, provideKey, providedKeyFor, provisionOrder, publicEnvFor } from "@geekmidas/manifest";
12
11
  import { createServer } from "node:net";
13
12
  import { config } from "dotenv";
14
- import { parse as parse$1, stringify } from "yaml";
15
13
  import { createHash } from "node:crypto";
16
14
  import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
17
15
  import { CreateBucketCommand, GetBucketPolicyCommand, HeadBucketCommand, PutBucketPolicyCommand, S3Client } from "@aws-sdk/client-s3";
18
16
  import { Client } from "pg";
17
+ import { stringify } from "yaml";
19
18
  import { promisify } from "node:util";
20
19
  import { cacheTableStatements } from "@geekmidas/cache/postgres";
20
+ //#region src/credentials/index.ts
21
+ const logger = console;
22
+ /**
23
+ * Load environment files
24
+ * @internal Exported for testing
25
+ */
26
+ function loadEnvFiles(envConfig, cwd = process.cwd()) {
27
+ const loaded = [];
28
+ const missing = [];
29
+ const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
30
+ for (const envFile of envFiles) {
31
+ const envPath = resolve(cwd, envFile);
32
+ if (existsSync(envPath)) {
33
+ config({
34
+ path: envPath,
35
+ override: true,
36
+ quiet: true
37
+ });
38
+ loaded.push(envFile);
39
+ } else if (envConfig) missing.push(envFile);
40
+ }
41
+ return {
42
+ loaded,
43
+ missing
44
+ };
45
+ }
46
+ /**
47
+ * Check if a port is available
48
+ * @internal Exported for testing
49
+ */
50
+ async function isPortAvailable(port) {
51
+ return new Promise((resolve) => {
52
+ const server = createServer();
53
+ server.once("error", (err) => {
54
+ if (err.code === "EADDRINUSE") resolve(false);
55
+ else resolve(false);
56
+ });
57
+ server.once("listening", () => {
58
+ server.close();
59
+ resolve(true);
60
+ });
61
+ server.listen(port);
62
+ });
63
+ }
64
+ /**
65
+ * Find an available port starting from the preferred port
66
+ * @internal Exported for testing
67
+ */
68
+ async function findAvailablePort(preferredPort, maxAttempts = 10) {
69
+ for (let i = 0; i < maxAttempts; i++) {
70
+ const port = preferredPort + i;
71
+ if (await isPortAvailable(port)) return port;
72
+ logger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
73
+ }
74
+ throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
75
+ }
76
+ const PORT_STATE_PATH = ".gkm/ports.json";
77
+ /**
78
+ * Load saved port state from .gkm/ports.json.
79
+ * @internal Exported for testing
80
+ */
81
+ async function loadPortState(workspaceRoot) {
82
+ try {
83
+ const raw = await readFile(join(workspaceRoot, PORT_STATE_PATH), "utf-8");
84
+ return JSON.parse(raw);
85
+ } catch {
86
+ return {};
87
+ }
88
+ }
89
+ /**
90
+ * Save port state to .gkm/ports.json.
91
+ * @internal Exported for testing
92
+ */
93
+ async function savePortState(workspaceRoot, ports) {
94
+ const dir = join(workspaceRoot, ".gkm");
95
+ await mkdir(dir, { recursive: true });
96
+ await writeFile(join(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
97
+ }
98
+ /**
99
+ * Load and flatten secrets for an app from encrypted storage.
100
+ * For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
101
+ * @internal Exported for testing
102
+ */
103
+ async function loadSecretsForApp(secretsRoot, stage, appName) {
104
+ let secrets = {};
105
+ if (secretsExist(stage, secretsRoot)) {
106
+ const stageSecrets = await readStageSecrets(stage, secretsRoot);
107
+ if (stageSecrets) {
108
+ logger.log(`🔐 Loading secrets from stage: ${stage}`);
109
+ secrets = toEmbeddableSecrets(stageSecrets);
110
+ }
111
+ }
112
+ if (Object.keys(secrets).length === 0) return {};
113
+ if (!appName) return secrets;
114
+ const prefix = appName.toUpperCase();
115
+ const mapped = { ...secrets };
116
+ const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
117
+ if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
118
+ return mapped;
119
+ }
120
+ /**
121
+ * Walk up the directory tree to find the root containing .gkm/secrets/.
122
+ * @internal Exported for testing
123
+ */
124
+ function findSecretsRoot(startDir) {
125
+ let dir = startDir;
126
+ while (dir !== "/") {
127
+ if (existsSync(join(dir, ".gkm", "secrets"))) return dir;
128
+ const parent = dirname(dir);
129
+ if (parent === dir) break;
130
+ dir = parent;
131
+ }
132
+ return startDir;
133
+ }
134
+ /**
135
+ * Generate the credentials injection code snippet.
136
+ * This is the common logic used by both entry wrapper and exec preload.
137
+ * @internal
138
+ */
139
+ function generateCredentialsInjection(secretsJsonPath) {
140
+ return `import { existsSync, readFileSync } from 'node:fs';
141
+
142
+ // Inject dev secrets via globalThis and process.env
143
+ // Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
144
+ // Object.assign on the Credentials export only mutates one module copy.
145
+ const secretsPath = '${secretsJsonPath}';
146
+ if (existsSync(secretsPath)) {
147
+ const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
148
+ globalThis.__gkm_credentials__ = secrets;
149
+ Object.assign(process.env, secrets);
150
+ }
151
+ `;
152
+ }
153
+ /**
154
+ * Create a preload script that injects secrets into Credentials.
155
+ * Used by `gkm exec` to inject secrets before running any command.
156
+ * @internal Exported for testing
157
+ */
158
+ async function createCredentialsPreload(preloadPath, secretsJsonPath) {
159
+ const content = `/**
160
+ * Credentials preload generated by 'gkm exec'
161
+ * This file is loaded via NODE_OPTIONS="--import <path>"
162
+ */
163
+ ${generateCredentialsInjection(secretsJsonPath)}`;
164
+ await writeFile(preloadPath, content);
165
+ }
166
+ /**
167
+ * Create a wrapper script that injects secrets before importing the entry file.
168
+ * @internal Exported for testing
169
+ */
170
+ async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
171
+ const content = `#!/usr/bin/env node
172
+ /**
173
+ * Entry wrapper generated by 'gkm dev --entry'
174
+ */
175
+ ${secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
176
+ ` : ""}// Import and run the user's entry file (dynamic import ensures secrets load first)
177
+ await import('${entryPath}');
178
+ `;
179
+ await writeFile(wrapperPath, content);
180
+ }
181
+ /**
182
+ * Prepare credentials for dev/exec/test modes.
183
+ * Loads workspace config, secrets, resolves Docker ports, rewrites URLs,
184
+ * injects PORT, dependency URLs, and writes credentials JSON.
185
+ *
186
+ * @param options.stage - The stage whose secrets to load. Default: the project's `stages.local`.
187
+ * @param options.startDocker - Start Docker Compose services after port resolution. Default: false.
188
+ * @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.
189
+ * @internal Exported for testing
190
+ */
191
+ async function prepareEntryCredentials(options) {
192
+ const cwd = options.cwd ?? process.cwd();
193
+ let workspaceAppPort;
194
+ let secretsRoot = cwd;
195
+ let appName;
196
+ let appInfo;
197
+ let workspace;
198
+ try {
199
+ appInfo = await loadWorkspaceAppInfo(cwd);
200
+ workspace = appInfo.workspace;
201
+ workspaceAppPort = appInfo.app.port;
202
+ secretsRoot = appInfo.workspaceRoot;
203
+ appName = appInfo.appName;
204
+ } catch {
205
+ workspace = await loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace).catch(() => void 0);
206
+ secretsRoot = workspace?.root ?? findSecretsRoot(cwd);
207
+ appName = getAppNameFromCwd(cwd) ?? void 0;
208
+ }
209
+ const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
210
+ const stage = options.stage ?? workspace?.stages.local;
211
+ const credentials = stage ? await loadSecretsForApp(secretsRoot, stage, appName) : {};
212
+ credentials.PORT = String(resolvedPort);
213
+ const declaredKeys = [];
214
+ if (workspace) {
215
+ const { reconcileWorkspace } = await Promise.resolve().then(() => workspace_exports);
216
+ const reconciled = await reconcileWorkspace(workspace, {
217
+ stage: options.reconcileStage ?? workspace.stages.local,
218
+ start: options.startDocker ?? false
219
+ });
220
+ Object.assign(credentials, reconciled.env);
221
+ declaredKeys.push(...Object.keys(reconciled.env));
222
+ if (Object.keys(reconciled.env).length > 0) logger.log(`🔌 Resolved ${Object.keys(reconciled.env).length} declared URL(s)`);
223
+ }
224
+ const secretsDir = join(secretsRoot, ".gkm");
225
+ await mkdir(secretsDir, { recursive: true });
226
+ const secretsFileName = options.secretsFileName ?? (appName ? `dev-secrets-${appName}.json` : "dev-secrets.json");
227
+ const secretsJsonPath = join(secretsDir, secretsFileName);
228
+ await writeFile(secretsJsonPath, JSON.stringify(credentials, null, 2));
229
+ return {
230
+ credentials,
231
+ resolvedPort,
232
+ secretsJsonPath,
233
+ appName,
234
+ secretsRoot,
235
+ appInfo,
236
+ declaredKeys
237
+ };
238
+ }
239
+ //#endregion
21
240
  //#region src/workspace/backends.ts
22
241
  /**
23
242
  * Which backend each declared kind resolves to — decided by where the project
@@ -2235,8 +2454,7 @@ var workspace_exports = /* @__PURE__ */ __exportAll({
2235
2454
  constructGlobs: () => constructGlobs,
2236
2455
  derivedContainers: () => derivedContainers,
2237
2456
  reconcileWorkspace: () => reconcileWorkspace,
2238
- surfaceAddresses: () => surfaceAddresses,
2239
- usesConstructs: () => usesConstructs
2457
+ surfaceAddresses: () => surfaceAddresses
2240
2458
  });
2241
2459
  /**
2242
2460
  * Every constructs glob in the workspace, resolved against its app.
@@ -2247,10 +2465,6 @@ var workspace_exports = /* @__PURE__ */ __exportAll({
2247
2465
  function constructGlobs(workspace) {
2248
2466
  return allConstructGlobs(workspace);
2249
2467
  }
2250
- /** Whether any app has adopted the constructs glob. */
2251
- function usesConstructs(workspace) {
2252
- return constructGlobs(workspace).length > 0;
2253
- }
2254
2468
  /** The backends a workspace's target implies, as the plan takes them. */
2255
2469
  function backendsOf(workspace) {
2256
2470
  const on = providerOf(workspace);
@@ -2336,484 +2550,6 @@ function localAddress(port) {
2336
2550
  return `http://localhost:${port}`;
2337
2551
  }
2338
2552
  //#endregion
2339
- //#region src/credentials/index.ts
2340
- const logger = console;
2341
- /**
2342
- * Load environment files
2343
- * @internal Exported for testing
2344
- */
2345
- function loadEnvFiles(envConfig, cwd = process.cwd()) {
2346
- const loaded = [];
2347
- const missing = [];
2348
- const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
2349
- for (const envFile of envFiles) {
2350
- const envPath = resolve(cwd, envFile);
2351
- if (existsSync(envPath)) {
2352
- config({
2353
- path: envPath,
2354
- override: true,
2355
- quiet: true
2356
- });
2357
- loaded.push(envFile);
2358
- } else if (envConfig) missing.push(envFile);
2359
- }
2360
- return {
2361
- loaded,
2362
- missing
2363
- };
2364
- }
2365
- /**
2366
- * Check if a port is available
2367
- * @internal Exported for testing
2368
- */
2369
- async function isPortAvailable(port) {
2370
- return new Promise((resolve) => {
2371
- const server = createServer();
2372
- server.once("error", (err) => {
2373
- if (err.code === "EADDRINUSE") resolve(false);
2374
- else resolve(false);
2375
- });
2376
- server.once("listening", () => {
2377
- server.close();
2378
- resolve(true);
2379
- });
2380
- server.listen(port);
2381
- });
2382
- }
2383
- /**
2384
- * Find an available port starting from the preferred port
2385
- * @internal Exported for testing
2386
- */
2387
- async function findAvailablePort(preferredPort, maxAttempts = 10) {
2388
- for (let i = 0; i < maxAttempts; i++) {
2389
- const port = preferredPort + i;
2390
- if (await isPortAvailable(port)) return port;
2391
- logger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
2392
- }
2393
- throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
2394
- }
2395
- const PORT_STATE_PATH = ".gkm/ports.json";
2396
- /**
2397
- * Parse docker-compose.yml and extract all port mappings that use env var interpolation.
2398
- * Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
2399
- * Fixed port mappings like `'5050:80'` are skipped.
2400
- * @internal Exported for testing
2401
- */
2402
- function parseComposePortMappings(composePath) {
2403
- if (!existsSync(composePath)) return [];
2404
- const content = readFileSync(composePath, "utf-8");
2405
- const compose = parse$1(content);
2406
- if (!compose?.services) return [];
2407
- const results = [];
2408
- for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
2409
- const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
2410
- if (match?.[1] && match[2] && match[3]) results.push({
2411
- service: serviceName,
2412
- envVar: match[1],
2413
- defaultPort: Number(match[2]),
2414
- containerPort: Number(match[3])
2415
- });
2416
- }
2417
- return results;
2418
- }
2419
- /**
2420
- * Load saved port state from .gkm/ports.json.
2421
- * @internal Exported for testing
2422
- */
2423
- async function loadPortState(workspaceRoot) {
2424
- try {
2425
- const raw = await readFile(join(workspaceRoot, PORT_STATE_PATH), "utf-8");
2426
- return JSON.parse(raw);
2427
- } catch {
2428
- return {};
2429
- }
2430
- }
2431
- /**
2432
- * Save port state to .gkm/ports.json.
2433
- * @internal Exported for testing
2434
- */
2435
- async function savePortState(workspaceRoot, ports) {
2436
- const dir = join(workspaceRoot, ".gkm");
2437
- await mkdir(dir, { recursive: true });
2438
- await writeFile(join(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
2439
- }
2440
- /**
2441
- * Check if a project's own Docker container is running and return its host port.
2442
- * Uses `docker compose port` scoped to the project's compose file.
2443
- * @internal Exported for testing
2444
- */
2445
- function getContainerHostPort(workspaceRoot, service, containerPort) {
2446
- try {
2447
- const match = execSync(`docker compose port ${service} ${containerPort}`, {
2448
- cwd: workspaceRoot,
2449
- stdio: "pipe"
2450
- }).toString().trim().match(/:(\d+)$/);
2451
- return match ? Number(match[1]) : null;
2452
- } catch {
2453
- return null;
2454
- }
2455
- }
2456
- /**
2457
- * Resolve host ports for Docker services by parsing docker-compose.yml.
2458
- * Priority: running container → saved state → find available port.
2459
- * Persists resolved ports to .gkm/ports.json.
2460
- * @internal Exported for testing
2461
- */
2462
- async function resolveServicePorts(workspaceRoot) {
2463
- const mappings = parseComposePortMappings(join(workspaceRoot, "docker-compose.yml"));
2464
- if (mappings.length === 0) return {
2465
- dockerEnv: {},
2466
- ports: {},
2467
- mappings: []
2468
- };
2469
- const savedState = await loadPortState(workspaceRoot);
2470
- const dockerEnv = {};
2471
- const ports = {};
2472
- const assignedPorts = /* @__PURE__ */ new Set();
2473
- logger.log("\n🔌 Resolving service ports...");
2474
- for (const mapping of mappings) {
2475
- const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
2476
- if (containerPort !== null) {
2477
- ports[mapping.envVar] = containerPort;
2478
- dockerEnv[mapping.envVar] = String(containerPort);
2479
- assignedPorts.add(containerPort);
2480
- logger.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
2481
- continue;
2482
- }
2483
- const savedPort = savedState[mapping.envVar];
2484
- if (savedPort && !assignedPorts.has(savedPort) && await isPortAvailable(savedPort)) {
2485
- ports[mapping.envVar] = savedPort;
2486
- dockerEnv[mapping.envVar] = String(savedPort);
2487
- assignedPorts.add(savedPort);
2488
- logger.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
2489
- continue;
2490
- }
2491
- let resolvedPort = await findAvailablePort(mapping.defaultPort);
2492
- while (assignedPorts.has(resolvedPort)) resolvedPort = await findAvailablePort(resolvedPort + 1);
2493
- ports[mapping.envVar] = resolvedPort;
2494
- dockerEnv[mapping.envVar] = String(resolvedPort);
2495
- assignedPorts.add(resolvedPort);
2496
- if (resolvedPort !== mapping.defaultPort) logger.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
2497
- else logger.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
2498
- }
2499
- await savePortState(workspaceRoot, ports);
2500
- return {
2501
- dockerEnv,
2502
- ports,
2503
- mappings
2504
- };
2505
- }
2506
- /**
2507
- * Replace a port in a URL string.
2508
- * Handles both `hostname:port` and `localhost:port` patterns.
2509
- * @internal Exported for testing
2510
- */
2511
- function replacePortInUrl(url, oldPort, newPort) {
2512
- if (oldPort === newPort) return url;
2513
- let result = url.replace(new RegExp(`:${oldPort}(?=[/?#]|$)`, "g"), `:${newPort}`);
2514
- result = result.replace(new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, "gi"), `%3A${newPort}`);
2515
- return result;
2516
- }
2517
- /**
2518
- * Rewrite connection URLs and port vars in secrets with resolved ports.
2519
- * Uses the parsed compose mappings to determine which default ports to replace.
2520
- * Pure transform — does not modify secrets on disk.
2521
- * @internal Exported for testing
2522
- */
2523
- function rewriteUrlsWithPorts(secrets, resolvedPorts) {
2524
- const { ports, mappings } = resolvedPorts;
2525
- const result = { ...secrets };
2526
- const portReplacements = [];
2527
- const serviceNames = /* @__PURE__ */ new Set();
2528
- for (const mapping of mappings) {
2529
- serviceNames.add(mapping.service);
2530
- const resolved = ports[mapping.envVar];
2531
- if (resolved !== void 0) portReplacements.push({
2532
- defaultPort: mapping.defaultPort,
2533
- resolvedPort: resolved
2534
- });
2535
- }
2536
- for (const [key, value] of Object.entries(result)) {
2537
- if (!key.endsWith("_HOST")) continue;
2538
- if (serviceNames.has(value)) result[key] = "localhost";
2539
- }
2540
- for (const [key, value] of Object.entries(result)) {
2541
- if (!key.endsWith("_PORT")) continue;
2542
- for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
2543
- }
2544
- for (const [key, value] of Object.entries(result)) {
2545
- if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && !key.endsWith("_CONNECTION_STRING") && key !== "DATABASE_URL") continue;
2546
- let rewritten = value;
2547
- for (const name of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name}:`, "g"), "@localhost:");
2548
- for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
2549
- result[key] = rewritten;
2550
- }
2551
- return result;
2552
- }
2553
- /**
2554
- * Build the environment variables to pass to `docker compose up`.
2555
- * Merges process.env, secrets, and port mappings so that Docker Compose
2556
- * can interpolate variables like ${POSTGRES_USER} correctly.
2557
- * @internal Exported for testing
2558
- */
2559
- function buildDockerComposeEnv(secretsEnv, portEnv) {
2560
- return {
2561
- ...process.env,
2562
- ...secretsEnv,
2563
- ...portEnv
2564
- };
2565
- }
2566
- /**
2567
- * Parse all service names from a docker-compose.yml file.
2568
- * @internal Exported for testing
2569
- */
2570
- function parseComposeServiceNames(composePath) {
2571
- if (!existsSync(composePath)) return [];
2572
- const content = readFileSync(composePath, "utf-8");
2573
- const compose = parse$1(content);
2574
- return Object.keys(compose?.services ?? {});
2575
- }
2576
- /**
2577
- * Start docker-compose services for a single-app project (no workspace config).
2578
- * Starts all services defined in docker-compose.yml.
2579
- */
2580
- async function startComposeServices(cwd, portEnv, secretsEnv) {
2581
- const composeFile = join(cwd, "docker-compose.yml");
2582
- if (!existsSync(composeFile)) return;
2583
- const servicesToStart = parseComposeServiceNames(composeFile);
2584
- if (servicesToStart.length === 0) return;
2585
- logger.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
2586
- try {
2587
- execSync(`docker compose up -d ${servicesToStart.join(" ")}`, {
2588
- cwd,
2589
- stdio: "inherit",
2590
- env: buildDockerComposeEnv(secretsEnv, portEnv)
2591
- });
2592
- logger.log("✅ Services started");
2593
- } catch (error) {
2594
- logger.error("❌ Failed to start services:", error.message);
2595
- throw error;
2596
- }
2597
- }
2598
- /**
2599
- * Start docker-compose services for a workspace.
2600
- * Discovers all services from docker-compose.yml and starts everything
2601
- * except app services (which are managed by turbo).
2602
- * @internal Exported for testing
2603
- */
2604
- async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
2605
- const composeFile = join(workspace.root, "docker-compose.yml");
2606
- if (!existsSync(composeFile)) return;
2607
- const allServices = parseComposeServiceNames(composeFile);
2608
- const appNames = new Set(Object.keys(workspace.apps));
2609
- const servicesToStart = allServices.filter((name) => !appNames.has(name));
2610
- if (servicesToStart.length === 0) return;
2611
- logger.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
2612
- try {
2613
- execSync(`docker compose up -d ${servicesToStart.join(" ")}`, {
2614
- cwd: workspace.root,
2615
- stdio: "inherit",
2616
- env: buildDockerComposeEnv(secretsEnv, portEnv)
2617
- });
2618
- logger.log("✅ Services started");
2619
- } catch (error) {
2620
- logger.error("❌ Failed to start services:", error.message);
2621
- throw error;
2622
- }
2623
- }
2624
- /**
2625
- * Load and flatten secrets for an app from encrypted storage.
2626
- * For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
2627
- * @internal Exported for testing
2628
- */
2629
- async function loadSecretsForApp(secretsRoot, stage, appName) {
2630
- let secrets = {};
2631
- if (secretsExist(stage, secretsRoot)) {
2632
- const stageSecrets = await readStageSecrets(stage, secretsRoot);
2633
- if (stageSecrets) {
2634
- logger.log(`🔐 Loading secrets from stage: ${stage}`);
2635
- secrets = toEmbeddableSecrets(stageSecrets);
2636
- }
2637
- }
2638
- if (Object.keys(secrets).length === 0) return {};
2639
- if (!appName) return secrets;
2640
- const prefix = appName.toUpperCase();
2641
- const mapped = { ...secrets };
2642
- const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
2643
- if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
2644
- return mapped;
2645
- }
2646
- /**
2647
- * Walk up the directory tree to find the root containing .gkm/secrets/.
2648
- * @internal Exported for testing
2649
- */
2650
- function findSecretsRoot(startDir) {
2651
- let dir = startDir;
2652
- while (dir !== "/") {
2653
- if (existsSync(join(dir, ".gkm", "secrets"))) return dir;
2654
- const parent = dirname(dir);
2655
- if (parent === dir) break;
2656
- dir = parent;
2657
- }
2658
- return startDir;
2659
- }
2660
- /**
2661
- * Generate the credentials injection code snippet.
2662
- * This is the common logic used by both entry wrapper and exec preload.
2663
- * @internal
2664
- */
2665
- function generateCredentialsInjection(secretsJsonPath) {
2666
- return `import { existsSync, readFileSync } from 'node:fs';
2667
-
2668
- // Inject dev secrets via globalThis and process.env
2669
- // Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
2670
- // Object.assign on the Credentials export only mutates one module copy.
2671
- const secretsPath = '${secretsJsonPath}';
2672
- if (existsSync(secretsPath)) {
2673
- const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
2674
- globalThis.__gkm_credentials__ = secrets;
2675
- Object.assign(process.env, secrets);
2676
- }
2677
- `;
2678
- }
2679
- /**
2680
- * Create a preload script that injects secrets into Credentials.
2681
- * Used by `gkm exec` to inject secrets before running any command.
2682
- * @internal Exported for testing
2683
- */
2684
- async function createCredentialsPreload(preloadPath, secretsJsonPath) {
2685
- const content = `/**
2686
- * Credentials preload generated by 'gkm exec'
2687
- * This file is loaded via NODE_OPTIONS="--import <path>"
2688
- */
2689
- ${generateCredentialsInjection(secretsJsonPath)}`;
2690
- await writeFile(preloadPath, content);
2691
- }
2692
- /**
2693
- * Create a wrapper script that injects secrets before importing the entry file.
2694
- * @internal Exported for testing
2695
- */
2696
- async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
2697
- const content = `#!/usr/bin/env node
2698
- /**
2699
- * Entry wrapper generated by 'gkm dev --entry'
2700
- */
2701
- ${secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
2702
- ` : ""}// Import and run the user's entry file (dynamic import ensures secrets load first)
2703
- await import('${entryPath}');
2704
- `;
2705
- await writeFile(wrapperPath, content);
2706
- }
2707
- /**
2708
- * Prepare credentials for dev/exec/test modes.
2709
- * Loads workspace config, secrets, resolves Docker ports, rewrites URLs,
2710
- * injects PORT, dependency URLs, and writes credentials JSON.
2711
- *
2712
- * @param options.resolveDockerPorts - How to resolve Docker ports:
2713
- * - `'full'` (default): probe running containers, saved state, then find available ports. Used by dev/test.
2714
- * - `'readonly'`: check running containers and saved state only, never probe for new ports. Used by exec.
2715
- * @param options.stage - The stage whose secrets to load. Default: the project's `stages.local`.
2716
- * @param options.startDocker - Start Docker Compose services after port resolution. Default: false.
2717
- * @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.
2718
- * @internal Exported for testing
2719
- */
2720
- async function prepareEntryCredentials(options) {
2721
- const cwd = options.cwd ?? process.cwd();
2722
- const portMode = options.resolveDockerPorts ?? "full";
2723
- let workspaceAppPort;
2724
- let secretsRoot = cwd;
2725
- let appName;
2726
- let appInfo;
2727
- let workspace;
2728
- try {
2729
- appInfo = await loadWorkspaceAppInfo(cwd);
2730
- workspace = appInfo.workspace;
2731
- workspaceAppPort = appInfo.app.port;
2732
- secretsRoot = appInfo.workspaceRoot;
2733
- appName = appInfo.appName;
2734
- } catch {
2735
- workspace = await loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace).catch(() => void 0);
2736
- secretsRoot = workspace?.root ?? findSecretsRoot(cwd);
2737
- appName = getAppNameFromCwd(cwd) ?? void 0;
2738
- }
2739
- const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
2740
- const stage = options.stage ?? workspace?.stages.local;
2741
- const credentials = stage ? await loadSecretsForApp(secretsRoot, stage, appName) : {};
2742
- credentials.PORT = String(resolvedPort);
2743
- const declaredKeys = [];
2744
- if (workspace && usesConstructs(workspace)) {
2745
- const { reconcileWorkspace } = await Promise.resolve().then(() => workspace_exports);
2746
- const reconciled = await reconcileWorkspace(workspace, {
2747
- stage: options.reconcileStage ?? workspace.stages.local,
2748
- start: options.startDocker ?? false
2749
- });
2750
- Object.assign(credentials, reconciled.env);
2751
- declaredKeys.push(...Object.keys(reconciled.env));
2752
- if (Object.keys(reconciled.env).length > 0) logger.log(`🔌 Resolved ${Object.keys(reconciled.env).length} declared URL(s)`);
2753
- } else {
2754
- const mappings = parseComposePortMappings(join(secretsRoot, "docker-compose.yml"));
2755
- if (mappings.length > 0) {
2756
- let resolvedPorts;
2757
- if (portMode === "full") resolvedPorts = await resolveServicePorts(secretsRoot);
2758
- else {
2759
- const savedPorts = await loadPortState(secretsRoot);
2760
- const ports = {};
2761
- for (const mapping of mappings) {
2762
- const containerPort = getContainerHostPort(secretsRoot, mapping.service, mapping.containerPort);
2763
- if (containerPort !== null) ports[mapping.envVar] = containerPort;
2764
- else {
2765
- const saved = savedPorts[mapping.envVar];
2766
- if (saved !== void 0) ports[mapping.envVar] = saved;
2767
- }
2768
- }
2769
- resolvedPorts = {
2770
- dockerEnv: {},
2771
- ports,
2772
- mappings
2773
- };
2774
- }
2775
- if (options.startDocker) {
2776
- if (appInfo) await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, credentials);
2777
- else await startComposeServices(secretsRoot, resolvedPorts.dockerEnv, credentials);
2778
- }
2779
- if (Object.keys(resolvedPorts.ports).length > 0) {
2780
- const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
2781
- Object.assign(credentials, rewritten);
2782
- logger.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
2783
- }
2784
- }
2785
- }
2786
- if (appInfo?.appName) {
2787
- const depEnv = getDependencyEnvVars(appInfo.workspace, appInfo.appName);
2788
- Object.assign(credentials, depEnv);
2789
- }
2790
- if (!credentials.EVENT_PUBLISHER_CONNECTION_STRING && !credentials.EVENT_SUBSCRIBER_CONNECTION_STRING && credentials.PGBOSS_DB_USER) {
2791
- const pgbossUrl = generatePgBossUrl({
2792
- username: credentials.PGBOSS_DB_USER,
2793
- password: credentials.PGBOSS_DB_PASSWORD ?? "",
2794
- host: credentials.POSTGRES_HOST ?? "localhost",
2795
- port: Number(credentials.POSTGRES_PORT ?? "5432"),
2796
- database: credentials.POSTGRES_DB ?? "app"
2797
- });
2798
- credentials.EVENT_PUBLISHER_CONNECTION_STRING = pgbossUrl;
2799
- credentials.EVENT_SUBSCRIBER_CONNECTION_STRING = pgbossUrl;
2800
- }
2801
- const secretsDir = join(secretsRoot, ".gkm");
2802
- await mkdir(secretsDir, { recursive: true });
2803
- const secretsFileName = options.secretsFileName ?? (appName ? `dev-secrets-${appName}.json` : "dev-secrets.json");
2804
- const secretsJsonPath = join(secretsDir, secretsFileName);
2805
- await writeFile(secretsJsonPath, JSON.stringify(credentials, null, 2));
2806
- return {
2807
- credentials,
2808
- resolvedPort,
2809
- secretsJsonPath,
2810
- appName,
2811
- secretsRoot,
2812
- appInfo,
2813
- declaredKeys
2814
- };
2815
- }
2816
- //#endregion
2817
- export { providerOf as C, eventsBackendFor as S, COMPOSE_PATH as _, loadEnvFiles as a, resourceName as b, resolveServicePorts as c, backendsOf as d, constructGlobs as f, workspace_exports as g, usesConstructs as h, isPortAvailable as i, rewriteUrlsWithPorts as l, reconcileWorkspace as m, createEntryWrapper as n, loadSecretsForApp as o, derivedContainers as p, findAvailablePort as r, prepareEntryCredentials as s, createCredentialsPreload as t, startWorkspaceServices as u, LOCAL_CA_PATH as v, storageBackendFor as w, cacheBackendFor as x, applyPostgres as y };
2553
+ export { isPortAvailable as _, workspace_exports as a, prepareEntryCredentials as b, applyPostgres as c, eventsBackendFor as d, providerOf as f, findAvailablePort as g, createEntryWrapper as h, reconcileWorkspace as i, resourceName as l, createCredentialsPreload as m, constructGlobs as n, COMPOSE_PATH as o, storageBackendFor as p, derivedContainers as r, LOCAL_CA_PATH as s, backendsOf as t, cacheBackendFor as u, loadEnvFiles as v, loadSecretsForApp as y };
2818
2554
 
2819
- //# sourceMappingURL=credentials-BpoOUa74.mjs.map
2555
+ //# sourceMappingURL=workspace-CUmR_k4J.mjs.map