@geekmidas/cli 1.10.4 → 1.10.6

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/config.d.cts +1 -1
  3. package/dist/config.d.mts +1 -1
  4. package/dist/{fullstack-secrets-DqxYGrgW.cjs → fullstack-secrets-BctGaE4E.cjs} +7 -2
  5. package/dist/fullstack-secrets-BctGaE4E.cjs.map +1 -0
  6. package/dist/{fullstack-secrets-odm79Uo1.mjs → fullstack-secrets-ca0Kyrvt.mjs} +7 -2
  7. package/dist/fullstack-secrets-ca0Kyrvt.mjs.map +1 -0
  8. package/dist/{index-3n-giNaw.d.mts → index-9tjTQjFt.d.mts} +3 -3
  9. package/dist/{index-3n-giNaw.d.mts.map → index-9tjTQjFt.d.mts.map} +1 -1
  10. package/dist/{index-CiEOtKEX.d.cts → index-VOKKO-lm.d.cts} +3 -3
  11. package/dist/{index-CiEOtKEX.d.cts.map → index-VOKKO-lm.d.cts.map} +1 -1
  12. package/dist/index.cjs +37 -56
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.mjs +37 -56
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/{reconcile-WzC1oAUV.mjs → reconcile-C5OyCA7V.mjs} +2 -2
  17. package/dist/{reconcile-WzC1oAUV.mjs.map → reconcile-C5OyCA7V.mjs.map} +1 -1
  18. package/dist/{reconcile-CCtrj-zt.cjs → reconcile-TEBsryVn.cjs} +2 -2
  19. package/dist/{reconcile-CCtrj-zt.cjs.map → reconcile-TEBsryVn.cjs.map} +1 -1
  20. package/dist/workspace/index.d.cts +1 -1
  21. package/dist/workspace/index.d.mts +1 -1
  22. package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
  23. package/dist/workspace-D4z4A4cq.mjs.map +1 -1
  24. package/package.json +4 -4
  25. package/src/dev/__tests__/index.spec.ts +35 -9
  26. package/src/dev/index.ts +18 -1
  27. package/src/docker/__tests__/compose.spec.ts +13 -11
  28. package/src/docker/compose.ts +10 -10
  29. package/src/init/__tests__/init.spec.ts +1 -1
  30. package/src/init/generators/docker.ts +7 -7
  31. package/src/init/index.ts +3 -1
  32. package/src/secrets/generator.ts +11 -0
  33. package/src/secrets/index.ts +12 -6
  34. package/src/setup/index.ts +3 -1
  35. package/src/test/__tests__/index.spec.ts +62 -34
  36. package/src/test/index.ts +0 -41
  37. package/src/workspace/types.ts +2 -2
  38. package/dist/fullstack-secrets-DqxYGrgW.cjs.map +0 -1
  39. package/dist/fullstack-secrets-odm79Uo1.mjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.4",
3
+ "version": "1.10.6",
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/constructs": "~3.0.2",
60
59
  "@geekmidas/envkit": "~1.0.3",
61
- "@geekmidas/schema": "~1.0.0",
60
+ "@geekmidas/constructs": "~3.0.2",
62
61
  "@geekmidas/logger": "~1.0.0",
63
- "@geekmidas/errors": "~1.0.0"
62
+ "@geekmidas/errors": "~1.0.0",
63
+ "@geekmidas/schema": "~1.0.0"
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
 
@@ -181,17 +181,19 @@ describe('generateDockerCompose', () => {
181
181
  services: { postgres: true },
182
182
  });
183
183
 
184
- expect(yaml).toContain('- postgres_data:/var/lib/postgresql/data');
185
- expect(yaml).toContain('postgres_data:');
184
+ expect(yaml).toContain('- dbdata:/var/lib/postgresql/18/data');
185
+ expect(yaml).toContain('dbdata:');
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', () => {
@@ -371,7 +373,7 @@ describe('generateDockerCompose', () => {
371
373
  services: { postgres: true, redis: true, rabbitmq: true },
372
374
  });
373
375
 
374
- expect(yaml).toContain('postgres_data:');
376
+ expect(yaml).toContain('dbdata:');
375
377
  expect(yaml).toContain('redis_data:');
376
378
  expect(yaml).toContain('rabbitmq_data:');
377
379
  });
