@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.
- package/CHANGELOG.md +12 -0
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/{fullstack-secrets-DqxYGrgW.cjs → fullstack-secrets-BctGaE4E.cjs} +7 -2
- package/dist/fullstack-secrets-BctGaE4E.cjs.map +1 -0
- package/dist/{fullstack-secrets-odm79Uo1.mjs → fullstack-secrets-ca0Kyrvt.mjs} +7 -2
- package/dist/fullstack-secrets-ca0Kyrvt.mjs.map +1 -0
- package/dist/{index-3n-giNaw.d.mts → index-9tjTQjFt.d.mts} +3 -3
- package/dist/{index-3n-giNaw.d.mts.map → index-9tjTQjFt.d.mts.map} +1 -1
- package/dist/{index-CiEOtKEX.d.cts → index-VOKKO-lm.d.cts} +3 -3
- package/dist/{index-CiEOtKEX.d.cts.map → index-VOKKO-lm.d.cts.map} +1 -1
- package/dist/index.cjs +37 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +37 -56
- package/dist/index.mjs.map +1 -1
- package/dist/{reconcile-WzC1oAUV.mjs → reconcile-C5OyCA7V.mjs} +2 -2
- package/dist/{reconcile-WzC1oAUV.mjs.map → reconcile-C5OyCA7V.mjs.map} +1 -1
- package/dist/{reconcile-CCtrj-zt.cjs → reconcile-TEBsryVn.cjs} +2 -2
- package/dist/{reconcile-CCtrj-zt.cjs.map → reconcile-TEBsryVn.cjs.map} +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
- package/dist/workspace-D4z4A4cq.mjs.map +1 -1
- package/package.json +4 -4
- package/src/dev/__tests__/index.spec.ts +35 -9
- package/src/dev/index.ts +18 -1
- package/src/docker/__tests__/compose.spec.ts +13 -11
- package/src/docker/compose.ts +10 -10
- package/src/init/__tests__/init.spec.ts +1 -1
- package/src/init/generators/docker.ts +7 -7
- package/src/init/index.ts +3 -1
- package/src/secrets/generator.ts +11 -0
- package/src/secrets/index.ts +12 -6
- package/src/setup/index.ts +3 -1
- package/src/test/__tests__/index.spec.ts +62 -34
- package/src/test/index.ts +0 -41
- package/src/workspace/types.ts +2 -2
- package/dist/fullstack-secrets-DqxYGrgW.cjs.map +0 -1
- 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.
|
|
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/
|
|
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@
|
|
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
|
|
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@
|
|
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@
|
|
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).
|
|
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).
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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('-
|
|
185
|
-
expect(yaml).toContain('
|
|
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(
|
|
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('
|
|
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:
|
|
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
|
|
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('
|
|
847
|
-
expect(yaml).toContain('
|
|
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', () => {
|
package/src/docker/compose.ts
CHANGED
|
@@ -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: '
|
|
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
|
|
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
|
-
-
|
|
157
|
+
- dbdata:/var/lib/postgresql/18/data
|
|
158
158
|
healthcheck:
|
|
159
|
-
test: ["CMD-SHELL", "pg_isready -U
|
|
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 += `
|
|
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
|
-
-
|
|
336
|
+
- dbdata:/var/lib/postgresql/18/data
|
|
337
337
|
healthcheck:
|
|
338
|
-
test: ["CMD-SHELL", "pg_isready -U
|
|
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 += `
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
- dbdata:/var/lib/postgresql/18/data${initVolume}
|
|
56
56
|
healthcheck:
|
|
57
|
-
test: ['CMD-SHELL', 'pg_isready -U
|
|
57
|
+
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER']
|
|
58
58
|
interval: 5s
|
|
59
59
|
timeout: 5s
|
|
60
60
|
retries: 5`);
|
|
61
|
-
volumes.push('
|
|
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> = {
|
package/src/secrets/generator.ts
CHANGED
|
@@ -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 {
|
package/src/secrets/index.ts
CHANGED
|
@@ -86,23 +86,29 @@ export async function secretsInitCommand(
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
//
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
package/src/setup/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
}
|
package/src/workspace/types.ts
CHANGED
|
@@ -85,7 +85,7 @@ export type FrontendFramework = 'nextjs' | 'remix' | 'vite';
|
|
|
85
85
|
* @example
|
|
86
86
|
* ```ts
|
|
87
87
|
* // Use specific version
|
|
88
|
-
* db: { version: '
|
|
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: '
|
|
147
|
+
* db: { version: '18-alpine' },
|
|
148
148
|
* cache: { version: '7-alpine' },
|
|
149
149
|
* }
|
|
150
150
|
*
|