@geekmidas/cli 1.10.17 → 1.10.18

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 (39) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/{bundler-B4AackW5.mjs → bundler-C5xkxnyr.mjs} +2 -2
  3. package/dist/{bundler-B4AackW5.mjs.map → bundler-C5xkxnyr.mjs.map} +1 -1
  4. package/dist/{bundler-BhhfkI9T.cjs → bundler-i-az1DZ2.cjs} +2 -2
  5. package/dist/{bundler-BhhfkI9T.cjs.map → bundler-i-az1DZ2.cjs.map} +1 -1
  6. package/dist/index.cjs +699 -706
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +687 -694
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/{openapi-BYxAWwok.cjs → openapi-CsCNpSf8.cjs} +1 -1
  11. package/dist/{openapi-BYxAWwok.cjs.map → openapi-CsCNpSf8.cjs.map} +1 -1
  12. package/dist/{openapi-DenF-okj.mjs → openapi-kvwpKbNe.mjs} +1 -1
  13. package/dist/{openapi-DenF-okj.mjs.map → openapi-kvwpKbNe.mjs.map} +1 -1
  14. package/dist/openapi.cjs +1 -1
  15. package/dist/openapi.mjs +1 -1
  16. package/dist/{storage-DOEtT2Hr.cjs → storage-ChVQI_G7.cjs} +1 -1
  17. package/dist/{storage-dbb9RyBl.mjs → storage-CpMNB77O.mjs} +1 -1
  18. package/dist/{storage-dbb9RyBl.mjs.map → storage-CpMNB77O.mjs.map} +1 -1
  19. package/dist/{storage-B1wvztiJ.cjs → storage-DLEb8Dkd.cjs} +1 -1
  20. package/dist/{storage-B1wvztiJ.cjs.map → storage-DLEb8Dkd.cjs.map} +1 -1
  21. package/dist/{storage-Cs4WBsc4.mjs → storage-mwbL7PhP.mjs} +1 -1
  22. package/dist/{sync-DGXXSk2v.cjs → sync-BWD_I5Ai.cjs} +2 -2
  23. package/dist/{sync-DGXXSk2v.cjs.map → sync-BWD_I5Ai.cjs.map} +1 -1
  24. package/dist/sync-ByaRPBxh.cjs +4 -0
  25. package/dist/{sync-COnAugP-.mjs → sync-CYBVB64f.mjs} +1 -1
  26. package/dist/{sync-D_NowTkZ.mjs → sync-lExOTa9t.mjs} +2 -2
  27. package/dist/{sync-D_NowTkZ.mjs.map → sync-lExOTa9t.mjs.map} +1 -1
  28. package/package.json +4 -4
  29. package/src/credentials/__tests__/fullDockerPorts.spec.ts +144 -0
  30. package/src/credentials/__tests__/helpers.ts +112 -0
  31. package/src/credentials/__tests__/prepareEntryCredentials.spec.ts +125 -0
  32. package/src/credentials/__tests__/readonlyDockerPorts.spec.ts +190 -0
  33. package/src/credentials/__tests__/workspaceCredentials.spec.ts +209 -0
  34. package/src/credentials/index.ts +826 -0
  35. package/src/dev/index.ts +44 -829
  36. package/src/exec/index.ts +120 -0
  37. package/src/setup/index.ts +4 -1
  38. package/src/test/index.ts +32 -109
  39. package/dist/sync-D1Pa30oV.cjs +0 -4
package/dist/index.cjs CHANGED
@@ -3,14 +3,14 @@ const require_chunk = require('./chunk-CUT6urMc.cjs');
3
3
  const require_workspace = require('./workspace-4SP3Gx4Y.cjs');
4
4
  const require_config = require('./config-D3ORuiUs.cjs');
5
5
  const require_credentials = require('./credentials-C8DWtnMY.cjs');
6
- const require_openapi = require('./openapi-BYxAWwok.cjs');
7
- const require_storage = require('./storage-B1wvztiJ.cjs');
6
+ const require_storage = require('./storage-DLEb8Dkd.cjs');
7
+ const require_openapi = require('./openapi-CsCNpSf8.cjs');
8
8
  const require_dokploy_api = require('./dokploy-api-DLgvEQlr.cjs');
9
9
  const require_encryption = require('./encryption-BE0UOb8j.cjs');
10
10
  const require_CachedStateProvider = require('./CachedStateProvider-D73dCqfH.cjs');
11
11
  const require_fullstack_secrets = require('./fullstack-secrets-DOHBU4Rp.cjs');
12
12
  const require_openapi_react_query = require('./openapi-react-query-DYbBq-WJ.cjs');
13
- const require_sync = require('./sync-DGXXSk2v.cjs');
13
+ const require_sync = require('./sync-BWD_I5Ai.cjs');
14
14
  const node_fs = require_chunk.__toESM(require("node:fs"));
15
15
  const node_path = require_chunk.__toESM(require("node:path"));
16
16
  const commander = require_chunk.__toESM(require("commander"));
@@ -18,15 +18,15 @@ const node_process = require_chunk.__toESM(require("node:process"));
18
18
  const node_readline_promises = require_chunk.__toESM(require("node:readline/promises"));
19
19
  const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
20
20
  const node_child_process = require_chunk.__toESM(require("node:child_process"));
21
- const node_net = require_chunk.__toESM(require("node:net"));
22
21
  const chokidar = require_chunk.__toESM(require("chokidar"));
23
- const dotenv = require_chunk.__toESM(require("dotenv"));
24
22
  const fast_glob = require_chunk.__toESM(require("fast-glob"));
23
+ const node_net = require_chunk.__toESM(require("node:net"));
24
+ const dotenv = require_chunk.__toESM(require("dotenv"));
25
25
  const yaml = require_chunk.__toESM(require("yaml"));
26
+ const node_crypto = require_chunk.__toESM(require("node:crypto"));
26
27
  const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
27
28
  const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
28
29
  const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
29
- const node_crypto = require_chunk.__toESM(require("node:crypto"));
30
30
  const pg = require_chunk.__toESM(require("pg"));
31
31
  const node_dns_promises = require_chunk.__toESM(require("node:dns/promises"));
32
32
  const node_module = require_chunk.__toESM(require("node:module"));
@@ -35,7 +35,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
35
35
 
36
36
  //#region package.json
37
37
  var name = "@geekmidas/cli";
38
- var version = "1.10.16";
38
+ var version = "1.10.17";
39
39
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
40
40
  var private$1 = false;
41
41
  var type = "module";