@@ -790,7 +792,7 @@ describe('generateWorkspaceCompose', () => {
790
792
  const yaml = generateWorkspaceCompose(workspace);
791
793
 
792
794
  expect(yaml).toContain('postgres:');
793
- expect(yaml).toContain('image: postgres:16-alpine');
795
+ expect(yaml).toContain('image: postgres:18-alpine');
794
796
  expect(yaml).toContain('container_name: test-workspace-postgres');
795
797
  });
796
798
 
@@ -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
 
@@ -843,8 +845,8 @@ describe('generateWorkspaceCompose', () => {
843
845
  });
844
846
  const yaml = generateWorkspaceCompose(workspace);
845
847
 
846
- expect(yaml).toContain('postgres_data:');
847
- expect(yaml).toContain('postgres_data:/var/lib/postgresql/data');
848
+ expect(yaml).toContain('dbdata:');
849
+ expect(yaml).toContain('dbdata:/var/lib/postgresql/18/data');
848
850
  });
849
851
 
850
852
  it('should add redis_data volume when redis is enabled', () => {
@@ -17,7 +17,7 @@ export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
17
17
 
18
18
  /** Default Docker image versions for services */
19
19
  export const DEFAULT_SERVICE_VERSIONS: Record<ComposeServiceName, string> = {
20
- postgres: '16-alpine',
20
+ postgres: '18-alpine',
21
21
  redis: '7-alpine',
22
22
  rabbitmq: '3-management-alpine',
23
23
  };
@@ -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
 
@@ -154,9 +154,9 @@ services:
154
154
  POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
155
155
  POSTGRES_DB: \${POSTGRES_DB:-app}
156
156
  volumes:
157
- - postgres_data:/var/lib/postgresql/data
157
+ - dbdata:/var/lib/postgresql/18/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
@@ -214,7 +214,7 @@ volumes:
214
214
  `;
215
215
 
216
216
  if (serviceMap.has('postgres')) {
217
- yaml += ` postgres_data:
217
+ yaml += ` dbdata:
218
218
  `;
219
219
  }
220
220
 
@@ -333,9 +333,9 @@ services:
333
333
  POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
334
334
  POSTGRES_DB: \${POSTGRES_DB:-app}
335
335
  volumes:
336
- - postgres_data:/var/lib/postgresql/data
336
+ - dbdata:/var/lib/postgresql/18/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
@@ -382,7 +382,7 @@ volumes:
382
382
  `;
383
383
 
384
384
  if (hasPostgres) {
385
- yaml += ` postgres_data:
385
+ yaml += ` dbdata:
386
386
  `;
387
387
  }
388
388
 
