@geekmidas/cli 1.10.17 → 1.10.19

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 +12 -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 +701 -707
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +689 -695
  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 +2 -2
  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 +48 -830
  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.mjs CHANGED
@@ -3,14 +3,14 @@ import { __require } from "./chunk-Duj1WY3L.mjs";
3
3
  import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-D4z4A4cq.mjs";
4
4
  import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-jsRYHOHU.mjs";
5
5
  import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-s1kLcIzK.mjs";
6
- import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, copyAllClients, copyClientToFrontends, generateOpenApi, getBackendOpenApiPath, isPartitionedRoutes, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-DenF-okj.mjs";
7
- import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-dbb9RyBl.mjs";
6
+ import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-CpMNB77O.mjs";
7
+ import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, copyAllClients, copyClientToFrontends, generateOpenApi, getBackendOpenApiPath, isPartitionedRoutes, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-kvwpKbNe.mjs";
8
8
  import { DokployApi } from "./dokploy-api-2ldYoN3i.mjs";
9
9
  import { encryptSecrets } from "./encryption-BOH5M-f-.mjs";
10
10
  import { CachedStateProvider } from "./CachedStateProvider-BDq5WqSy.mjs";
11
11
  import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateLocalStackCredentials, generateSecurePassword, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-x2Kffx7-.mjs";
12
12
  import { generateReactQueryCommand } from "./openapi-react-query-C4UdILaI.mjs";
13
- import { isSSMConfigured, pullSecrets, pushSecrets } from "./sync-D_NowTkZ.mjs";
13
+ import { isSSMConfigured, pullSecrets, pushSecrets } from "./sync-lExOTa9t.mjs";
14
14
  import { createRequire } from "node:module";
15
15
  import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
16
16
  import { basename, dirname, join, parse, relative, resolve } from "node:path";
@@ -19,15 +19,15 @@ import { stdin, stdout } from "node:process";
19
19
  import * as readline from "node:readline/promises";
20
20
  import { mkdir, readFile, writeFile } from "node:fs/promises";
21
21
  import { execSync, spawn } from "node:child_process";
22
- import { createServer } from "node:net";
23
22
  import chokidar from "chokidar";
24
- import { config } from "dotenv";
25
23
  import fg from "fast-glob";
24
+ import { createServer } from "node:net";
25
+ import { config } from "dotenv";
26
26
  import { parse as parse$1 } from "yaml";
27
+ import { randomBytes } from "node:crypto";
27
28
  import { Cron } from "@geekmidas/constructs/crons";
28
29
  import { Function } from "@geekmidas/constructs/functions";
29
30
  import { Subscriber } from "@geekmidas/constructs/subscribers";
30
- import { randomBytes } from "node:crypto";
31
31
  import { Client } from "pg";
32
32
  import { lookup } from "node:dns/promises";
33
33
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -35,7 +35,7 @@ import prompts from "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.18";
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
  */
