@geekmidas/cli 1.10.3 → 1.10.5

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.
@@ -1,4 +1,4 @@
1
- const require_fullstack_secrets = require('./fullstack-secrets-COWz084x.cjs');
1
+ const require_fullstack_secrets = require('./fullstack-secrets-DqxYGrgW.cjs');
2
2
 
3
3
  //#region src/secrets/reconcile.ts
4
4
  /**
@@ -33,4 +33,4 @@ function reconcileMissingSecrets(secrets, workspace) {
33
33
 
34
34
  //#endregion
35
35
  exports.reconcileMissingSecrets = reconcileMissingSecrets;
36
- //# sourceMappingURL=reconcile-7yarEvmK.cjs.map
36
+ //# sourceMappingURL=reconcile-CCtrj-zt.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-7yarEvmK.cjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,yDAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
1
+ {"version":3,"file":"reconcile-CCtrj-zt.cjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,yDAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
@@ -1,4 +1,4 @@
1
- import { generateFullstackCustomSecrets } from "./fullstack-secrets-UZAFWuH4.mjs";
1
+ import { generateFullstackCustomSecrets } from "./fullstack-secrets-odm79Uo1.mjs";
2
2
 
3
3
  //#region src/secrets/reconcile.ts
4
4
  /**
@@ -33,4 +33,4 @@ function reconcileMissingSecrets(secrets, workspace) {
33
33
 
34
34
  //#endregion
35
35
  export { reconcileMissingSecrets };
36
- //# sourceMappingURL=reconcile-D2WCDQue.mjs.map
36
+ //# sourceMappingURL=reconcile-WzC1oAUV.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-D2WCDQue.mjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,+BAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
1
+ {"version":3,"file":"reconcile-WzC1oAUV.mjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,+BAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,11 +56,11 @@
56
56
  "prompts": "~2.4.2",
57
57
  "tsx": "~4.20.3",
58
58
  "yaml": "~2.8.2",
59
- "@geekmidas/envkit": "~1.0.3",
60
59
  "@geekmidas/constructs": "~3.0.2",
61
60
  "@geekmidas/errors": "~1.0.0",
62
61
  "@geekmidas/logger": "~1.0.0",
63
- "@geekmidas/schema": "~1.0.0"
62
+ "@geekmidas/schema": "~1.0.0",
63
+ "@geekmidas/envkit": "~1.0.3"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/lodash.kebabcase": "^4.1.9",
@@ -1257,10 +1257,11 @@ describe('rewriteUrlsWithPorts', () => {
1257
1257
  containerPort: 5672,
1258
1258
  };
1259
1259
 
1260
- it('should rewrite DATABASE_URL with resolved postgres port', () => {
1260
+ it('should rewrite DATABASE_URL with resolved postgres port and hostname', () => {
1261
1261
  const secrets = {
1262
1262
  DATABASE_URL: 'postgresql://app:pass@postgres:5432/mydb',
1263
1263
  POSTGRES_PORT: '5432',
1264
+ POSTGRES_HOST: 'postgres',
1264
1265
  SOME_OTHER: 'value',
1265
1266
  };
1266
1267
  const result = rewriteUrlsWithPorts(secrets, {
@@ -1269,9 +1270,10 @@ describe('rewriteUrlsWithPorts', () => {
1269
1270
  mappings: [pgMapping],
1270
1271
  });
1271
1272
  expect(result.DATABASE_URL).toBe(
1272
- 'postgresql://app:pass@postgres:5433/mydb',
1273
+ 'postgresql://app:pass@localhost:5433/mydb',
1273
1274
  );
1274
1275
  expect(result.POSTGRES_PORT).toBe('5433');
1276
+ expect(result.POSTGRES_HOST).toBe('localhost');
1275
1277
  expect(result.SOME_OTHER).toBe('value');
1276
1278
  });
1277
1279
 
@@ -1293,18 +1295,20 @@ describe('rewriteUrlsWithPorts', () => {
1293
1295
  );
1294
1296
  });
1295
1297
 
1296
- it('should rewrite REDIS_URL and REDIS_PORT', () => {
1298
+ it('should rewrite REDIS_URL, REDIS_PORT, and REDIS_HOST', () => {
1297
1299
  const secrets = {
1298
1300
  REDIS_URL: 'redis://:pass@redis:6379',
1299
1301
  REDIS_PORT: '6379',
1302
+ REDIS_HOST: 'redis',
1300
1303
  };
1301
1304
  const result = rewriteUrlsWithPorts(secrets, {
1302
1305
  dockerEnv: { REDIS_HOST_PORT: '6380' },
1303
1306
  ports: { REDIS_HOST_PORT: 6380 },
1304
1307
  mappings: [redisMapping],
1305
1308
  });
1306
- expect(result.REDIS_URL).toBe('redis://:pass@redis:6380');
1309
+ expect(result.REDIS_URL).toBe('redis://:pass@localhost:6380');
1307
1310
  expect(result.REDIS_PORT).toBe('6380');
1311
+ expect(result.REDIS_HOST).toBe('localhost');
1308
1312
  });
1309
1313
 
1310
1314
  it('should rewrite RABBITMQ_URL and RABBITMQ_PORT', () => {
@@ -1317,7 +1321,7 @@ describe('rewriteUrlsWithPorts', () => {
1317
1321
  ports: { RABBITMQ_HOST_PORT: 5673 },
1318
1322
  mappings: [rmqMapping],
1319
1323
  });
1320
- expect(result.RABBITMQ_URL).toBe('amqp://app:pass@rabbitmq:5673/%2F');
1324
+ expect(result.RABBITMQ_URL).toBe('amqp://app:pass@localhost:5673/%2F');
1321
1325
  expect(result.RABBITMQ_PORT).toBe('5673');
1322
1326
  });
1323
1327
 
@@ -1336,26 +1340,48 @@ describe('rewriteUrlsWithPorts', () => {
1336
1340
  ports: { POSTGRES_HOST_PORT: 5433, REDIS_HOST_PORT: 6380 },
1337
1341
  mappings: [pgMapping, redisMapping],
1338
1342
  });
1339
- expect(result.DATABASE_URL).toContain(':5433/');
1343
+ expect(result.DATABASE_URL).toBe(
1344
+ 'postgresql://app:pass@localhost:5433/mydb',
1345
+ );
1340
1346
  expect(result.POSTGRES_PORT).toBe('5433');
1341
- expect(result.REDIS_URL).toContain(':6380');
1347
+ expect(result.REDIS_URL).toBe('redis://:pass@localhost:6380');
1342
1348
  expect(result.REDIS_PORT).toBe('6380');
1343
1349
  });
1344
1350
 
1345
- it('should not modify secrets when ports are defaults', () => {
1351
+ it('should rewrite hostnames even when ports are defaults', () => {
1346
1352
  const secrets = {
1347
1353
  DATABASE_URL: 'postgresql://app:pass@postgres:5432/mydb',
1348
1354
  POSTGRES_PORT: '5432',
1355
+ POSTGRES_HOST: 'postgres',
1349
1356
  };
1350
1357
  const result = rewriteUrlsWithPorts(secrets, {
1351
1358
  dockerEnv: { POSTGRES_HOST_PORT: '5432' },
1352
1359
  ports: { POSTGRES_HOST_PORT: 5432 },
1353
1360
  mappings: [pgMapping],
1354
1361
  });
1355
- expect(result.DATABASE_URL).toBe(secrets.DATABASE_URL);
1362
+ expect(result.DATABASE_URL).toBe(
1363
+ 'postgresql://app:pass@localhost:5432/mydb',
1364
+ );
1365
+ expect(result.POSTGRES_HOST).toBe('localhost');
1356
1366
  expect(result.POSTGRES_PORT).toBe('5432');
1357
1367
  });
1358
1368
 
1369
+ it('should not rewrite _HOST vars that are already localhost', () => {
1370
+ const secrets = {
1371
+ DATABASE_URL: 'postgresql://app:pass@localhost:5432/mydb',
1372
+ POSTGRES_HOST: 'localhost',
1373
+ };
1374
+ const result = rewriteUrlsWithPorts(secrets, {
1375
+ dockerEnv: { POSTGRES_HOST_PORT: '5432' },
1376
+ ports: { POSTGRES_HOST_PORT: 5432 },
1377
+ mappings: [pgMapping],
1378
+ });
1379
+ expect(result.DATABASE_URL).toBe(
1380
+ 'postgresql://app:pass@localhost:5432/mydb',
1381
+ );
1382
+ expect(result.POSTGRES_HOST).toBe('localhost');
1383
+ });
1384
+
1359
1385
  it('should return empty for no mappings', () => {
1360
1386
  const result = rewriteUrlsWithPorts(
1361
1387
  {},
package/src/dev/index.ts CHANGED
@@ -354,7 +354,10 @@ export function rewriteUrlsWithPorts(
354
354
 
355
355
  // Build a map of defaultPort → resolvedPort for all changed ports
356
356
  const portReplacements: { defaultPort: number; resolvedPort: number }[] = [];
357
+ // Collect Docker service names for hostname rewriting
358
+ const serviceNames = new Set<string>();
357
359
  for (const mapping of mappings) {
360
+ serviceNames.add(mapping.service);
358
361
  const resolved = ports[mapping.envVar];
359
362
  if (resolved !== undefined) {
360
363
  portReplacements.push({
@@ -364,6 +367,14 @@ export function rewriteUrlsWithPorts(
364
367
  }
365
368
  }
366
369
 
370
+ // Rewrite _HOST env vars that use Docker service names
371
+ for (const [key, value] of Object.entries(result)) {
372
+ if (!key.endsWith('_HOST')) continue;
373
+ if (serviceNames.has(value)) {
374
+ result[key] = 'localhost';
375
+ }
376
+ }
377
+
367
378
  // Rewrite _PORT env vars whose values match a default port
368
379
  for (const [key, value] of Object.entries(result)) {
369
380
  if (!key.endsWith('_PORT')) continue;
@@ -374,11 +385,17 @@ export function rewriteUrlsWithPorts(
374
385
  }
375
386
  }
376
387
 
377
- // Rewrite URLs containing default ports
388
+ // Rewrite URLs: replace Docker service hostnames with localhost and fix ports
378
389
  for (const [key, value] of Object.entries(result)) {
379
390
  if (!key.endsWith('_URL') && key !== 'DATABASE_URL') continue;
380
391
 
381
392
  let rewritten = value;
393
+ for (const name of serviceNames) {
394
+ rewritten = rewritten.replace(
395
+ new RegExp(`@${name}:`, 'g'),
396
+ '@localhost:',
397
+ );
398
+ }
382
399
  for (const { defaultPort, resolvedPort } of portReplacements) {
383
400
  rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
384
401
  }
@@ -122,14 +122,14 @@ describe('generateDockerCompose', () => {
122
122
  });
123
123
 
124
124
  describe('postgres service', () => {
125
- it('should add DATABASE_URL environment variable', () => {
125
+ it('should add DATABASE_URL environment variable with credential interpolation', () => {
126
126
  const yaml = generateDockerCompose({
127
127
  ...baseOptions,
128
128
  services: { postgres: true },
129
129
  });
130
130
 
131
131
  expect(yaml).toContain(
132
- '- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}',
132
+ '- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-app}}',
133
133
  );
134
134
  });
135
135
 
@@ -185,13 +185,15 @@ describe('generateDockerCompose', () => {
185
185
  expect(yaml).toContain('postgres_data:');
186
186
  });
187
187
 
188
- it('should include postgres healthcheck', () => {
188
+ it('should include postgres healthcheck using POSTGRES_USER', () => {
189
189
  const yaml = generateDockerCompose({
190
190
  ...baseOptions,
191
191
  services: { postgres: true },
192
192
  });
193
193
 
194
- expect(yaml).toContain('test: ["CMD-SHELL", "pg_isready -U postgres"]');
194
+ expect(yaml).toContain(
195
+ 'test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]',
196
+ );
195
197
  });
196
198
 
197
199
  it('should add depends_on for postgres', () => {
@@ -801,7 +803,7 @@ describe('generateWorkspaceCompose', () => {
801
803
  const yaml = generateWorkspaceCompose(workspace);
802
804
 
803
805
  expect(yaml).toContain(
804
- 'DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}',
806
+ 'DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-app}}',
805
807
  );
806
808
  });
807
809
 
@@ -105,7 +105,7 @@ services:
105
105
 
106
106
  // Add environment variables based on services
107
107
  if (serviceMap.has('postgres')) {
108
- yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
108
+ yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
109
109
  `;
110
110
  }
111
111
 
@@ -156,7 +156,7 @@ services:
156
156
  volumes:
157
157
  - postgres_data:/var/lib/postgresql/data
158
158
  healthcheck:
159
- test: ["CMD-SHELL", "pg_isready -U postgres"]
159
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
160
160
  interval: 5s
161
161
  timeout: 5s
162
162
  retries: 5
@@ -335,7 +335,7 @@ services:
335
335
  volumes:
336
336
  - postgres_data:/var/lib/postgresql/data
337
337
  healthcheck:
338
- test: ["CMD-SHELL", "pg_isready -U postgres"]
338
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
339
339
  interval: 5s
340
340
  timeout: 5s
341
341
  retries: 5
@@ -480,7 +480,7 @@ function generateAppService(
480
480
  // Add infrastructure service URLs for backend apps
481
481
  if (app.type === 'backend') {
482
482
  if (hasPostgres) {
483
- yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
483
+ yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
484
484
  `;
485
485
  }
486
486
  if (hasRedis) {
@@ -46,15 +46,15 @@ export function generateDockerFiles(
46
46
  container_name: ${options.name}-postgres
47
47
  restart: unless-stopped${envFile}
48
48
  environment:
49
- POSTGRES_USER: postgres
50
- POSTGRES_PASSWORD: postgres
51
- POSTGRES_DB: ${options.name.replace(/-/g, '_')}_dev
49
+ POSTGRES_USER: \${POSTGRES_USER:-postgres}
50
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
51
+ POSTGRES_DB: \${POSTGRES_DB:-${options.name.replace(/-/g, '_')}_dev}
52
52
  ports:
53
53
  - '\${POSTGRES_HOST_PORT:-5432}:5432'
54
54
  volumes:
55
55
  - postgres_data:/var/lib/postgresql/data${initVolume}
56
56
  healthcheck:
57
- test: ['CMD-SHELL', 'pg_isready -U postgres']
57
+ test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER']
58
58
  interval: 5s
59
59
  timeout: 5s
60
60
  retries: 5`);
@@ -364,6 +364,7 @@ export default defineWorkspace({
364
364
  path: 'apps/auth',
365
365
  port: 3002,
366
366
  entry: './src/index.ts',
367
+ framework: 'better-auth',
367
368
  envParser: './src/config/env#envParser',
368
369
  logger: './src/config/logger#logger',
369
370
  },
@@ -418,6 +419,12 @@ export default defineWorkspace({
418
419
  },`;
419
420
  }
420
421
 
422
+ // Always enable secrets for workspace dev environment
423
+ config += `
424
+ secrets: {
425
+ enabled: true,
426
+ },`;
427
+
421
428
  config += `
422
429
  });
423
430
  `;
@@ -40,7 +40,7 @@ describe('generateServiceCredentials', () => {
40
40
  it('should generate postgres credentials with defaults', () => {
41
41
  const creds = generateServiceCredentials('postgres');
42
42
 
43
- expect(creds.host).toBe('postgres');
43
+ expect(creds.host).toBe('localhost');
44
44
  expect(creds.port).toBe(5432);
45
45
  expect(creds.username).toBe('app');
46
46
  expect(creds.database).toBe('app');
@@ -50,7 +50,7 @@ describe('generateServiceCredentials', () => {
50
50
  it('should generate redis credentials with defaults', () => {
51
51
  const creds = generateServiceCredentials('redis');
52
52
 
53
- expect(creds.host).toBe('redis');
53
+ expect(creds.host).toBe('localhost');
54
54
  expect(creds.port).toBe(6379);
55
55
  expect(creds.username).toBe('default');
56
56
  expect(creds.password).toHaveLength(32);
@@ -59,7 +59,7 @@ describe('generateServiceCredentials', () => {
59
59
  it('should generate rabbitmq credentials with defaults', () => {
60
60
  const creds = generateServiceCredentials('rabbitmq');
61
61
 
62
- expect(creds.host).toBe('rabbitmq');
62
+ expect(creds.host).toBe('localhost');
63
63
  expect(creds.port).toBe(5672);
64
64
  expect(creds.username).toBe('app');
65
65
  expect(creds.vhost).toBe('/');
@@ -12,24 +12,24 @@ export function generateSecurePassword(length = 32): string {
12
12
  .slice(0, length);
13
13
  }
14
14
 
15
- /** Default service configurations */
15
+ /** Default service configurations (localhost for local dev via Docker port mapping) */
16
16
  const SERVICE_DEFAULTS: Record<