@@ -409,7 +409,7 @@ function getInfraServiceImage(
409
409
  config: boolean | { version?: string; image?: string } | undefined,
410
410
  ): string {
411
411
  const defaults: Record<'postgres' | 'redis', string> = {
412
- postgres: 'postgres:16-alpine',
412
+ postgres: 'postgres:18-alpine',
413
413
  redis: 'redis:7-alpine',
414
414
  };
415
415
 
@@ -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) {
@@ -545,7 +545,7 @@ describe('initCommand', () => {
545
545
 
546
546
  const dockerPath = join(tempDir, 'my-api', 'docker-compose.yml');
547
547
  const content = await readFile(dockerPath, 'utf-8');
548
- expect(content).toContain('postgres:16-alpine');
548
+ expect(content).toContain('postgres:18-alpine');
549
549
  expect(content).toContain("'${POSTGRES_HOST_PORT:-5432}:5432'");
550
550
  });
551
551
 
@@ -42,23 +42,23 @@ export function generateDockerFiles(
42
42
  : '';
43
43
 
44
44
  services.push(` postgres:
45
- image: postgres:16-alpine
45
+ image: postgres:18-alpine
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
- - postgres_data:/var/lib/postgresql/data${initVolume}
55
+ - dbdata:/var/lib/postgresql/18/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`);
61
- volumes.push(' postgres_data:');
61
+ volumes.push(' dbdata:');
62
62
 
63
63
  // Generate PostgreSQL init script and .env for fullstack template
64
64
  if (isFullstack && dbApps?.length) {
package/src/init/index.ts CHANGED
@@ -331,7 +331,9 @@ export async function initCommand(
331
331
  if (services.db) secretServices.push('postgres');
332
332
  if (services.cache) secretServices.push('redis');
333
333
 
334
- const devSecrets = createStageSecrets('development', secretServices);
334
+ const devSecrets = createStageSecrets('development', secretServices, {
335
+ projectName: name,
336
+ });
335
337
 
336
338
  // Add common custom secrets
337
339
  const customSecrets: Record<string, string> = {
@@ -114,13 +114,24 @@ export function generateConnectionUrls(
114
114
 
115
115
  /**
116
116
  * Create a new StageSecrets object with generated credentials.
117
+ * @param stage - The deployment stage (e.g., 'development', 'production')
118
+ * @param services - List of services to generate credentials for
119
+ * @param options - Optional configuration
120
+ * @param options.projectName - Project name used to derive the database name (e.g., 'myapp' → 'myapp_dev')
117
121
  */
118
122
  export function createStageSecrets(
119
123
  stage: string,
120
124
  services: ComposeServiceName[],
125
+ options?: { projectName?: string },
121
126
  ): StageSecrets {
122
127
  const now = new Date().toISOString();
123
128
  const serviceCredentials = generateServicesCredentials(services);
129
+
130
+ // Override postgres database name with project-derived name if provided
131
+ if (options?.projectName && serviceCredentials.postgres) {
132
+ serviceCredentials.postgres.database = `${options.projectName.replace(/-/g, '_')}_dev`;
133
+ }
134
+
124
135
  const urls = generateConnectionUrls(serviceCredentials);
125
136
 
126
137
  return {
@@ -86,23 +86,29 @@ export async function secretsInitCommand(
86
86
  );
87
87
  }
88
88
 
89
- // Generate secrets
90
- const secrets = createStageSecrets(stage, services);
91
-
92
- // Detect workspace mode and generate fullstack-aware custom secrets
89
+ // Detect workspace mode for project name and fullstack secrets
90
+ let projectName: string | undefined;
91
+ let workspaceSecrets: Record<string, string> | undefined;
93
92
  try {
94
93
  const loaded = await loadWorkspaceConfig();
94
+ projectName = loaded.workspace.name;
95
95
  const isMultiApp = Object.keys(loaded.workspace.apps).length > 1;
96
96
 
97
97
  if (isMultiApp) {
98
- const customSecrets = generateFullstackCustomSecrets(loaded.workspace);
99
- secrets.custom = customSecrets;
98
+ workspaceSecrets = generateFullstackCustomSecrets(loaded.workspace);
100
99
  logger.log(' Detected workspace mode — generating per-app secrets');
101
100
  }
102
101
  } catch {
103
102
  // Not a workspace — single-app mode, skip custom secrets
104
103
  }
105
104
 
105
+ // Generate secrets (with project name so DATABASE_URL matches app-specific URLs)
106
+ const secrets = createStageSecrets(stage, services, { projectName });
107
+
108
+ if (workspaceSecrets) {
109
+ secrets.custom = workspaceSecrets;
110
+ }
111
+
106
112
  // Write to file
107
113
  await writeStageSecrets(secrets);
108
114
 
@@ -196,7 +196,9 @@ async function generateFreshSecrets(
196
196
  if (workspace.services.cache) serviceNames.push('redis');
197
197
 
198
198
  // Create base secrets with service credentials
199
- const secrets = createStageSecrets(stage, serviceNames);
199
+ const secrets = createStageSecrets(stage, serviceNames, {
200
+ projectName: workspace.name,
201
+ });
200
202
 
201
203
  // Generate fullstack-aware custom secrets
202
204
  const isMultiApp = Object.keys(workspace.apps).length > 1;
@@ -24,7 +24,7 @@ import {
24
24
  rewriteUrlsWithPorts,
25
25
  savePortState,
26
26
  } from '../../dev/index';
27
- import { ensureTestDatabase, rewriteDatabaseUrlForTests } from '../index';
27
+ import { rewriteDatabaseUrlForTests } from '../index';
28
28
 
29
29
  describe('rewriteDatabaseUrlForTests', () => {
30
30
  beforeAll(() => {
@@ -128,39 +128,6 @@ describe('rewriteDatabaseUrlForTests', () => {
128
128
  });
129
129
  });
130
130
 
131
- describe('ensureTestDatabase', () => {
132
- beforeAll(() => {
133
- vi.spyOn(console, 'log').mockImplementation(() => {});
134
- });
135
-
136
- afterAll(() => {
137
- vi.restoreAllMocks();
138
- });
139
-
140
- it('should do nothing when DATABASE_URL is missing', async () => {
141
- // Should resolve without error
142
- await ensureTestDatabase({});
143
- await ensureTestDatabase({ REDIS_URL: 'redis://localhost:6379' });
144
- });
145
-
146
- it('should do nothing when database name is empty', async () => {
147
- await ensureTestDatabase({
148
- DATABASE_URL: 'postgresql://app:secret@localhost:5432/',
149
- });
150
- });
151
-
152
- it('should not throw when postgres is unreachable', async () => {
153
- // Use a port that's almost certainly not running postgres
154
- await ensureTestDatabase({
155
- DATABASE_URL: 'postgresql://app:secret@localhost:59999/test_db',
156
- });
157
- // Should log a warning but not throw
158
- expect(console.log).toHaveBeenCalledWith(
159
- expect.stringContaining('Could not ensure test database'),
160
- );
161
- });
162
- });
163
-
164
131
  describe('port rewriting + test database pipeline', () => {
165
132
  let testDir: string;
166
133
 
@@ -359,4 +326,65 @@ services:
359
326
  // RabbitMQ port rewritten
360
327
  expect(secrets.RABBITMQ_URL).toBe('amqp://app:secret@localhost:5673');
361
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
+ });
362
390
  });
package/src/test/index.ts CHANGED
@@ -81,7 +81,6 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
81
81
 
82
82
  // 4. Use a separate test database (append _test suffix)
83
83
  secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
84
- await ensureTestDatabase(secretsEnv);
85
84
 
86
85
  // 5. Load workspace config + dependency URLs + sniff env vars
87
86
  let dependencyEnv: Record<string, string> = {};
@@ -225,43 +224,3 @@ export function rewriteDatabaseUrlForTests(
225
224
 
226
225
  return result;
227
226
  }
228
-
229
- /**
230
- * Ensure the test database exists by connecting to the default database
231
- * and running CREATE DATABASE IF NOT EXISTS.
232
- * @internal Exported for testing
233
- */
234
- export async function ensureTestDatabase(
235
- env: Record<string, string>,
236
- ): Promise<void> {
237
- const databaseUrl = env.DATABASE_URL;
238
- if (!databaseUrl) return;
239
-
240
- try {
241
- const url = new URL(databaseUrl);
242
- const testDbName = url.pathname.slice(1);
243
- if (!testDbName) return;
244
-
245
- // Connect to the default 'postgres' database to create the test database
246
- url.pathname = '/postgres';
247
- const { default: pg } = await import('pg');
248
- const client = new pg.Client({ connectionString: url.toString() });
249
- await client.connect();
250
-
251
- try {
252
- await client.query(`CREATE DATABASE "${testDbName}"`);
253
- console.log(` 📦 Created test database "${testDbName}"`);
254
- } catch (err: unknown) {
255
- // 42P04 = database already exists — that's fine
256
- if ((err as { code?: string }).code !== '42P04') throw err;
257
- } finally {
258
- await client.end();
259
- }
260
- } catch (err) {
261
- // Don't fail test startup if we can't create the database
262
- // (e.g., postgres not running yet, will fail later with a clear error)
263
- console.log(
264
- ` ⚠️ Could not ensure test database: ${(err as Error).message}`,
265
- );
266
- }
267
- }
@@ -85,7 +85,7 @@ export type FrontendFramework = 'nextjs' | 'remix' | 'vite';
85
85
  * @example
86
86
  * ```ts
87
87
  * // Use specific version
88
- * db: { version: '16-alpine' }
88
+ * db: { version: '18-alpine' }
89
89
  *
90
90
  * // Use custom image
91
91
  * db: { image: 'timescale/timescaledb:latest-pg16' }
@@ -144,7 +144,7 @@ export interface MailServiceConfig extends ServiceImageConfig {
144
144
  *
145
145
  * // Custom versions
146
146
  * services: {
147
- * db: { version: '16-alpine' },
147
+ * db: { version: '18-alpine' },
148
148
  * cache: { version: '7-alpine' },
149
149
  * }
150
150
  *