@@ -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 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: ${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: ${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 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 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 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: ${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: ${getCredentialsPath()}`);
265
265
  }
266
266
  /**
267
267
  * Mask a token for display
@@ -343,135 +343,590 @@ function isEnabled(config$1) {
343
343
  }
344
344
 
345
345
  //#endregion
346
- //#region src/generators/CronGenerator.ts
347
- var CronGenerator = class extends 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 = join(outputDir, "crons");
354
- await 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: 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 = resolve(cwd, envFile);
358
+ if (existsSync(envPath)) {
359
+ 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 Cron.isCron(value);
371
- }
372
- async generateCronHandler(outputDir, sourceFile, exportName, context) {
373
- const handlerFileName = `${exportName}.ts`;
374
- const handlerPath = join(outputDir, handlerFileName);
375
- const relativePath = relative(dirname(handlerPath), sourceFile);
376
- const importPath = relativePath.replace(/\.ts$/, ".js");
377
- const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
378
- const relativeLoggerPath = relative(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 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 ConstructGenerator {
396
- isConstruct(value) {
397
- return 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$1) => {
378
+ const server = createServer();
379
+ server.once("error", (err) => {
380
+ if (err.code === "EADDRINUSE") resolve$1(false);
381
+ else resolve$1(false);
382
+ });
383
+ server.once("listening", () => {
384
+ server.close();
385
+ resolve$1(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 = join(outputDir, "functions");
405
- await 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: 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 (!existsSync(composePath)) return [];
411
+ const content = readFileSync(composePath, "utf-8");
412
+ const compose = parse$1(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 = join(outputDir, handlerFileName);
422
- const relativePath = relative(dirname(handlerPath), sourceFile);
423
- const importPath = relativePath.replace(/\.ts$/, ".js");
424
- const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
425
- const relativeLoggerPath = relative(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 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 readFile(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 ConstructGenerator {
443
- isConstruct(value) {
444
- return 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 = join(workspaceRoot, ".gkm");
444
+ await mkdir(dir, { recursive: true });
445
+ await writeFile(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 = 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;
454
- }
455
- if (constructs.length === 0) return subscriberInfos;
456
- if (provider !== "aws-lambda") return subscriberInfos;
457
- const subscribersDir = join(outputDir, "subscribers");
458
- await 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: 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}`);
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 = 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;
470
491
  }
471
- return subscriberInfos;
472
- }
473
- async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
474
- const handlerFileName = `${exportName}.ts`;
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;
499
+ }
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 (!existsSync(composePath)) return [];
581
+ const content = readFileSync(composePath, "utf-8");
582
+ const compose = parse$1(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 = join(cwd, "docker-compose.yml");
591
+ if (!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
+ 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 = join(workspace.root, "docker-compose.yml");
615
+ if (!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
+ 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 (secretsExist(stage, secretsRoot)) {
641
+ const stageSecrets = await readStageSecrets(stage, secretsRoot);
642
+ if (stageSecrets) {
643
+ logger$13.log(`🔐 Loading secrets from stage: ${stage}`);
644
+ secrets = 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 (existsSync(join(dir, ".gkm", "secrets"))) return dir;
664
+ const parent = 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 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 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 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 = 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 = 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 = getDependencyEnvVars(appInfo.workspace, appInfo.appName);
783
+ Object.assign(credentials, depEnv);
784
+ }
785
+ const secretsDir = join(secretsRoot, ".gkm");
786
+ await mkdir(secretsDir, { recursive: true });
787
+ const secretsFileName = options.secretsFileName ?? (appName ? `dev-secrets-${appName}.json` : "dev-secrets.json");
788
+ const secretsJsonPath = join(secretsDir, secretsFileName);
789
+ await 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 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 = join(outputDir, "crons");
809
+ await 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: 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 Cron.isCron(value);
826
+ }
827
+ async generateCronHandler(outputDir, sourceFile, exportName, context) {
828
+ const handlerFileName = `${exportName}.ts`;
829
+ const handlerPath = join(outputDir, handlerFileName);
830
+ const relativePath = relative(dirname(handlerPath), sourceFile);
831
+ const importPath = relativePath.replace(/\.ts$/, ".js");
832
+ const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
833
+ const relativeLoggerPath = relative(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 writeFile(handlerPath, content);
844
+ return handlerPath;
845
+ }
846
+ };
847
+
848
+ //#endregion
849
+ //#region src/generators/FunctionGenerator.ts
850
+ var FunctionGenerator = class extends ConstructGenerator {
851
+ isConstruct(value) {
852
+ return 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 = join(outputDir, "functions");
860
+ await 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: 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 = join(outputDir, handlerFileName);
877
+ const relativePath = relative(dirname(handlerPath), sourceFile);
878
+ const importPath = relativePath.replace(/\.ts$/, ".js");
879
+ const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
880
+ const relativeLoggerPath = relative(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 writeFile(handlerPath, content);
891
+ return handlerPath;
892
+ }
893
+ };
894
+
895
+ //#endregion
896
+ //#region src/generators/SubscriberGenerator.ts
897
+ var SubscriberGenerator = class extends ConstructGenerator {
898
+ isConstruct(value) {
899
+ return 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 = join(outputDir, "subscribers");
913
+ await 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: 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) {
929
+ const handlerFileName = `${exportName}.ts`;
475
930
  const handlerPath = join(outputDir, handlerFileName);
476
931
  const relativePath = relative(dirname(handlerPath), sourceFile);
477
932
  const importPath = relativePath.replace(/\.ts$/, ".js");
@@ -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 = resolve(cwd, envFile);
647
- if (existsSync(envPath)) {
648
- 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$1) => {
667
- const server = createServer();
668
- server.once("error", (err) => {
669
- if (err.code === "EADDRINUSE") resolve$1(false);
670
- else resolve$1(false);
671
- });
672
- server.once("listening", () => {
673
- server.close();
674
- resolve$1(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 = join(cwd, ".gkm");
1116
+ await mkdir(preloadDir, { recursive: true });
1117
+ const preloadPath = 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 = 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$1) => {
1141
+ child.on("close", (code) => resolve$1(code ?? 0));
1142
+ child.on("error", (error) => {
1143
+ logger$12.error(`Failed to run command: ${error.message}`);
1144
+ resolve$1(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 (!existsSync(composePath)) return [];
700
- const content = readFileSync(composePath, "utf-8");
701
- const compose = parse$1(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 readFile(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 = join(workspaceRoot, ".gkm");
733
- await mkdir(dir, { recursive: true });
734
- await writeFile(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 = 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 = 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
@@ -1025,7 +1327,8 @@ async function devCommand(options) {
1025
1327
  if (Object.keys(appSecrets).length > 0) {
1026
1328
  const secretsDir = join(secretsRoot, ".gkm");
1027
1329
  await mkdir(secretsDir, { recursive: true });
1028
- secretsJsonPath = join(secretsDir, "dev-secrets.json");
1330
+ const secretsFileName = workspaceAppName ? `dev-secrets-${workspaceAppName}.json` : "dev-secrets.json";
1331
+ secretsJsonPath = join(secretsDir, secretsFileName);
1029
1332
  await writeFile(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
1030
1333
  logger$11.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
1031
1334
  }
@@ -1204,103 +1507,6 @@ async function loadDevSecrets(workspace) {
1204
1507
  return {};
1205
1508
  }
1206
1509
  /**
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 (secretsExist(stage, secretsRoot)) {
1216
- const stageSecrets = await readStageSecrets(stage, secretsRoot);
1217
- if (stageSecrets) {
1218
- logger$11.log(`🔐 Loading secrets from stage: ${stage}`);
1219
- secrets = 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 (!existsSync(composePath)) return [];
1250
- const content = readFileSync(composePath, "utf-8");
1251
- const compose = parse$1(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 = join(cwd, "docker-compose.yml");
1267
- if (!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
- 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 = join(workspace.root, "docker-compose.yml");
1285
- if (!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
- 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
1510
  * Workspace dev command - orchestrates multi-app development using Turbo.
1305
1511
  *
1306
1512
  * Flow:
@@ -1503,105 +1709,6 @@ async function buildServer(config$1, context, provider, enableOpenApi, appRoot =
1503
1709
  ]);
1504
1710
  }
1505
1711
  /**
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 (existsSync(join(dir, ".gkm", "secrets"))) return dir;
1514
- const parent = 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 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 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 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 = 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 = join(secretsRoot, ".gkm");
1592
- await mkdir(secretsDir, { recursive: true });
1593
- const secretsFileName = appName ? `dev-secrets-${appName}.json` : "dev-secrets.json";
1594
- const secretsJsonPath = join(secretsDir, secretsFileName);
1595
- await writeFile(secretsJsonPath, JSON.stringify(credentials, null, 2));
1596
- return {
1597
- credentials,
1598
- resolvedPort,
1599
- secretsJsonPath,
1600
- appName,
1601
- secretsRoot
1602
- };
1603
- }
1604
- /**
1605
1712
  * Run any TypeScript file with secret injection.
1606
1713
  * Does not require gkm.config.ts.
1607
1714
  */
@@ -1876,73 +1983,6 @@ var DevServer = class {
1876
1983
  await fsWriteFile(serverPath, content);
1877
1984
  }
1878
1985
  };
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 loadWorkspaceAppInfo(cwd);
1907
- if (appInfo.appName) {
1908
- const depEnv = getDependencyEnvVars(appInfo.workspace, appInfo.appName);
1909
- Object.assign(credentials, depEnv);
1910
- }
1911
- } catch {}
1912
- const preloadDir = join(cwd, ".gkm");
1913
- await mkdir(preloadDir, { recursive: true });
1914
- const preloadPath = 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 = 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$1) => {
1938
- child.on("close", (code) => resolve$1(code ?? 0));
1939
- child.on("error", (error) => {
1940
- logger$11.error(`Failed to run command: ${error.message}`);
1941
- resolve$1(1);
1942
- });
1943
- });
1944
- if (exitCode !== 0) process.exit(exitCode);
1945
- }
1946
1986
 
1947
1987
  //#endregion
1948
1988
  //#region src/build/manifests.ts
@@ -2214,7 +2254,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2214
2254
  let masterKey;
2215
2255
  if (context.production?.bundle && !skipBundle) {
2216
2256
  logger$9.log(`\n📦 Bundling production server...`);
2217
- const { bundleServer } = await import("./bundler-B4AackW5.mjs");
2257
+ const { bundleServer } = await import("./bundler-C5xkxnyr.mjs");
2218
2258
  const allConstructs = [
2219
2259
  ...endpoints.map((e) => e.construct),
2220
2260
  ...functions.map((f) => f.construct),
@@ -6465,7 +6505,7 @@ async function deployCommand(options) {
6465
6505
  dokployConfig = setupResult.config;
6466
6506
  finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
6467
6507
  if (setupResult.serviceUrls) {
6468
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-Cs4WBsc4.mjs");
6508
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-mwbL7PhP.mjs");
6469
6509
  let secrets = await readStageSecrets$1(stage);
6470
6510
  if (!secrets) {
6471
6511
  logger$3.log(` Creating secrets file for stage "${stage}"...`);
@@ -11875,75 +11915,29 @@ async function testCommand(options = {}) {
11875
11915
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
11876
11916
  const defaultEnv = loadEnvFiles(".env");
11877
11917
  if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
11878
- let secretsEnv = {};
11879
- try {
11880
- const secrets = await readStageSecrets(stage);
11881
- if (secrets) {
11882
- secretsEnv = 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 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 = 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 });
11918
+ const result = await prepareEntryCredentials({
11919
+ stages: [stage],
11920
+ startDocker: true,
11921
+ secretsFileName: "test-secrets.json",
11922
+ resolveDockerPorts: "full"
11923
+ });
11924
+ let finalCredentials = { ...result.credentials };
11925
+ if (result.appInfo) {
11926
+ const sniffed = await sniffAppEnvironment(result.appInfo.app, result.appInfo.appName, result.appInfo.workspaceRoot, { logWarnings: false });
11901
11927
  if (sniffed.requiredEnvVars.length > 0) {
11902
11928
  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;
11929
+ const filtered = {};
11930
+ for (const [key, value] of Object.entries(finalCredentials)) if (needed.has(key)) filtered[key] = value;
11931
+ finalCredentials = filtered;
11911
11932
  console.log(` 🔍 Sniffed ${sniffed.requiredEnvVars.length} required env var(s)`);
11912
11933
  }
11913
- } catch {
11914
- const composePath = 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
11934
  }
11935
- secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
11935
+ finalCredentials = rewriteDatabaseUrlForTests(finalCredentials);
11936
11936
  console.log("");
11937
- const allSecrets = {
11938
- ...secretsEnv,
11939
- ...dependencyEnv
11940
- };
11937
+ await writeFile(result.secretsJsonPath, JSON.stringify(finalCredentials, null, 2));
11941
11938
  const gkmDir = join(cwd, ".gkm");
11942
- await mkdir(gkmDir, { recursive: true });
11943
- const secretsJsonPath = join(gkmDir, "test-secrets.json");
11944
- await writeFile(secretsJsonPath, JSON.stringify(allSecrets, null, 2));
11945
11939
  const preloadPath = join(gkmDir, "test-credentials-preload.ts");
11946
- await createCredentialsPreload(preloadPath, secretsJsonPath);
11940
+ await createCredentialsPreload(preloadPath, result.secretsJsonPath);
11947
11941
  const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
11948
11942
  const tsxImport = "--import=tsx";
11949
11943
  const preloadImport = `--import=${preloadPath}`;
@@ -11963,7 +11957,7 @@ async function testCommand(options = {}) {
11963
11957
  stdio: "inherit",
11964
11958
  env: {
11965
11959
  ...process.env,
11966
- ...allSecrets,
11960
+ ...finalCredentials,
11967
11961
  NODE_ENV: "test",
11968
11962
  NODE_OPTIONS: nodeOptions
11969
11963
  }
@@ -12367,9 +12361,9 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
12367
12361
  const globalOptions = program.opts();
12368
12362
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12369
12363
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
12370
- const { pushSecrets: pushSecrets$1 } = await import("./sync-COnAugP-.mjs");
12364
+ const { pushSecrets: pushSecrets$1 } = await import("./sync-CYBVB64f.mjs");
12371
12365
  const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12372
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12366
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-mwbL7PhP.mjs");
12373
12367
  const { workspace } = await loadWorkspaceConfig$1();
12374
12368
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12375
12369
  if (secrets) {
@@ -12392,8 +12386,8 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
12392
12386
  const globalOptions = program.opts();
12393
12387
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12394
12388
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
12395
- const { pullSecrets: pullSecrets$1 } = await import("./sync-COnAugP-.mjs");
12396
- const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12389
+ const { pullSecrets: pullSecrets$1 } = await import("./sync-CYBVB64f.mjs");
12390
+ const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-mwbL7PhP.mjs");
12397
12391
  const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12398
12392
  const { workspace } = await loadWorkspaceConfig$1();
12399
12393
  let secrets = await pullSecrets$1(options.stage, workspace);
@@ -12420,7 +12414,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
12420
12414
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12421
12415
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
12422
12416
  const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12423
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12417
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-mwbL7PhP.mjs");
12424
12418
  const { workspace } = await loadWorkspaceConfig$1();
12425
12419
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12426
12420
  if (!secrets) {