17
17
  ComposeServiceName,
18
18
  Omit<ServiceCredentials, 'password'>
19
19
  > = {
20
20
  postgres: {
21
- host: 'postgres',
21
+ host: 'localhost',
22
22
  port: 5432,
23
23
  username: 'app',
24
24
  database: 'app',
25
25
  },
26
26
  redis: {
27
- host: 'redis',
27
+ host: 'localhost',
28
28
  port: 6379,
29
29
  username: 'default',
30
30
  },
31
31
  rabbitmq: {
32
- host: 'rabbitmq',
32
+ host: 'localhost',
33
33
  port: 5672,
34
34
  username: 'app',
35
35
  vhost: '/',
@@ -1,4 +1,10 @@
1
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs';
2
8
  import { tmpdir } from 'node:os';
3
9
  import { join } from 'node:path';
4
10
  import {
@@ -12,12 +18,13 @@ import {
12
18
  vi,
13
19
  } from 'vitest';
14
20
  import {
21
+ createCredentialsPreload,
15
22
  loadPortState,
16
23
  parseComposePortMappings,
17
24
  rewriteUrlsWithPorts,
18
25
  savePortState,
19
26
  } from '../../dev/index';
20
- import { ensureTestDatabase, rewriteDatabaseUrlForTests } from '../index';
27
+ import { rewriteDatabaseUrlForTests } from '../index';
21
28
 
22
29
  describe('rewriteDatabaseUrlForTests', () => {
23
30
  beforeAll(() => {
@@ -121,39 +128,6 @@ describe('rewriteDatabaseUrlForTests', () => {
121
128
  });
122
129
  });
123
130
 
124
- describe('ensureTestDatabase', () => {
125
- beforeAll(() => {
126
- vi.spyOn(console, 'log').mockImplementation(() => {});
127
- });
128
-
129
- afterAll(() => {
130
- vi.restoreAllMocks();
131
- });
132
-
133
- it('should do nothing when DATABASE_URL is missing', async () => {
134
- // Should resolve without error
135
- await ensureTestDatabase({});
136
- await ensureTestDatabase({ REDIS_URL: 'redis://localhost:6379' });
137
- });
138
-
139
- it('should do nothing when database name is empty', async () => {
140
- await ensureTestDatabase({
141
- DATABASE_URL: 'postgresql://app:secret@localhost:5432/',
142
- });
143
- });
144
-
145
- it('should not throw when postgres is unreachable', async () => {
146
- // Use a port that's almost certainly not running postgres
147
- await ensureTestDatabase({
148
- DATABASE_URL: 'postgresql://app:secret@localhost:59999/test_db',
149
- });
150
- // Should log a warning but not throw
151
- expect(console.log).toHaveBeenCalledWith(
152
- expect.stringContaining('Could not ensure test database'),
153
- );
154
- });
155
- });
156
-
157
131
  describe('port rewriting + test database pipeline', () => {
158
132
  let testDir: string;
159
133
 
@@ -274,6 +248,38 @@ services:
274
248
  );
275
249
  });
276
250
 
251
+ it('should write test-secrets.json and credentials preload', async () => {
252
+ const gkmDir = join(testDir, '.gkm');
253
+ mkdirSync(gkmDir, { recursive: true });
254
+
255
+ const secrets = {
256
+ DATABASE_URL: 'postgresql://app:secret@localhost:5433/myapp_test',
257
+ JWT_SECRET: 'test-jwt-secret',
258
+ };
259
+
260
+ const secretsJsonPath = join(gkmDir, 'test-secrets.json');
261
+ writeFileSync(secretsJsonPath, JSON.stringify(secrets, null, 2));
262
+
263
+ const preloadPath = join(gkmDir, 'test-credentials-preload.ts');
264
+ await createCredentialsPreload(preloadPath, secretsJsonPath);
265
+
266
+ // Verify secrets JSON was written
267
+ expect(existsSync(secretsJsonPath)).toBe(true);
268
+ const written = JSON.parse(readFileSync(secretsJsonPath, 'utf-8'));
269
+ expect(written.DATABASE_URL).toBe(secrets.DATABASE_URL);
270
+ expect(written.JWT_SECRET).toBe(secrets.JWT_SECRET);
271
+
272
+ // Verify preload script was created with correct content
273
+ expect(existsSync(preloadPath)).toBe(true);
274
+ const preloadContent = readFileSync(preloadPath, 'utf-8');
275
+ expect(preloadContent).toContain(
276
+ "import { Credentials } from '@geekmidas/envkit/credentials'",
277
+ );
278
+ expect(preloadContent).toContain('Object.assign(Credentials');
279
+ expect(preloadContent).toContain('Object.assign(process.env');
280
+ expect(preloadContent).toContain(secretsJsonPath);
281
+ });
282
+
277
283
  it('should handle worker template with rabbitmq ports', async () => {
278
284
  writeFileSync(
279
285
  join(testDir, 'docker-compose.yml'),
@@ -320,4 +326,65 @@ services:
320
326
  // RabbitMQ port rewritten
321
327
  expect(secrets.RABBITMQ_URL).toBe('amqp://app:secret@localhost:5673');
322
328
  });
329
+
330
+ it('should preserve root postgres credentials through the pipeline', async () => {
331
+ writeFileSync(
332
+ join(testDir, 'docker-compose.yml'),
333
+ `
334
+ services:
335
+ postgres:
336
+ image: postgres:17
337
+ ports:
338
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
339
+ `,
340
+ );
341
+
342
+ await savePortState(testDir, {
343
+ POSTGRES_HOST_PORT: 5434,
344
+ });
345
+
346
+ // Simulate toEmbeddableSecrets output for a workspace with app-specific users
347
+ let secrets: Record<string, string> = {
348
+ DATABASE_URL: 'postgresql://app:rootpass@postgres:5432/app',
349
+ API_DATABASE_URL: 'postgresql://api:apipass@localhost:5432/myproject_dev',
350
+ AUTH_DATABASE_URL:
351
+ 'postgresql://auth:authpass@localhost:5432/myproject_dev',
352
+ POSTGRES_USER: 'app',
353
+ POSTGRES_PASSWORD: 'rootpass',
354
+ POSTGRES_DB: 'app',
355
+ POSTGRES_HOST: 'postgres',
356
+ POSTGRES_PORT: '5432',
357
+ };
358
+
359
+ // Apply port + host rewriting
360
+ const mappings = parseComposePortMappings(
361
+ join(testDir, 'docker-compose.yml'),
362
+ );
363
+ const ports = await loadPortState(testDir);
364
+ secrets = rewriteUrlsWithPorts(secrets, {
365
+ dockerEnv: {},
366
+ ports,
367
+ mappings,
368
+ });
369
+
370
+ // Apply test database suffix
371
+ secrets = rewriteDatabaseUrlForTests(secrets);
372
+
373
+ // Root credentials should be present and rewritten to localhost
374
+ expect(secrets.POSTGRES_USER).toBe('app');
375
+ expect(secrets.POSTGRES_PASSWORD).toBe('rootpass');
376
+ expect(secrets.POSTGRES_HOST).toBe('localhost');
377
+ expect(secrets.POSTGRES_PORT).toBe('5434');
378
+
379
+ // All DATABASE_URLs should have localhost, resolved port, and _test suffix
380
+ expect(secrets.DATABASE_URL).toBe(
381
+ 'postgresql://app:rootpass@localhost:5434/app_test',
382
+ );
383
+ expect(secrets.API_DATABASE_URL).toBe(
384
+ 'postgresql://api:apipass@localhost:5434/myproject_dev_test',
385
+ );
386
+ expect(secrets.AUTH_DATABASE_URL).toBe(
387
+ 'postgresql://auth:authpass@localhost:5434/myproject_dev_test',
388
+ );
389
+ });
323
390
  });