@@ -133,7 +133,7 @@ var package_default = {
133
133
 
134
134
  //#endregion
135
135
  //#region src/auth/index.ts
136
- const logger$12 = console;
136
+ const logger$14 = console;
137
137
  /**
138
138
  * Validate Dokploy token by making a test API call
139
139
  */
@@ -152,7 +152,7 @@ async function prompt$1(message, hidden = false) {
152
152
  if (!process.stdin.isTTY) throw new Error("Interactive input required. Please provide --token option.");
153
153
  if (hidden) {
154
154
  process.stdout.write(message);
155
- return new Promise((resolve$3, reject) => {
155
+ return new Promise((resolve$4, reject) => {
156
156
  let value = "";
157
157
  const cleanup = () => {
158
158
  process.stdin.setRawMode(false);
@@ -169,7 +169,7 @@ async function prompt$1(message, hidden = false) {
169
169
  if (c === "\n" || c === "\r") {
170
170
  cleanup();
171
171
  process.stdout.write("\n");
172
- resolve$3(value);
172
+ resolve$4(value);
173
173
  } else if (c === "") {
174
174
  cleanup();
175
175
  process.stdout.write("\n");
@@ -201,36 +201,36 @@ async function prompt$1(message, hidden = false) {
201
201
  async function loginCommand(options) {
202
202
  const { service, token: providedToken, endpoint: providedEndpoint } = options;
203
203
  if (service === "dokploy") {
204
- logger$12.log("\n🔐 Logging in to Dokploy...\n");
204
+ logger$14.log("\n🔐 Logging in to Dokploy...\n");
205
205
  let endpoint = providedEndpoint;
206
206
  if (!endpoint) endpoint = await prompt$1("Dokploy URL (e.g., https://dokploy.example.com): ");
207
207
  endpoint = endpoint.replace(/\/$/, "");
208
208
  try {
209
209
  new URL(endpoint);
210
210
  } catch {
211
- logger$12.error("Invalid URL format");
211
+ logger$14.error("Invalid URL format");
212
212
  process.exit(1);
213
213
  }
214
214
  let token = providedToken;
215
215
  if (!token) {
216
- logger$12.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
216
+ logger$14.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
217
217
  token = await prompt$1("API Token: ", true);
218
218
  }
219
219
  if (!token) {
220
- logger$12.error("Token is required");
220
+ logger$14.error("Token is required");
221
221
  process.exit(1);
222
222
  }
223
- logger$12.log("\nValidating credentials...");
223
+ logger$14.log("\nValidating credentials...");
224
224
  const isValid = await validateDokployToken(endpoint, token);
225
225
  if (!isValid) {
226
- logger$12.error("\n✗ Invalid credentials. Please check your token and try again.");
226
+ logger$14.error("\n✗ Invalid credentials. Please check your token and try again.");
227
227
  process.exit(1);
228
228
  }
229
229
  await require_credentials.storeDokployCredentials(token, endpoint);
230
- logger$12.log("\n✓ Successfully logged in to Dokploy!");
231
- logger$12.log(` Endpoint: ${endpoint}`);
232
- logger$12.log(` Credentials stored in: ${require_credentials.getCredentialsPath()}`);
233
- logger$12.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
230
+ logger$14.log("\n✓ Successfully logged in to Dokploy!");
231
+ logger$14.log(` Endpoint: ${endpoint}`);
232
+ logger$14.log(` Credentials stored in: ${require_credentials.getCredentialsPath()}`);
233
+ logger$14.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
234
234
  }
235
235
  }
236
236
  /**
@@ -240,28 +240,28 @@ async function logoutCommand(options) {
240
240
  const { service = "dokploy" } = options;
241
241
  if (service === "all") {
242
242
  const dokployRemoved = await require_credentials.removeDokployCredentials();
243
- if (dokployRemoved) logger$12.log("\n✓ Logged out from all services");
244
- else logger$12.log("\nNo stored credentials found");
243
+ if (dokployRemoved) logger$14.log("\n✓ Logged out from all services");
244
+ else logger$14.log("\nNo stored credentials found");
245
245
  return;
246
246
  }
247
247
  if (service === "dokploy") {
248
248
  const removed = await require_credentials.removeDokployCredentials();
249
- if (removed) logger$12.log("\n✓ Logged out from Dokploy");
250
- else logger$12.log("\nNo Dokploy credentials found");
249
+ if (removed) logger$14.log("\n✓ Logged out from Dokploy");
250
+ else logger$14.log("\nNo Dokploy credentials found");
251
251
  }
252
252
  }
253
253
  /**
254
254
  * Show current login status
255
255
  */
256
256
  async function whoamiCommand() {
257
- logger$12.log("\n📋 Current credentials:\n");
257
+ logger$14.log("\n📋 Current credentials:\n");
258
258
  const dokploy = await require_credentials.getDokployCredentials();
259
259
  if (dokploy) {
260
- logger$12.log(" Dokploy:");
261
- logger$12.log(` Endpoint: ${dokploy.endpoint}`);
262
- logger$12.log(` Token: ${maskToken(dokploy.token)}`);
263
- } else logger$12.log(" Dokploy: Not logged in");
264
- logger$12.log(`\n Credentials file: ${require_credentials.getCredentialsPath()}`);
260
+ logger$14.log(" Dokploy:");
261
+ logger$14.log(` Endpoint: ${dokploy.endpoint}`);
262
+ logger$14.log(` Token: ${maskToken(dokploy.token)}`);
263
+ } else logger$14.log(" Dokploy: Not logged in");
264
+ logger$14.log(`\n Credentials file: ${require_credentials.getCredentialsPath()}`);
265
265
  }
266
266
  /**
267
267
  * Mask a token for display
@@ -343,134 +343,589 @@ function isEnabled(config) {
343
343
  }
344
344
 
345
345
  //#endregion
346
- //#region src/generators/CronGenerator.ts
347
- var CronGenerator = class extends require_openapi.ConstructGenerator {
348
- async build(context, constructs, outputDir, options) {
349
- const provider = options?.provider || "aws-lambda";
350
- const logger$13 = console;
351
- const cronInfos = [];
352
- if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
353
- const cronsDir = (0, node_path.join)(outputDir, "crons");
354
- await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
355
- for (const { key, construct, path } of constructs) {
356
- const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
357
- cronInfos.push({
358
- name: key,
359
- handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
360
- schedule: construct.schedule || "rate(1 hour)",
361
- timeout: construct.timeout,
362
- memorySize: construct.memorySize,
363
- environment: await construct.getEnvironment()
346
+ //#region src/credentials/index.ts
347
+ const logger$13 = console;
348
+ /**
349
+ * Load environment files
350
+ * @internal Exported for testing
351
+ */
352
+ function loadEnvFiles(envConfig, cwd = process.cwd()) {
353
+ const loaded = [];
354
+ const missing = [];
355
+ const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
356
+ for (const envFile of envFiles) {
357
+ const envPath = (0, node_path.resolve)(cwd, envFile);
358
+ if ((0, node_fs.existsSync)(envPath)) {
359
+ (0, dotenv.config)({
360
+ path: envPath,
361
+ override: true,
362
+ quiet: true
364
363
  });
365
- logger$13.log(`Generated cron handler: ${key}`);
366
- }
367
- return cronInfos;
368
- }
369
- isConstruct(value) {
370
- return __geekmidas_constructs_crons.Cron.isCron(value);
371
- }
372
- async generateCronHandler(outputDir, sourceFile, exportName, context) {
373
- const handlerFileName = `${exportName}.ts`;
374
- const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
375
- const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
376
- const importPath = relativePath.replace(/\.ts$/, ".js");
377
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
378
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
379
- const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
380
- import { ${exportName} } from '${importPath}';
381
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
382
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
383
-
384
- const adapter = new AWSScheduledFunction(envParser, ${exportName});
385
-
386
- export const handler = adapter.handler;
387
- `;
388
- await (0, node_fs_promises.writeFile)(handlerPath, content);
389
- return handlerPath;
364
+ loaded.push(envFile);
365
+ } else if (envConfig) missing.push(envFile);
390
366
  }
391
- };
392
-
393
- //#endregion
394
- //#region src/generators/FunctionGenerator.ts
395
- var FunctionGenerator = class extends require_openapi.ConstructGenerator {
396
- isConstruct(value) {
397
- return __geekmidas_constructs_functions.Function.isFunction(value);
367
+ return {
368
+ loaded,
369
+ missing
370
+ };
371
+ }
372
+ /**
373
+ * Check if a port is available
374
+ * @internal Exported for testing
375
+ */
376
+ async function isPortAvailable(port) {
377
+ return new Promise((resolve$4) => {
378
+ const server = (0, node_net.createServer)();
379
+ server.once("error", (err) => {
380
+ if (err.code === "EADDRINUSE") resolve$4(false);
381
+ else resolve$4(false);
382
+ });
383
+ server.once("listening", () => {
384
+ server.close();
385
+ resolve$4(true);
386
+ });
387
+ server.listen(port);
388
+ });
389
+ }
390
+ /**
391
+ * Find an available port starting from the preferred port
392
+ * @internal Exported for testing
393
+ */
394
+ async function findAvailablePort(preferredPort, maxAttempts = 10) {
395
+ for (let i = 0; i < maxAttempts; i++) {
396
+ const port = preferredPort + i;
397
+ if (await isPortAvailable(port)) return port;
398
+ logger$13.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
398
399
  }
399
- async build(context, constructs, outputDir, options) {
400
- const provider = options?.provider || "aws-lambda";
401
- const logger$13 = console;
402
- const functionInfos = [];
403
- if (constructs.length === 0 || provider !== "aws-lambda") return functionInfos;
404
- const functionsDir = (0, node_path.join)(outputDir, "functions");
405
- await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
406
- for (const { key, construct, path } of constructs) {
407
- const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
408
- functionInfos.push({
409
- name: key,
410
- handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
411
- timeout: construct.timeout,
412
- memorySize: construct.memorySize,
413
- environment: await construct.getEnvironment()
414
- });
415
- logger$13.log(`Generated function handler: ${key}`);
416
- }
417
- return functionInfos;
400
+ throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
401
+ }
402
+ const PORT_STATE_PATH = ".gkm/ports.json";
403
+ /**
404
+ * Parse docker-compose.yml and extract all port mappings that use env var interpolation.
405
+ * Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
406
+ * Fixed port mappings like `'5050:80'` are skipped.
407
+ * @internal Exported for testing
408
+ */
409
+ function parseComposePortMappings(composePath) {
410
+ if (!(0, node_fs.existsSync)(composePath)) return [];
411
+ const content = (0, node_fs.readFileSync)(composePath, "utf-8");
412
+ const compose = (0, yaml.parse)(content);
413
+ if (!compose?.services) return [];
414
+ const results = [];
415
+ for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
416
+ const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
417
+ if (match?.[1] && match[2] && match[3]) results.push({
418
+ service: serviceName,
419
+ envVar: match[1],
420
+ defaultPort: Number(match[2]),
421
+ containerPort: Number(match[3])
422
+ });
418
423
  }
419
- async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
420
- const handlerFileName = `${exportName}.ts`;
421
- const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
422
- const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
423
- const importPath = relativePath.replace(/\.ts$/, ".js");
424
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
425
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
426
- const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
427
- import { ${exportName} } from '${importPath}';
428
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
429
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
430
-
431
- const adapter = new AWSLambdaFunction(envParser, ${exportName});
432
-
433
- export const handler = adapter.handler;
434
- `;
435
- await (0, node_fs_promises.writeFile)(handlerPath, content);
436
- return handlerPath;
424
+ return results;
425
+ }
426
+ /**
427
+ * Load saved port state from .gkm/ports.json.
428
+ * @internal Exported for testing
429
+ */
430
+ async function loadPortState(workspaceRoot) {
431
+ try {
432
+ const raw = await (0, node_fs_promises.readFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), "utf-8");
433
+ return JSON.parse(raw);
434
+ } catch {
435
+ return {};
437
436
  }
438
- };
439
-
440
- //#endregion
441
- //#region src/generators/SubscriberGenerator.ts
442
- var SubscriberGenerator = class extends require_openapi.ConstructGenerator {
443
- isConstruct(value) {
444
- return __geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
437
+ }
438
+ /**
439
+ * Save port state to .gkm/ports.json.
440
+ * @internal Exported for testing
441
+ */
442
+ async function savePortState(workspaceRoot, ports) {
443
+ const dir = (0, node_path.join)(workspaceRoot, ".gkm");
444
+ await (0, node_fs_promises.mkdir)(dir, { recursive: true });
445
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
446
+ }
447
+ /**
448
+ * Check if a project's own Docker container is running and return its host port.
449
+ * Uses `docker compose port` scoped to the project's compose file.
450
+ * @internal Exported for testing
451
+ */
452
+ function getContainerHostPort(workspaceRoot, service, containerPort) {
453
+ try {
454
+ const result = (0, node_child_process.execSync)(`docker compose port ${service} ${containerPort}`, {
455
+ cwd: workspaceRoot,
456
+ stdio: "pipe"
457
+ }).toString().trim();
458
+ const match = result.match(/:(\d+)$/);
459
+ return match ? Number(match[1]) : null;
460
+ } catch {
461
+ return null;
445
462
  }
446
- async build(context, constructs, outputDir, options) {
447
- const provider = options?.provider || "aws-lambda";
448
- const logger$13 = console;
449
- const subscriberInfos = [];
450
- if (provider === "server") {
451
- await this.generateServerSubscribersFile(outputDir, constructs);
452
- logger$13.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
453
- return subscriberInfos;
463
+ }
464
+ /**
465
+ * Resolve host ports for Docker services by parsing docker-compose.yml.
466
+ * Priority: running container → saved state → find available port.
467
+ * Persists resolved ports to .gkm/ports.json.
468
+ * @internal Exported for testing
469
+ */
470
+ async function resolveServicePorts(workspaceRoot) {
471
+ const composePath = (0, node_path.join)(workspaceRoot, "docker-compose.yml");
472
+ const mappings = parseComposePortMappings(composePath);
473
+ if (mappings.length === 0) return {
474
+ dockerEnv: {},
475
+ ports: {},
476
+ mappings: []
477
+ };
478
+ const savedState = await loadPortState(workspaceRoot);
479
+ const dockerEnv = {};
480
+ const ports = {};
481
+ const assignedPorts = /* @__PURE__ */ new Set();
482
+ logger$13.log("\n🔌 Resolving service ports...");
483
+ for (const mapping of mappings) {
484
+ const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
485
+ if (containerPort !== null) {
486
+ ports[mapping.envVar] = containerPort;
487
+ dockerEnv[mapping.envVar] = String(containerPort);
488
+ assignedPorts.add(containerPort);
489
+ logger$13.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
490
+ continue;
454
491
  }
455
- if (constructs.length === 0) return subscriberInfos;
456
- if (provider !== "aws-lambda") return subscriberInfos;
457
- const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
458
- await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
459
- for (const { key, construct, path } of constructs) {
460
- const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
461
- subscriberInfos.push({
462
- name: key,
463
- handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
464
- subscribedEvents: construct.subscribedEvents || [],
465
- timeout: construct.timeout,
466
- memorySize: construct.memorySize,
467
- environment: await construct.getEnvironment()
468
- });
469
- logger$13.log(`Generated subscriber handler: ${key}`);
492
+ const savedPort = savedState[mapping.envVar];
493
+ if (savedPort && !assignedPorts.has(savedPort) && await isPortAvailable(savedPort)) {
494
+ ports[mapping.envVar] = savedPort;
495
+ dockerEnv[mapping.envVar] = String(savedPort);
496
+ assignedPorts.add(savedPort);
497
+ logger$13.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
498
+ continue;
470
499
  }
471
- return subscriberInfos;
472
- }
473
- async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
500
+ let resolvedPort = await findAvailablePort(mapping.defaultPort);
501
+ while (assignedPorts.has(resolvedPort)) resolvedPort = await findAvailablePort(resolvedPort + 1);
502
+ ports[mapping.envVar] = resolvedPort;
503
+ dockerEnv[mapping.envVar] = String(resolvedPort);
504
+ assignedPorts.add(resolvedPort);
505
+ if (resolvedPort !== mapping.defaultPort) logger$13.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
506
+ else logger$13.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
507
+ }
508
+ await savePortState(workspaceRoot, ports);
509
+ return {
510
+ dockerEnv,
511
+ ports,
512
+ mappings
513
+ };
514
+ }
515
+ /**
516
+ * Replace a port in a URL string.
517
+ * Handles both `hostname:port` and `localhost:port` patterns.
518
+ * @internal Exported for testing
519
+ */
520
+ function replacePortInUrl(url, oldPort, newPort) {
521
+ if (oldPort === newPort) return url;
522
+ let result = url.replace(new RegExp(`:${oldPort}(?=[/?#]|$)`, "g"), `:${newPort}`);
523
+ result = result.replace(new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, "gi"), `%3A${newPort}`);
524
+ return result;
525
+ }
526
+ /**
527
+ * Rewrite connection URLs and port vars in secrets with resolved ports.
528
+ * Uses the parsed compose mappings to determine which default ports to replace.
529
+ * Pure transform — does not modify secrets on disk.
530
+ * @internal Exported for testing
531
+ */
532
+ function rewriteUrlsWithPorts(secrets, resolvedPorts) {
533
+ const { ports, mappings } = resolvedPorts;
534
+ const result = { ...secrets };
535
+ const portReplacements = [];
536
+ const serviceNames = /* @__PURE__ */ new Set();
537
+ for (const mapping of mappings) {
538
+ serviceNames.add(mapping.service);
539
+ const resolved = ports[mapping.envVar];
540
+ if (resolved !== void 0) portReplacements.push({
541
+ defaultPort: mapping.defaultPort,
542
+ resolvedPort: resolved
543
+ });
544
+ }
545
+ for (const [key, value] of Object.entries(result)) {
546
+ if (!key.endsWith("_HOST")) continue;
547
+ if (serviceNames.has(value)) result[key] = "localhost";
548
+ }
549
+ for (const [key, value] of Object.entries(result)) {
550
+ if (!key.endsWith("_PORT")) continue;
551
+ for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
552
+ }
553
+ for (const [key, value] of Object.entries(result)) {
554
+ if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && !key.endsWith("_CONNECTION_STRING") && key !== "DATABASE_URL") continue;
555
+ let rewritten = value;
556
+ for (const name$1 of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name$1}:`, "g"), "@localhost:");
557
+ for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
558
+ result[key] = rewritten;
559
+ }
560
+ return result;
561
+ }
562
+ /**
563
+ * Build the environment variables to pass to `docker compose up`.
564
+ * Merges process.env, secrets, and port mappings so that Docker Compose
565
+ * can interpolate variables like ${POSTGRES_USER} correctly.
566
+ * @internal Exported for testing
567
+ */
568
+ function buildDockerComposeEnv(secretsEnv, portEnv) {
569
+ return {
570
+ ...process.env,
571
+ ...secretsEnv,
572
+ ...portEnv
573
+ };
574
+ }
575
+ /**
576
+ * Parse all service names from a docker-compose.yml file.
577
+ * @internal Exported for testing
578
+ */
579
+ function parseComposeServiceNames(composePath) {
580
+ if (!(0, node_fs.existsSync)(composePath)) return [];
581
+ const content = (0, node_fs.readFileSync)(composePath, "utf-8");
582
+ const compose = (0, yaml.parse)(content);
583
+ return Object.keys(compose?.services ?? {});
584
+ }
585
+ /**
586
+ * Start docker-compose services for a single-app project (no workspace config).
587
+ * Starts all services defined in docker-compose.yml.
588
+ */
589
+ async function startComposeServices(cwd, portEnv, secretsEnv) {
590
+ const composeFile = (0, node_path.join)(cwd, "docker-compose.yml");
591
+ if (!(0, node_fs.existsSync)(composeFile)) return;
592
+ const servicesToStart = parseComposeServiceNames(composeFile);
593
+ if (servicesToStart.length === 0) return;
594
+ logger$13.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
595
+ try {
596
+ (0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
597
+ cwd,
598
+ stdio: "inherit",
599
+ env: buildDockerComposeEnv(secretsEnv, portEnv)
600
+ });
601
+ logger$13.log("✅ Services started");
602
+ } catch (error) {
603
+ logger$13.error("❌ Failed to start services:", error.message);
604
+ throw error;
605
+ }
606
+ }
607
+ /**
608
+ * Start docker-compose services for a workspace.
609
+ * Discovers all services from docker-compose.yml and starts everything
610
+ * except app services (which are managed by turbo).
611
+ * @internal Exported for testing
612
+ */
613
+ async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
614
+ const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
615
+ if (!(0, node_fs.existsSync)(composeFile)) return;
616
+ const allServices = parseComposeServiceNames(composeFile);
617
+ const appNames = new Set(Object.keys(workspace.apps));
618
+ const servicesToStart = allServices.filter((name$1) => !appNames.has(name$1));
619
+ if (servicesToStart.length === 0) return;
620
+ logger$13.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
621
+ try {
622
+ (0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
623
+ cwd: workspace.root,
624
+ stdio: "inherit",
625
+ env: buildDockerComposeEnv(secretsEnv, portEnv)
626
+ });
627
+ logger$13.log("✅ Services started");
628
+ } catch (error) {
629
+ logger$13.error("❌ Failed to start services:", error.message);
630
+ throw error;
631
+ }
632
+ }
633
+ /**
634
+ * Load and flatten secrets for an app from encrypted storage.
635
+ * For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
636
+ * @internal Exported for testing
637
+ */
638
+ async function loadSecretsForApp(secretsRoot, appName, stages = ["dev", "development"]) {
639
+ let secrets = {};
640
+ for (const stage of stages) if (require_storage.secretsExist(stage, secretsRoot)) {
641
+ const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
642
+ if (stageSecrets) {
643
+ logger$13.log(`🔐 Loading secrets from stage: ${stage}`);
644
+ secrets = require_storage.toEmbeddableSecrets(stageSecrets);
645
+ break;
646
+ }
647
+ }
648
+ if (Object.keys(secrets).length === 0) return {};
649
+ if (!appName) return secrets;
650
+ const prefix = appName.toUpperCase();
651
+ const mapped = { ...secrets };
652
+ const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
653
+ if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
654
+ return mapped;
655
+ }
656
+ /**
657
+ * Walk up the directory tree to find the root containing .gkm/secrets/.
658
+ * @internal Exported for testing
659
+ */
660
+ function findSecretsRoot(startDir) {
661
+ let dir = startDir;
662
+ while (dir !== "/") {
663
+ if ((0, node_fs.existsSync)((0, node_path.join)(dir, ".gkm", "secrets"))) return dir;
664
+ const parent = (0, node_path.dirname)(dir);
665
+ if (parent === dir) break;
666
+ dir = parent;
667
+ }
668
+ return startDir;
669
+ }
670
+ /**
671
+ * Generate the credentials injection code snippet.
672
+ * This is the common logic used by both entry wrapper and exec preload.
673
+ * @internal
674
+ */
675
+ function generateCredentialsInjection(secretsJsonPath) {
676
+ return `import { existsSync, readFileSync } from 'node:fs';
677
+
678
+ // Inject dev secrets via globalThis and process.env
679
+ // Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
680
+ // Object.assign on the Credentials export only mutates one module copy.
681
+ const secretsPath = '${secretsJsonPath}';
682
+ if (existsSync(secretsPath)) {
683
+ const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
684
+ globalThis.__gkm_credentials__ = secrets;
685
+ Object.assign(process.env, secrets);
686
+ }
687
+ `;
688
+ }
689
+ /**
690
+ * Create a preload script that injects secrets into Credentials.
691
+ * Used by `gkm exec` to inject secrets before running any command.
692
+ * @internal Exported for testing
693
+ */
694
+ async function createCredentialsPreload(preloadPath, secretsJsonPath) {
695
+ const content = `/**
696
+ * Credentials preload generated by 'gkm exec'
697
+ * This file is loaded via NODE_OPTIONS="--import <path>"
698
+ */
699
+ ${generateCredentialsInjection(secretsJsonPath)}`;
700
+ await (0, node_fs_promises.writeFile)(preloadPath, content);
701
+ }
702
+ /**
703
+ * Create a wrapper script that injects secrets before importing the entry file.
704
+ * @internal Exported for testing
705
+ */
706
+ async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
707
+ const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
708
+ ` : "";
709
+ const content = `#!/usr/bin/env node
710
+ /**
711
+ * Entry wrapper generated by 'gkm dev --entry'
712
+ */
713
+ ${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)
714
+ await import('${entryPath}');
715
+ `;
716
+ await (0, node_fs_promises.writeFile)(wrapperPath, content);
717
+ }
718
+ /**
719
+ * Prepare credentials for dev/exec/test modes.
720
+ * Loads workspace config, secrets, resolves Docker ports, rewrites URLs,
721
+ * injects PORT, dependency URLs, and writes credentials JSON.
722
+ *
723
+ * @param options.resolveDockerPorts - How to resolve Docker ports:
724
+ * - `'full'` (default): probe running containers, saved state, then find available ports. Used by dev/test.
725
+ * - `'readonly'`: check running containers and saved state only, never probe for new ports. Used by exec.
726
+ * @param options.stages - Secret stages to try, in order. Default: ['dev', 'development'].
727
+ * @param options.startDocker - Start Docker Compose services after port resolution. Default: false.
728
+ * @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.
729
+ * @internal Exported for testing
730
+ */
731
+ async function prepareEntryCredentials(options) {
732
+ const cwd = options.cwd ?? process.cwd();
733
+ const portMode = options.resolveDockerPorts ?? "full";
734
+ let workspaceAppPort;
735
+ let secretsRoot = cwd;
736
+ let appName;
737
+ let appInfo;
738
+ try {
739
+ appInfo = await require_config.loadWorkspaceAppInfo(cwd);
740
+ workspaceAppPort = appInfo.app.port;
741
+ secretsRoot = appInfo.workspaceRoot;
742
+ appName = appInfo.appName;
743
+ } catch (error) {
744
+ logger$13.log(`⚠️ Could not load workspace config: ${error.message}`);
745
+ secretsRoot = findSecretsRoot(cwd);
746
+ appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
747
+ }
748
+ const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
749
+ const credentials = await loadSecretsForApp(secretsRoot, appName, options.stages);
750
+ credentials.PORT = String(resolvedPort);
751
+ const composePath = (0, node_path.join)(secretsRoot, "docker-compose.yml");
752
+ const mappings = parseComposePortMappings(composePath);
753
+ if (mappings.length > 0) {
754
+ let resolvedPorts;
755
+ if (portMode === "full") resolvedPorts = await resolveServicePorts(secretsRoot);
756
+ else {
757
+ const savedPorts = await loadPortState(secretsRoot);
758
+ const ports = {};
759
+ for (const mapping of mappings) {
760
+ const containerPort = getContainerHostPort(secretsRoot, mapping.service, mapping.containerPort);
761
+ if (containerPort !== null) ports[mapping.envVar] = containerPort;
762
+ else {
763
+ const saved = savedPorts[mapping.envVar];
764
+ if (saved !== void 0) ports[mapping.envVar] = saved;
765
+ }
766
+ }
767
+ resolvedPorts = {
768
+ dockerEnv: {},
769
+ ports,
770
+ mappings
771
+ };
772
+ }
773
+ if (options.startDocker) if (appInfo) await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, credentials);
774
+ else await startComposeServices(secretsRoot, resolvedPorts.dockerEnv, credentials);
775
+ if (Object.keys(resolvedPorts.ports).length > 0) {
776
+ const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
777
+ Object.assign(credentials, rewritten);
778
+ logger$13.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
779
+ }
780
+ }
781
+ if (appInfo?.appName) {
782
+ const depEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
783
+ Object.assign(credentials, depEnv);
784
+ }
785
+ const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
786
+ await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
787
+ const secretsFileName = options.secretsFileName ?? (appName ? `dev-secrets-${appName}.json` : "dev-secrets.json");
788
+ const secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
789
+ await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(credentials, null, 2));
790
+ return {
791
+ credentials,
792
+ resolvedPort,
793
+ secretsJsonPath,
794
+ appName,
795
+ secretsRoot,
796
+ appInfo
797
+ };
798
+ }
799
+
800
+ //#endregion
801
+ //#region src/generators/CronGenerator.ts
802
+ var CronGenerator = class extends require_openapi.ConstructGenerator {
803
+ async build(context, constructs, outputDir, options) {
804
+ const provider = options?.provider || "aws-lambda";
805
+ const logger$15 = console;
806
+ const cronInfos = [];
807
+ if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
808
+ const cronsDir = (0, node_path.join)(outputDir, "crons");
809
+ await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
810
+ for (const { key, construct, path } of constructs) {
811
+ const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
812
+ cronInfos.push({
813
+ name: key,
814
+ handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
815
+ schedule: construct.schedule || "rate(1 hour)",
816
+ timeout: construct.timeout,
817
+ memorySize: construct.memorySize,
818
+ environment: await construct.getEnvironment()
819
+ });
820
+ logger$15.log(`Generated cron handler: ${key}`);
821
+ }
822
+ return cronInfos;
823
+ }
824
+ isConstruct(value) {
825
+ return __geekmidas_constructs_crons.Cron.isCron(value);
826
+ }
827
+ async generateCronHandler(outputDir, sourceFile, exportName, context) {
828
+ const handlerFileName = `${exportName}.ts`;
829
+ const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
830
+ const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
831
+ const importPath = relativePath.replace(/\.ts$/, ".js");
832
+ const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
833
+ const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
834
+ const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
835
+ import { ${exportName} } from '${importPath}';
836
+ import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
837
+ import ${context.loggerImportPattern} from '${relativeLoggerPath}';
838
+
839
+ const adapter = new AWSScheduledFunction(envParser, ${exportName});
840
+
841
+ export const handler = adapter.handler;
842
+ `;
843
+ await (0, node_fs_promises.writeFile)(handlerPath, content);
844
+ return handlerPath;
845
+ }
846
+ };
847
+
848
+ //#endregion
849
+ //#region src/generators/FunctionGenerator.ts
850
+ var FunctionGenerator = class extends require_openapi.ConstructGenerator {
851
+ isConstruct(value) {
852
+ return __geekmidas_constructs_functions.Function.isFunction(value);
853
+ }
854
+ async build(context, constructs, outputDir, options) {
855
+ const provider = options?.provider || "aws-lambda";
856
+ const logger$15 = console;
857
+ const functionInfos = [];
858
+ if (constructs.length === 0 || provider !== "aws-lambda") return functionInfos;
859
+ const functionsDir = (0, node_path.join)(outputDir, "functions");
860
+ await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
861
+ for (const { key, construct, path } of constructs) {
862
+ const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
863
+ functionInfos.push({
864
+ name: key,
865
+ handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
866
+ timeout: construct.timeout,
867
+ memorySize: construct.memorySize,
868
+ environment: await construct.getEnvironment()
869
+ });
870
+ logger$15.log(`Generated function handler: ${key}`);
871
+ }
872
+ return functionInfos;
873
+ }
874
+ async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
875
+ const handlerFileName = `${exportName}.ts`;
876
+ const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
877
+ const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
878
+ const importPath = relativePath.replace(/\.ts$/, ".js");
879
+ const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
880
+ const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
881
+ const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
882
+ import { ${exportName} } from '${importPath}';
883
+ import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
884
+ import ${context.loggerImportPattern} from '${relativeLoggerPath}';
885
+
886
+ const adapter = new AWSLambdaFunction(envParser, ${exportName});
887
+
888
+ export const handler = adapter.handler;
889
+ `;
890
+ await (0, node_fs_promises.writeFile)(handlerPath, content);
891
+ return handlerPath;
892
+ }
893
+ };
894
+
895
+ //#endregion
896
+ //#region src/generators/SubscriberGenerator.ts
897
+ var SubscriberGenerator = class extends require_openapi.ConstructGenerator {
898
+ isConstruct(value) {
899
+ return __geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
900
+ }
901
+ async build(context, constructs, outputDir, options) {
902
+ const provider = options?.provider || "aws-lambda";
903
+ const logger$15 = console;
904
+ const subscriberInfos = [];
905
+ if (provider === "server") {
906
+ await this.generateServerSubscribersFile(outputDir, constructs);
907
+ logger$15.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
908
+ return subscriberInfos;
909
+ }
910
+ if (constructs.length === 0) return subscriberInfos;
911
+ if (provider !== "aws-lambda") return subscriberInfos;
912
+ const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
913
+ await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
914
+ for (const { key, construct, path } of constructs) {
915
+ const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
916
+ subscriberInfos.push({
917
+ name: key,
918
+ handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
919
+ subscribedEvents: construct.subscribedEvents || [],
920
+ timeout: construct.timeout,
921
+ memorySize: construct.memorySize,
922
+ environment: await construct.getEnvironment()
923
+ });
924
+ logger$15.log(`Generated subscriber handler: ${key}`);
925
+ }
926
+ return subscriberInfos;
927
+ }
928
+ async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
474
929
  const handlerFileName = `${exportName}.ts`;
475
930
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
476
931
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
@@ -632,222 +1087,69 @@ export async function setupSubscribers(
632
1087
  };
633
1088
 
634
1089
  //#endregion
635
- //#region src/dev/index.ts
636
- const logger$11 = console;
637
- /**
638
- * Load environment files
639
- * @internal Exported for testing
640
- */
641
- function loadEnvFiles(envConfig, cwd = process.cwd()) {
642
- const loaded = [];
643
- const missing = [];
644
- const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
645
- for (const envFile of envFiles) {
646
- const envPath = (0, node_path.resolve)(cwd, envFile);
647
- if ((0, node_fs.existsSync)(envPath)) {
648
- (0, dotenv.config)({
649
- path: envPath,
650
- override: true,
651
- quiet: true
652
- });
653
- loaded.push(envFile);
654
- } else if (envConfig) missing.push(envFile);
655
- }
656
- return {
657
- loaded,
658
- missing
659
- };
660
- }
1090
+ //#region src/exec/index.ts
1091
+ const logger$12 = console;
661
1092
  /**
662
- * Check if a port is available
663
- * @internal Exported for testing
1093
+ * Run a command with secrets injected into Credentials.
1094
+ * Uses Node's --import flag to preload a script that populates Credentials
1095
+ * before the command loads any modules that depend on them.
1096
+ *
1097
+ * @example
1098
+ * ```bash
1099
+ * gkm exec -- npx @better-auth/cli migrate
1100
+ * gkm exec -- npx prisma migrate dev
1101
+ * ```
664
1102
  */
665
- async function isPortAvailable(port) {
666
- return new Promise((resolve$3) => {
667
- const server = (0, node_net.createServer)();
668
- server.once("error", (err) => {
669
- if (err.code === "EADDRINUSE") resolve$3(false);
670
- else resolve$3(false);
671
- });
672
- server.once("listening", () => {
673
- server.close();
674
- resolve$3(true);
675
- });
676
- server.listen(port);
1103
+ async function execCommand(commandArgs, options = {}) {
1104
+ const cwd = options.cwd ?? process.cwd();
1105
+ if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
1106
+ const defaultEnv = loadEnvFiles(".env");
1107
+ if (defaultEnv.loaded.length > 0) logger$12.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1108
+ const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({
1109
+ cwd,
1110
+ resolveDockerPorts: "readonly"
677
1111
  });
1112
+ if (appName) logger$12.log(`📦 App: ${appName}`);
1113
+ const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1114
+ if (secretCount > 0) logger$12.log(`🔐 Loaded ${secretCount} secret(s)`);
1115
+ const preloadDir = (0, node_path.join)(cwd, ".gkm");
1116
+ await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
1117
+ const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.ts");
1118
+ await createCredentialsPreload(preloadPath, secretsJsonPath);
1119
+ const [cmd, ...rawArgs] = commandArgs;
1120
+ if (!cmd) throw new Error("No command specified");
1121
+ const args = rawArgs.map((arg) => arg.replace(/\$PORT\b/g, credentials.PORT ?? "3000"));
1122
+ logger$12.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
1123
+ const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
1124
+ const tsxImport = "--import=tsx";
1125
+ const preloadImport = `--import=${preloadPath}`;
1126
+ const nodeOptions = [
1127
+ existingNodeOptions,
1128
+ tsxImport,
1129
+ preloadImport
1130
+ ].filter(Boolean).join(" ");
1131
+ const child = (0, node_child_process.spawn)(cmd, args, {
1132
+ cwd,
1133
+ stdio: "inherit",
1134
+ env: {
1135
+ ...process.env,
1136
+ ...credentials,
1137
+ NODE_OPTIONS: nodeOptions
1138
+ }
1139
+ });
1140
+ const exitCode = await new Promise((resolve$4) => {
1141
+ child.on("close", (code) => resolve$4(code ?? 0));
1142
+ child.on("error", (error) => {
1143
+ logger$12.error(`Failed to run command: ${error.message}`);
1144
+ resolve$4(1);
1145
+ });
1146
+ });
1147
+ if (exitCode !== 0) process.exit(exitCode);
678
1148
  }
679
- /**
680
- * Find an available port starting from the preferred port
681
- * @internal Exported for testing
682
- */
683
- async function findAvailablePort(preferredPort, maxAttempts = 10) {
684
- for (let i = 0; i < maxAttempts; i++) {
685
- const port = preferredPort + i;
686
- if (await isPortAvailable(port)) return port;
687
- logger$11.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
688
- }
689
- throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
690
- }
691
- const PORT_STATE_PATH = ".gkm/ports.json";
692
- /**
693
- * Parse docker-compose.yml and extract all port mappings that use env var interpolation.
694
- * Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
695
- * Fixed port mappings like `'5050:80'` are skipped.
696
- * @internal Exported for testing
697
- */
698
- function parseComposePortMappings(composePath) {
699
- if (!(0, node_fs.existsSync)(composePath)) return [];
700
- const content = (0, node_fs.readFileSync)(composePath, "utf-8");
701
- const compose = (0, yaml.parse)(content);
702
- if (!compose?.services) return [];
703
- const results = [];
704
- for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
705
- const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
706
- if (match?.[1] && match[2] && match[3]) results.push({
707
- service: serviceName,
708
- envVar: match[1],
709
- defaultPort: Number(match[2]),
710
- containerPort: Number(match[3])
711
- });
712
- }
713
- return results;
714
- }
715
- /**
716
- * Load saved port state from .gkm/ports.json.
717
- * @internal Exported for testing
718
- */
719
- async function loadPortState(workspaceRoot) {
720
- try {
721
- const raw = await (0, node_fs_promises.readFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), "utf-8");
722
- return JSON.parse(raw);
723
- } catch {
724
- return {};
725
- }
726
- }
727
- /**
728
- * Save port state to .gkm/ports.json.
729
- * @internal Exported for testing
730
- */
731
- async function savePortState(workspaceRoot, ports) {
732
- const dir = (0, node_path.join)(workspaceRoot, ".gkm");
733
- await (0, node_fs_promises.mkdir)(dir, { recursive: true });
734
- await (0, node_fs_promises.writeFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
735
- }
736
- /**
737
- * Check if a project's own Docker container is running and return its host port.
738
- * Uses `docker compose port` scoped to the project's compose file.
739
- * @internal Exported for testing
740
- */
741
- function getContainerHostPort(workspaceRoot, service, containerPort) {
742
- try {
743
- const result = (0, node_child_process.execSync)(`docker compose port ${service} ${containerPort}`, {
744
- cwd: workspaceRoot,
745
- stdio: "pipe"
746
- }).toString().trim();
747
- const match = result.match(/:(\d+)$/);
748
- return match ? Number(match[1]) : null;
749
- } catch {
750
- return null;
751
- }
752
- }
753
- /**
754
- * Resolve host ports for Docker services by parsing docker-compose.yml.
755
- * Priority: running container → saved state → find available port.
756
- * Persists resolved ports to .gkm/ports.json.
757
- * @internal Exported for testing
758
- */
759
- async function resolveServicePorts(workspaceRoot) {
760
- const composePath = (0, node_path.join)(workspaceRoot, "docker-compose.yml");
761
- const mappings = parseComposePortMappings(composePath);
762
- if (mappings.length === 0) return {
763
- dockerEnv: {},
764
- ports: {},
765
- mappings: []
766
- };
767
- const savedState = await loadPortState(workspaceRoot);
768
- const dockerEnv = {};
769
- const ports = {};
770
- const assignedPorts = /* @__PURE__ */ new Set();
771
- logger$11.log("\n🔌 Resolving service ports...");
772
- for (const mapping of mappings) {
773
- const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
774
- if (containerPort !== null) {
775
- ports[mapping.envVar] = containerPort;
776
- dockerEnv[mapping.envVar] = String(containerPort);
777
- assignedPorts.add(containerPort);
778
- logger$11.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
779
- continue;
780
- }
781
- const savedPort = savedState[mapping.envVar];
782
- if (savedPort && !assignedPorts.has(savedPort) && await isPortAvailable(savedPort)) {
783
- ports[mapping.envVar] = savedPort;
784
- dockerEnv[mapping.envVar] = String(savedPort);
785
- assignedPorts.add(savedPort);
786
- logger$11.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
787
- continue;
788
- }
789
- let resolvedPort = await findAvailablePort(mapping.defaultPort);
790
- while (assignedPorts.has(resolvedPort)) resolvedPort = await findAvailablePort(resolvedPort + 1);
791
- ports[mapping.envVar] = resolvedPort;
792
- dockerEnv[mapping.envVar] = String(resolvedPort);
793
- assignedPorts.add(resolvedPort);
794
- if (resolvedPort !== mapping.defaultPort) logger$11.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
795
- else logger$11.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
796
- }
797
- await savePortState(workspaceRoot, ports);
798
- return {
799
- dockerEnv,
800
- ports,
801
- mappings
802
- };
803
- }
804
- /**
805
- * Replace a port in a URL string.
806
- * Handles both `hostname:port` and `localhost:port` patterns.
807
- * @internal Exported for testing
808
- */
809
- function replacePortInUrl(url, oldPort, newPort) {
810
- if (oldPort === newPort) return url;
811
- let result = url.replace(new RegExp(`:${oldPort}(?=[/?#]|$)`, "g"), `:${newPort}`);
812
- result = result.replace(new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, "gi"), `%3A${newPort}`);
813
- return result;
814
- }
815
- /**
816
- * Rewrite connection URLs and port vars in secrets with resolved ports.
817
- * Uses the parsed compose mappings to determine which default ports to replace.
818
- * Pure transform — does not modify secrets on disk.
819
- * @internal Exported for testing
820
- */
821
- function rewriteUrlsWithPorts(secrets, resolvedPorts) {
822
- const { ports, mappings } = resolvedPorts;
823
- const result = { ...secrets };
824
- const portReplacements = [];
825
- const serviceNames = /* @__PURE__ */ new Set();
826
- for (const mapping of mappings) {
827
- serviceNames.add(mapping.service);
828
- const resolved = ports[mapping.envVar];
829
- if (resolved !== void 0) portReplacements.push({
830
- defaultPort: mapping.defaultPort,
831
- resolvedPort: resolved
832
- });
833
- }
834
- for (const [key, value] of Object.entries(result)) {
835
- if (!key.endsWith("_HOST")) continue;
836
- if (serviceNames.has(value)) result[key] = "localhost";
837
- }
838
- for (const [key, value] of Object.entries(result)) {
839
- if (!key.endsWith("_PORT")) continue;
840
- for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
841
- }
842
- for (const [key, value] of Object.entries(result)) {
843
- if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && !key.endsWith("_CONNECTION_STRING") && key !== "DATABASE_URL") continue;
844
- let rewritten = value;
845
- for (const name$1 of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name$1}:`, "g"), "@localhost:");
846
- for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
847
- result[key] = rewritten;
848
- }
849
- return result;
850
- }
1149
+
1150
+ //#endregion
1151
+ //#region src/dev/index.ts
1152
+ const logger$11 = console;
851
1153
  /**
852
1154
  * Normalize telescope configuration
853
1155
  * @internal Exported for testing
@@ -1204,103 +1506,6 @@ async function loadDevSecrets(workspace) {
1204
1506
  return {};
1205
1507
  }
1206
1508
  /**
1207
- * Load secrets from a path for dev mode.
1208
- * For single app: returns secrets as-is.
1209
- * For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
1210
- * @internal Exported for testing
1211
- */
1212
- async function loadSecretsForApp(secretsRoot, appName) {
1213
- const stages = ["dev", "development"];
1214
- let secrets = {};
1215
- for (const stage of stages) if (require_storage.secretsExist(stage, secretsRoot)) {
1216
- const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
1217
- if (stageSecrets) {
1218
- logger$11.log(`🔐 Loading secrets from stage: ${stage}`);
1219
- secrets = require_storage.toEmbeddableSecrets(stageSecrets);
1220
- break;
1221
- }
1222
- }
1223
- if (Object.keys(secrets).length === 0) return {};
1224
- if (!appName) return secrets;
1225
- const prefix = appName.toUpperCase();
1226
- const mapped = { ...secrets };
1227
- const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
1228
- if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
1229
- return mapped;
1230
- }
1231
- /**
1232
- * Build the environment variables to pass to `docker compose up`.
1233
- * Merges process.env, secrets, and port mappings so that Docker Compose
1234
- * can interpolate variables like ${POSTGRES_USER} correctly.
1235
- * @internal Exported for testing
1236
- */
1237
- function buildDockerComposeEnv(secretsEnv, portEnv) {
1238
- return {
1239
- ...process.env,
1240
- ...secretsEnv,
1241
- ...portEnv
1242
- };
1243
- }
1244
- /**
1245
- * Parse all service names from a docker-compose.yml file.
1246
- * @internal Exported for testing
1247
- */
1248
- function parseComposeServiceNames(composePath) {
1249
- if (!(0, node_fs.existsSync)(composePath)) return [];
1250
- const content = (0, node_fs.readFileSync)(composePath, "utf-8");
1251
- const compose = (0, yaml.parse)(content);
1252
- return Object.keys(compose?.services ?? {});
1253
- }
1254
- /**
1255
- * Start docker-compose services for the workspace.
1256
- * Parses the docker-compose.yml to discover all services and starts
1257
- * everything except app services (which are managed by turbo).
1258
- * This ensures manually added services are always started.
1259
- * @internal Exported for testing
1260
- */
1261
- /**
1262
- * Start docker-compose services for a single-app project (no workspace config).
1263
- * Starts all services defined in docker-compose.yml.
1264
- */
1265
- async function startComposeServices(cwd, portEnv, secretsEnv) {
1266
- const composeFile = (0, node_path.join)(cwd, "docker-compose.yml");
1267
- if (!(0, node_fs.existsSync)(composeFile)) return;
1268
- const servicesToStart = parseComposeServiceNames(composeFile);
1269
- if (servicesToStart.length === 0) return;
1270
- logger$11.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
1271
- try {
1272
- (0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
1273
- cwd,
1274
- stdio: "inherit",
1275
- env: buildDockerComposeEnv(secretsEnv, portEnv)
1276
- });
1277
- logger$11.log("✅ Services started");
1278
- } catch (error) {
1279
- logger$11.error("❌ Failed to start services:", error.message);
1280
- throw error;
1281
- }
1282
- }
1283
- async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
1284
- const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
1285
- if (!(0, node_fs.existsSync)(composeFile)) return;
1286
- const allServices = parseComposeServiceNames(composeFile);
1287
- const appNames = new Set(Object.keys(workspace.apps));
1288
- const servicesToStart = allServices.filter((name$1) => !appNames.has(name$1));
1289
- if (servicesToStart.length === 0) return;
1290
- logger$11.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
1291
- try {
1292
- (0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
1293
- cwd: workspace.root,
1294
- stdio: "inherit",
1295
- env: buildDockerComposeEnv(secretsEnv, portEnv)
1296
- });
1297
- logger$11.log("✅ Services started");
1298
- } catch (error) {
1299
- logger$11.error("❌ Failed to start services:", error.message);
1300
- throw error;
1301
- }
1302
- }
1303
- /**
1304
1509
  * Workspace dev command - orchestrates multi-app development using Turbo.
1305
1510
  *
1306
1511
  * Flow:
@@ -1467,7 +1672,7 @@ async function workspaceDevCommand(workspace, options) {
1467
1672
  };
1468
1673
  process.on("SIGINT", shutdown);
1469
1674
  process.on("SIGTERM", shutdown);
1470
- return new Promise((resolve$3, reject) => {
1675
+ return new Promise((resolve$4, reject) => {
1471
1676
  turboProcess.on("error", (error) => {
1472
1677
  logger$11.error("❌ Turbo error:", error);
1473
1678
  reject(error);
@@ -1475,7 +1680,7 @@ async function workspaceDevCommand(workspace, options) {
1475
1680
  turboProcess.on("exit", (code) => {
1476
1681
  if (openApiWatcher) openApiWatcher.close().catch(() => {});
1477
1682
  if (code !== null && code !== 0) reject(new Error(`Turbo exited with code ${code}`));
1478
- else resolve$3();
1683
+ else resolve$4();
1479
1684
  });
1480
1685
  });
1481
1686
  }
@@ -1503,105 +1708,6 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
1503
1708
  ]);
1504
1709
  }
1505
1710
  /**
1506
- * Find the directory containing .gkm/secrets/.
1507
- * Walks up from cwd until it finds one, or returns cwd.
1508
- * @internal Exported for testing
1509
- */
1510
- function findSecretsRoot(startDir) {
1511
- let dir = startDir;
1512
- while (dir !== "/") {
1513
- if ((0, node_fs.existsSync)((0, node_path.join)(dir, ".gkm", "secrets"))) return dir;
1514
- const parent = (0, node_path.dirname)(dir);
1515
- if (parent === dir) break;
1516
- dir = parent;
1517
- }
1518
- return startDir;
1519
- }
1520
- /**
1521
- * Generate the credentials injection code snippet.
1522
- * This is the common logic used by both entry wrapper and exec preload.
1523
- * @internal
1524
- */
1525
- function generateCredentialsInjection(secretsJsonPath) {
1526
- return `import { existsSync, readFileSync } from 'node:fs';
1527
-
1528
- // Inject dev secrets via globalThis and process.env
1529
- // Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
1530
- // Object.assign on the Credentials export only mutates one module copy.
1531
- const secretsPath = '${secretsJsonPath}';
1532
- if (existsSync(secretsPath)) {
1533
- const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1534
- globalThis.__gkm_credentials__ = secrets;
1535
- Object.assign(process.env, secrets);
1536
- }
1537
- `;
1538
- }
1539
- /**
1540
- * Create a preload script that injects secrets into Credentials.
1541
- * Used by `gkm exec` to inject secrets before running any command.
1542
- * @internal Exported for testing
1543
- */
1544
- async function createCredentialsPreload(preloadPath, secretsJsonPath) {
1545
- const content = `/**
1546
- * Credentials preload generated by 'gkm exec'
1547
- * This file is loaded via NODE_OPTIONS="--import <path>"
1548
- */
1549
- ${generateCredentialsInjection(secretsJsonPath)}`;
1550
- await (0, node_fs_promises.writeFile)(preloadPath, content);
1551
- }
1552
- /**
1553
- * Create a wrapper script that injects secrets before importing the entry file.
1554
- * @internal Exported for testing
1555
- */
1556
- async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
1557
- const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
1558
- ` : "";
1559
- const content = `#!/usr/bin/env node
1560
- /**
1561
- * Entry wrapper generated by 'gkm dev --entry'
1562
- */
1563
- ${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)
1564
- await import('${entryPath}');
1565
- `;
1566
- await (0, node_fs_promises.writeFile)(wrapperPath, content);
1567
- }
1568
- /**
1569
- * Prepare credentials for entry dev mode.
1570
- * Loads workspace config, secrets, and injects PORT.
1571
- * @internal Exported for testing
1572
- */
1573
- async function prepareEntryCredentials(options) {
1574
- const cwd = options.cwd ?? process.cwd();
1575
- let workspaceAppPort;
1576
- let secretsRoot = cwd;
1577
- let appName;
1578
- try {
1579
- const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
1580
- workspaceAppPort = appInfo.app.port;
1581
- secretsRoot = appInfo.workspaceRoot;
1582
- appName = appInfo.appName;
1583
- } catch (error) {
1584
- logger$11.log(`⚠️ Could not load workspace config: ${error.message}`);
1585
- secretsRoot = findSecretsRoot(cwd);
1586
- appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
1587
- }
1588
- const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
1589
- const credentials = await loadSecretsForApp(secretsRoot, appName);
1590
- credentials.PORT = String(resolvedPort);
1591
- const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
1592
- await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
1593
- const secretsFileName = appName ? `dev-secrets-${appName}.json` : "dev-secrets.json";
1594
- const secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
1595
- await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(credentials, null, 2));
1596
- return {
1597
- credentials,
1598
- resolvedPort,
1599
- secretsJsonPath,
1600
- appName,
1601
- secretsRoot
1602
- };
1603
- }
1604
- /**
1605
1711
  * Run any TypeScript file with secret injection.
1606
1712
  * Does not require gkm.config.ts.
1607
1713
  */
@@ -1686,12 +1792,12 @@ var EntryRunner = class {
1686
1792
  if (code !== null && code !== 0 && code !== 143) logger$11.error(`❌ Process exited with code ${code}`);
1687
1793
  this.isRunning = false;
1688
1794
  });
1689
- await new Promise((resolve$3) => setTimeout(resolve$3, 500));
1795
+ await new Promise((resolve$4) => setTimeout(resolve$4, 500));
1690
1796
  if (this.isRunning) logger$11.log(`\n🎉 Running at http://localhost:${this.port}`);
1691
1797
  }
1692
1798
  async restart() {
1693
1799
  this.stopProcess();
1694
- await new Promise((resolve$3) => setTimeout(resolve$3, 500));
1800
+ await new Promise((resolve$4) => setTimeout(resolve$4, 500));
1695
1801
  await this.runProcess();
1696
1802
  }
1697
1803
  stop() {
@@ -1824,7 +1930,7 @@ var DevServer = class {
1824
1930
  if (code !== null && code !== 0 && signal !== "SIGTERM") logger$11.error(`❌ Server exited with code ${code}`);
1825
1931
  this.isRunning = false;
1826
1932
  });
1827
- await new Promise((resolve$3) => setTimeout(resolve$3, 1e3));
1933
+ await new Promise((resolve$4) => setTimeout(resolve$4, 1e3));
1828
1934
  if (this.isRunning) {
1829
1935
  logger$11.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
1830
1936
  if (this.enableOpenApi) logger$11.log(`📚 API Docs available at http://localhost:${this.actualPort}/__docs`);
@@ -1859,7 +1965,7 @@ var DevServer = class {
1859
1965
  let attempts = 0;
1860
1966
  while (attempts < 30) {
1861
1967
  if (await isPortAvailable(portToReuse)) break;
1862
- await new Promise((resolve$3) => setTimeout(resolve$3, 100));
1968
+ await new Promise((resolve$4) => setTimeout(resolve$4, 100));
1863
1969
  attempts++;
1864
1970
  }
1865
1971
  this.requestedPort = portToReuse;
@@ -1876,73 +1982,6 @@ var DevServer = class {
1876
1982
  await fsWriteFile(serverPath, content);
1877
1983
  }
1878
1984
  };
1879
- /**
1880
- * Run a command with secrets injected into Credentials.
1881
- * Uses Node's --import flag to preload a script that populates Credentials
1882
- * before the command loads any modules that depend on them.
1883
- *
1884
- * @example
1885
- * ```bash
1886
- * gkm exec -- npx @better-auth/cli migrate
1887
- * gkm exec -- npx prisma migrate dev
1888
- * ```
1889
- */
1890
- async function execCommand(commandArgs, options = {}) {
1891
- const cwd = options.cwd ?? process.cwd();
1892
- if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
1893
- const defaultEnv = loadEnvFiles(".env");
1894
- if (defaultEnv.loaded.length > 0) logger$11.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1895
- const { credentials, secretsJsonPath, appName, secretsRoot } = await prepareEntryCredentials({ cwd });
1896
- if (appName) logger$11.log(`📦 App: ${appName}`);
1897
- const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1898
- if (secretCount > 0) logger$11.log(`🔐 Loaded ${secretCount} secret(s)`);
1899
- const resolvedPorts = await resolveServicePorts(secretsRoot);
1900
- if (resolvedPorts.mappings.length > 0 && Object.keys(resolvedPorts.ports).length > 0) {
1901
- const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
1902
- Object.assign(credentials, rewritten);
1903
- logger$11.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
1904
- }
1905
- try {
1906
- const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
1907
- if (appInfo.appName) {
1908
- const depEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
1909
- Object.assign(credentials, depEnv);
1910
- }
1911
- } catch {}
1912
- const preloadDir = (0, node_path.join)(cwd, ".gkm");
1913
- await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
1914
- const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.ts");
1915
- await createCredentialsPreload(preloadPath, secretsJsonPath);
1916
- const [cmd, ...rawArgs] = commandArgs;
1917
- if (!cmd) throw new Error("No command specified");
1918
- const args = rawArgs.map((arg) => arg.replace(/\$PORT\b/g, credentials.PORT ?? "3000"));
1919
- logger$11.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
1920
- const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
1921
- const tsxImport = "--import=tsx";
1922
- const preloadImport = `--import=${preloadPath}`;
1923
- const nodeOptions = [
1924
- existingNodeOptions,
1925
- tsxImport,
1926
- preloadImport
1927
- ].filter(Boolean).join(" ");
1928
- const child = (0, node_child_process.spawn)(cmd, args, {
1929
- cwd,
1930
- stdio: "inherit",
1931
- env: {
1932
- ...process.env,
1933
- ...credentials,
1934
- NODE_OPTIONS: nodeOptions
1935
- }
1936
- });
1937
- const exitCode = await new Promise((resolve$3) => {
1938
- child.on("close", (code) => resolve$3(code ?? 0));
1939
- child.on("error", (error) => {
1940
- logger$11.error(`Failed to run command: ${error.message}`);
1941
- resolve$3(1);
1942
- });
1943
- });
1944
- if (exitCode !== 0) process.exit(exitCode);
1945
- }
1946
1985
 
1947
1986
  //#endregion
1948
1987
  //#region src/build/manifests.ts
@@ -2214,7 +2253,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2214
2253
  let masterKey;
2215
2254
  if (context.production?.bundle && !skipBundle) {
2216
2255
  logger$9.log(`\n📦 Bundling production server...`);
2217
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-BhhfkI9T.cjs"));
2256
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-i-az1DZ2.cjs"));
2218
2257
  const allConstructs = [
2219
2258
  ...endpoints.map((e) => e.construct),
2220
2259
  ...functions.map((f) => f.construct),
@@ -2284,7 +2323,7 @@ async function workspaceBuildCommand(workspace, options) {
2284
2323
  try {
2285
2324
  const turboCommand = getTurboCommand(pm);
2286
2325
  logger$9.log(`Running: ${turboCommand}`);
2287
- await new Promise((resolve$3, reject) => {
2326
+ await new Promise((resolve$4, reject) => {
2288
2327
  const child = (0, node_child_process.spawn)(turboCommand, {
2289
2328
  shell: true,
2290
2329
  cwd: workspace.root,
@@ -2295,7 +2334,7 @@ async function workspaceBuildCommand(workspace, options) {
2295
2334
  }
2296
2335
  });
2297
2336
  child.on("close", (code) => {
2298
- if (code === 0) resolve$3();
2337
+ if (code === 0) resolve$4();
2299
2338
  else reject(new Error(`Turbo build failed with exit code ${code}`));
2300
2339
  });
2301
2340
  child.on("error", (err) => {
@@ -5438,7 +5477,7 @@ async function prompt(message, hidden = false) {
5438
5477
  if (!process.stdin.isTTY) throw new Error("Interactive input required. Please configure manually.");
5439
5478
  if (hidden) {
5440
5479
  process.stdout.write(message);
5441
- return new Promise((resolve$3) => {
5480
+ return new Promise((resolve$4) => {
5442
5481
  let value = "";
5443
5482
  const onData = (char) => {
5444
5483
  const c = char.toString();
@@ -5447,7 +5486,7 @@ async function prompt(message, hidden = false) {
5447
5486
  process.stdin.pause();
5448
5487
  process.stdin.removeListener("data", onData);
5449
5488
  process.stdout.write("\n");
5450
- resolve$3(value);
5489
+ resolve$4(value);
5451
5490
  } else if (c === "") {
5452
5491
  process.stdin.setRawMode(false);
5453
5492
  process.stdin.pause();
@@ -6465,7 +6504,7 @@ async function deployCommand(options) {
6465
6504
  dokployConfig = setupResult.config;
6466
6505
  finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
6467
6506
  if (setupResult.serviceUrls) {
6468
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-DOEtT2Hr.cjs"));
6507
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
6469
6508
  let secrets = await readStageSecrets$1(stage);
6470
6509
  if (!secrets) {
6471
6510
  logger$3.log(` Creating secrets file for stage "${stage}"...`);
@@ -11875,75 +11914,29 @@ async function testCommand(options = {}) {
11875
11914
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
11876
11915
  const defaultEnv = loadEnvFiles(".env");
11877
11916
  if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
11878
- let secretsEnv = {};
11879
- try {
11880
- const secrets = await require_storage.readStageSecrets(stage);
11881
- if (secrets) {
11882
- secretsEnv = require_storage.toEmbeddableSecrets(secrets);
11883
- console.log(` 🔐 Loaded ${Object.keys(secretsEnv).length} secrets from ${stage}`);
11884
- } else console.log(` No secrets found for ${stage}`);
11885
- } catch (error) {
11886
- if (error instanceof Error && error.message.includes("key not found")) console.log(` Decryption key not found for ${stage}`);
11887
- else throw error;
11888
- }
11889
- let dependencyEnv = {};
11890
- try {
11891
- const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
11892
- const resolvedPorts = await resolveServicePorts(appInfo.workspaceRoot);
11893
- await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, secretsEnv);
11894
- if (resolvedPorts.mappings.length > 0) {
11895
- secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
11896
- console.log(` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
11897
- }
11898
- dependencyEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
11899
- if (Object.keys(dependencyEnv).length > 0) console.log(` 🔗 Loaded ${Object.keys(dependencyEnv).length} dependency URL(s)`);
11900
- const sniffed = await sniffAppEnvironment(appInfo.app, appInfo.appName, appInfo.workspaceRoot, { logWarnings: false });
11917
+ const result = await prepareEntryCredentials({
11918
+ stages: [stage],
11919
+ startDocker: true,
11920
+ secretsFileName: "test-secrets.json",
11921
+ resolveDockerPorts: "full"
11922
+ });
11923
+ let finalCredentials = { ...result.credentials };
11924
+ if (result.appInfo) {
11925
+ const sniffed = await sniffAppEnvironment(result.appInfo.app, result.appInfo.appName, result.appInfo.workspaceRoot, { logWarnings: false });
11901
11926
  if (sniffed.requiredEnvVars.length > 0) {
11902
11927
  const needed = new Set(sniffed.requiredEnvVars);
11903
- const allEnv = {
11904
- ...secretsEnv,
11905
- ...dependencyEnv
11906
- };
11907
- const filteredEnv = {};
11908
- for (const [key, value] of Object.entries(allEnv)) if (needed.has(key)) filteredEnv[key] = value;
11909
- secretsEnv = {};
11910
- dependencyEnv = filteredEnv;
11928
+ const filtered = {};
11929
+ for (const [key, value] of Object.entries(finalCredentials)) if (needed.has(key)) filtered[key] = value;
11930
+ finalCredentials = filtered;
11911
11931
  console.log(` 🔍 Sniffed ${sniffed.requiredEnvVars.length} required env var(s)`);
11912
11932
  }
11913
- } catch {
11914
- const composePath = (0, node_path.join)(cwd, "docker-compose.yml");
11915
- const mappings = parseComposePortMappings(composePath);
11916
- if (mappings.length > 0) {
11917
- const resolvedPorts = await resolveServicePorts(cwd);
11918
- await startComposeServices(cwd, resolvedPorts.dockerEnv, secretsEnv);
11919
- if (resolvedPorts.mappings.length > 0) {
11920
- secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
11921
- console.log(` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
11922
- } else {
11923
- const ports = await loadPortState(cwd);
11924
- if (Object.keys(ports).length > 0) {
11925
- secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
11926
- dockerEnv: {},
11927
- ports,
11928
- mappings
11929
- });
11930
- console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
11931
- }
11932
- }
11933
- }
11934
11933
  }
11935
- secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
11934
+ finalCredentials = rewriteDatabaseUrlForTests(finalCredentials);
11936
11935
  console.log("");
11937
- const allSecrets = {
11938
- ...secretsEnv,
11939
- ...dependencyEnv
11940
- };
11936
+ await (0, node_fs_promises.writeFile)(result.secretsJsonPath, JSON.stringify(finalCredentials, null, 2));
11941
11937
  const gkmDir = (0, node_path.join)(cwd, ".gkm");
11942
- await (0, node_fs_promises.mkdir)(gkmDir, { recursive: true });
11943
- const secretsJsonPath = (0, node_path.join)(gkmDir, "test-secrets.json");
11944
- await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(allSecrets, null, 2));
11945
11938
  const preloadPath = (0, node_path.join)(gkmDir, "test-credentials-preload.ts");
11946
- await createCredentialsPreload(preloadPath, secretsJsonPath);
11939
+ await createCredentialsPreload(preloadPath, result.secretsJsonPath);
11947
11940
  const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
11948
11941
  const tsxImport = "--import=tsx";
11949
11942
  const preloadImport = `--import=${preloadPath}`;
@@ -11963,14 +11956,14 @@ async function testCommand(options = {}) {
11963
11956
  stdio: "inherit",
11964
11957
  env: {
11965
11958
  ...process.env,
11966
- ...allSecrets,
11959
+ ...finalCredentials,
11967
11960
  NODE_ENV: "test",
11968
11961
  NODE_OPTIONS: nodeOptions
11969
11962
  }
11970
11963
  });
11971
- return new Promise((resolve$3, reject) => {
11964
+ return new Promise((resolve$4, reject) => {
11972
11965
  vitestProcess.on("close", (code) => {
11973
- if (code === 0) resolve$3();
11966
+ if (code === 0) resolve$4();
11974
11967
  else reject(new Error(`Tests failed with exit code ${code}`));
11975
11968
  });
11976
11969
  vitestProcess.on("error", (error) => {
@@ -12367,9 +12360,9 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
12367
12360
  const globalOptions = program.opts();
12368
12361
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12369
12362
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
12370
- const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-D1Pa30oV.cjs"));
12363
+ const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-ByaRPBxh.cjs"));
12371
12364
  const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
12372
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DOEtT2Hr.cjs"));
12365
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
12373
12366
  const { workspace } = await loadWorkspaceConfig$1();
12374
12367
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12375
12368
  if (secrets) {
@@ -12392,8 +12385,8 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
12392
12385
  const globalOptions = program.opts();
12393
12386
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12394
12387
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
12395
- const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-D1Pa30oV.cjs"));
12396
- const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DOEtT2Hr.cjs"));
12388
+ const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-ByaRPBxh.cjs"));
12389
+ const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
12397
12390
  const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
12398
12391
  const { workspace } = await loadWorkspaceConfig$1();
12399
12392
  let secrets = await pullSecrets$1(options.stage, workspace);
@@ -12420,7 +12413,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
12420
12413
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12421
12414
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
12422
12415
  const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
12423
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DOEtT2Hr.cjs"));
12416
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
12424
12417
  const { workspace } = await loadWorkspaceConfig$1();
12425
12418
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12426
12419
  if (!secrets) {