@geekmidas/cli 1.10.6 → 1.10.8
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/README.md +44 -1
- package/dist/index.cjs +54 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +54 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/dev/__tests__/index.spec.ts +69 -0
- package/src/dev/index.ts +27 -9
- package/src/docker/__tests__/compose.spec.ts +6 -1
- package/src/docker/compose.ts +8 -3
- package/src/init/generators/docker.ts +3 -1
- package/src/test/__tests__/index.spec.ts +115 -0
- package/src/test/index.ts +41 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.8",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -56,10 +56,10 @@
|
|
|
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
|
-
"@geekmidas/
|
|
60
|
+
"@geekmidas/envkit": "~1.0.3",
|
|
62
61
|
"@geekmidas/errors": "~1.0.0",
|
|
62
|
+
"@geekmidas/logger": "~1.0.0",
|
|
63
63
|
"@geekmidas/schema": "~1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
NormalizedWorkspace,
|
|
10
10
|
} from '../../workspace/index.js';
|
|
11
11
|
import {
|
|
12
|
+
buildDockerComposeEnv,
|
|
12
13
|
checkPortConflicts,
|
|
13
14
|
findAvailablePort,
|
|
14
15
|
generateAllDependencyEnvVars,
|
|
@@ -1753,3 +1754,71 @@ describe('generateServerEntryContent', () => {
|
|
|
1753
1754
|
expect(content).toContain("await import('./custom-app.js')");
|
|
1754
1755
|
});
|
|
1755
1756
|
});
|
|
1757
|
+
|
|
1758
|
+
describe('buildDockerComposeEnv', () => {
|
|
1759
|
+
it('should include secrets in the env passed to docker compose', () => {
|
|
1760
|
+
const secretsEnv = {
|
|
1761
|
+
POSTGRES_USER: 'app',
|
|
1762
|
+
POSTGRES_PASSWORD: 'supersecret',
|
|
1763
|
+
POSTGRES_DB: 'myproject_dev',
|
|
1764
|
+
};
|
|
1765
|
+
const portEnv = { POSTGRES_HOST_PORT: '5434' };
|
|
1766
|
+
|
|
1767
|
+
const env = buildDockerComposeEnv(secretsEnv, portEnv);
|
|
1768
|
+
|
|
1769
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
1770
|
+
expect(env.POSTGRES_PASSWORD).toBe('supersecret');
|
|
1771
|
+
expect(env.POSTGRES_DB).toBe('myproject_dev');
|
|
1772
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
it('should include multiple service secrets', () => {
|
|
1776
|
+
const secretsEnv = {
|
|
1777
|
+
POSTGRES_USER: 'app',
|
|
1778
|
+
POSTGRES_PASSWORD: 'dbpass',
|
|
1779
|
+
REDIS_PASSWORD: 'redispass',
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
const env = buildDockerComposeEnv(secretsEnv, {});
|
|
1783
|
+
|
|
1784
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
1785
|
+
expect(env.POSTGRES_PASSWORD).toBe('dbpass');
|
|
1786
|
+
expect(env.REDIS_PASSWORD).toBe('redispass');
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
it('should let port env override secrets env for same key', () => {
|
|
1790
|
+
const secretsEnv = { POSTGRES_HOST_PORT: '5432' };
|
|
1791
|
+
const portEnv = { POSTGRES_HOST_PORT: '5434' };
|
|
1792
|
+
|
|
1793
|
+
const env = buildDockerComposeEnv(secretsEnv, portEnv);
|
|
1794
|
+
|
|
1795
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
it('should work without secrets env', () => {
|
|
1799
|
+
const env = buildDockerComposeEnv(undefined, {
|
|
1800
|
+
POSTGRES_HOST_PORT: '5434',
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
it('should work without any arguments', () => {
|
|
1807
|
+
const env = buildDockerComposeEnv();
|
|
1808
|
+
|
|
1809
|
+
// Should at least have process.env
|
|
1810
|
+
expect(env.PATH).toBeDefined();
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
it('should include process.env as base', () => {
|
|
1814
|
+
const env = buildDockerComposeEnv(
|
|
1815
|
+
{ POSTGRES_USER: 'app' },
|
|
1816
|
+
{ POSTGRES_HOST_PORT: '5434' },
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
// process.env values should be present
|
|
1820
|
+
expect(env.PATH).toBeDefined();
|
|
1821
|
+
// Custom values should override
|
|
1822
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
1823
|
+
});
|
|
1824
|
+
});
|
package/src/dev/index.ts
CHANGED
|
@@ -1111,13 +1111,29 @@ export async function loadSecretsForApp(
|
|
|
1111
1111
|
return mapped;
|
|
1112
1112
|
}
|
|
1113
1113
|
|
|
1114
|
+
/**
|
|
1115
|
+
* Build the environment variables to pass to `docker compose up`.
|
|
1116
|
+
* Merges process.env, secrets, and port mappings so that Docker Compose
|
|
1117
|
+
* can interpolate variables like ${POSTGRES_USER} correctly.
|
|
1118
|
+
* @internal Exported for testing
|
|
1119
|
+
*/
|
|
1120
|
+
export function buildDockerComposeEnv(
|
|
1121
|
+
secretsEnv?: Record<string, string>,
|
|
1122
|
+
portEnv?: Record<string, string>,
|
|
1123
|
+
): Record<string, string | undefined> {
|
|
1124
|
+
return { ...process.env, ...secretsEnv, ...portEnv };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1114
1127
|
/**
|
|
1115
1128
|
* Start docker-compose services for the workspace.
|
|
1129
|
+
* Passes both port mappings and secrets to docker-compose so that
|
|
1130
|
+
* variables like POSTGRES_USER/POSTGRES_PASSWORD are available.
|
|
1116
1131
|
* @internal Exported for testing
|
|
1117
1132
|
*/
|
|
1118
1133
|
export async function startWorkspaceServices(
|
|
1119
1134
|
workspace: NormalizedWorkspace,
|
|
1120
1135
|
portEnv?: Record<string, string>,
|
|
1136
|
+
secretsEnv?: Record<string, string>,
|
|
1121
1137
|
): Promise<void> {
|
|
1122
1138
|
const services = workspace.services;
|
|
1123
1139
|
if (!services.db && !services.cache && !services.mail) {
|
|
@@ -1152,11 +1168,12 @@ export async function startWorkspaceServices(
|
|
|
1152
1168
|
return;
|
|
1153
1169
|
}
|
|
1154
1170
|
|
|
1155
|
-
// Start services with docker-compose
|
|
1171
|
+
// Start services with docker-compose, passing secrets so that
|
|
1172
|
+
// POSTGRES_USER, POSTGRES_PASSWORD, etc. are interpolated correctly
|
|
1156
1173
|
execSync(`docker compose up -d ${servicesToStart.join(' ')}`, {
|
|
1157
1174
|
cwd: workspace.root,
|
|
1158
1175
|
stdio: 'inherit',
|
|
1159
|
-
env:
|
|
1176
|
+
env: buildDockerComposeEnv(secretsEnv, portEnv),
|
|
1160
1177
|
});
|
|
1161
1178
|
|
|
1162
1179
|
logger.log('✅ Services started');
|
|
@@ -1246,14 +1263,15 @@ async function workspaceDevCommand(
|
|
|
1246
1263
|
// Resolve dynamic service ports from docker-compose.yml
|
|
1247
1264
|
const resolvedPorts = await resolveServicePorts(workspace.root);
|
|
1248
1265
|
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1266
|
+
// Load secrets BEFORE starting Docker so POSTGRES_USER, POSTGRES_PASSWORD,
|
|
1267
|
+
// etc. are available for docker-compose variable interpolation
|
|
1268
|
+
const rawSecrets = await loadDevSecrets(workspace);
|
|
1251
1269
|
|
|
1252
|
-
//
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
);
|
|
1270
|
+
// Start docker-compose services with resolved ports AND secrets
|
|
1271
|
+
await startWorkspaceServices(workspace, resolvedPorts.dockerEnv, rawSecrets);
|
|
1272
|
+
|
|
1273
|
+
// Rewrite URLs with resolved ports (hostnames and port numbers)
|
|
1274
|
+
const secretsEnv = rewriteUrlsWithPorts(rawSecrets, resolvedPorts);
|
|
1257
1275
|
if (Object.keys(secretsEnv).length > 0) {
|
|
1258
1276
|
logger.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
|
|
1259
1277
|
}
|
|
@@ -577,7 +577,12 @@ describe('generateWorkspaceCompose', () => {
|
|
|
577
577
|
const workspace = createWorkspace();
|
|
578
578
|
const yaml = generateWorkspaceCompose(workspace);
|
|
579
579
|
|
|
580
|
-
expect(yaml).toContain(
|
|
580
|
+
expect(yaml).toContain(
|
|
581
|
+
'# Use "gkm dev" or "gkm test" to start services.',
|
|
582
|
+
);
|
|
583
|
+
expect(yaml).toContain(
|
|
584
|
+
'# Running "docker compose up" directly will not inject secrets or resolve ports.',
|
|
585
|
+
);
|
|
581
586
|
});
|
|
582
587
|
|
|
583
588
|
it('should include services section', () => {
|
package/src/docker/compose.ts
CHANGED
|
@@ -87,7 +87,9 @@ export function generateDockerCompose(options: ComposeOptions): string {
|
|
|
87
87
|
|
|
88
88
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
89
89
|
|
|
90
|
-
let yaml =
|
|
90
|
+
let yaml = `# Use "gkm dev" or "gkm test" to start services.
|
|
91
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
92
|
+
version: '3.8'
|
|
91
93
|
|
|
92
94
|
services:
|
|
93
95
|
api:
|
|
@@ -248,7 +250,9 @@ export function generateMinimalDockerCompose(
|
|
|
248
250
|
|
|
249
251
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
250
252
|
|
|
251
|
-
return
|
|
253
|
+
return `# Use "gkm dev" or "gkm test" to start services.
|
|
254
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
255
|
+
version: '3.8'
|
|
252
256
|
|
|
253
257
|
services:
|
|
254
258
|
api:
|
|
@@ -307,7 +311,8 @@ export function generateWorkspaceCompose(
|
|
|
307
311
|
const redisImage = getInfraServiceImage('redis', services.cache);
|
|
308
312
|
|
|
309
313
|
let yaml = `# Docker Compose for ${workspace.name} workspace
|
|
310
|
-
#
|
|
314
|
+
# Use "gkm dev" or "gkm test" to start services.
|
|
315
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
311
316
|
|
|
312
317
|
services:
|
|
313
318
|
`;
|
|
@@ -161,7 +161,9 @@ export function generateDockerFiles(
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// Build docker-compose.yml
|
|
164
|
-
let dockerCompose =
|
|
164
|
+
let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
|
|
165
|
+
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
166
|
+
services:
|
|
165
167
|
${services.join('\n\n')}
|
|
166
168
|
`;
|
|
167
169
|
|
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
vi,
|
|
19
19
|
} from 'vitest';
|
|
20
20
|
import {
|
|
21
|
+
buildDockerComposeEnv,
|
|
21
22
|
createCredentialsPreload,
|
|
22
23
|
loadPortState,
|
|
23
24
|
parseComposePortMappings,
|
|
25
|
+
resolveServicePorts,
|
|
24
26
|
rewriteUrlsWithPorts,
|
|
25
27
|
savePortState,
|
|
26
28
|
} from '../../dev/index';
|
|
@@ -388,3 +390,116 @@ services:
|
|
|
388
390
|
);
|
|
389
391
|
});
|
|
390
392
|
});
|
|
393
|
+
|
|
394
|
+
describe('test command Docker startup pipeline', () => {
|
|
395
|
+
let testDir: string;
|
|
396
|
+
|
|
397
|
+
beforeAll(() => {
|
|
398
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
399
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
afterAll(() => {
|
|
403
|
+
vi.restoreAllMocks();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
beforeEach(() => {
|
|
407
|
+
testDir = join(tmpdir(), `gkm-test-docker-${Date.now()}`);
|
|
408
|
+
mkdirSync(testDir, { recursive: true });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
afterEach(() => {
|
|
412
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should build docker compose env with secrets so credentials are interpolated', () => {
|
|
416
|
+
const secretsEnv = {
|
|
417
|
+
POSTGRES_USER: 'app',
|
|
418
|
+
POSTGRES_PASSWORD: 'supersecret',
|
|
419
|
+
POSTGRES_DB: 'myapp',
|
|
420
|
+
};
|
|
421
|
+
const portEnv = { POSTGRES_HOST_PORT: '5434' };
|
|
422
|
+
|
|
423
|
+
// This is the env that gets passed to `docker compose up -d`
|
|
424
|
+
const env = buildDockerComposeEnv(secretsEnv, portEnv);
|
|
425
|
+
|
|
426
|
+
// Secrets are available so ${POSTGRES_USER:-postgres} resolves correctly
|
|
427
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
428
|
+
expect(env.POSTGRES_PASSWORD).toBe('supersecret');
|
|
429
|
+
expect(env.POSTGRES_DB).toBe('myapp');
|
|
430
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should resolve ports, rewrite URLs and hostnames, then append _test', async () => {
|
|
434
|
+
writeFileSync(
|
|
435
|
+
join(testDir, 'docker-compose.yml'),
|
|
436
|
+
`
|
|
437
|
+
services:
|
|
438
|
+
postgres:
|
|
439
|
+
image: postgres:18
|
|
440
|
+
ports:
|
|
441
|
+
- '\${POSTGRES_HOST_PORT:-5432}:5432'
|
|
442
|
+
redis:
|
|
443
|
+
image: redis:7
|
|
444
|
+
ports:
|
|
445
|
+
- '\${REDIS_HOST_PORT:-6379}:6379'
|
|
446
|
+
`,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// Simulate secrets loaded from encrypted store (Docker hostnames)
|
|
450
|
+
const secretsEnv: Record<string, string> = {
|
|
451
|
+
DATABASE_URL: 'postgresql://app:supersecret@postgres:5432/myapp',
|
|
452
|
+
REDIS_URL: 'redis://:redispass@redis:6379',
|
|
453
|
+
POSTGRES_USER: 'app',
|
|
454
|
+
POSTGRES_PASSWORD: 'supersecret',
|
|
455
|
+
POSTGRES_DB: 'myapp',
|
|
456
|
+
POSTGRES_HOST: 'postgres',
|
|
457
|
+
POSTGRES_PORT: '5432',
|
|
458
|
+
REDIS_PASSWORD: 'redispass',
|
|
459
|
+
REDIS_HOST: 'redis',
|
|
460
|
+
REDIS_PORT: '6379',
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Step 1: Resolve ports
|
|
464
|
+
const resolvedPorts = await resolveServicePorts(testDir);
|
|
465
|
+
|
|
466
|
+
// Step 2: Build env for docker compose (secrets + ports)
|
|
467
|
+
const dockerEnv = buildDockerComposeEnv(
|
|
468
|
+
secretsEnv,
|
|
469
|
+
resolvedPorts.dockerEnv,
|
|
470
|
+
);
|
|
471
|
+
expect(dockerEnv.POSTGRES_USER).toBe('app');
|
|
472
|
+
expect(dockerEnv.POSTGRES_PASSWORD).toBe('supersecret');
|
|
473
|
+
|
|
474
|
+
// Step 3: Rewrite URLs with ports and hostnames
|
|
475
|
+
const rewritten = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
|
|
476
|
+
|
|
477
|
+
// Step 4: Apply test suffix
|
|
478
|
+
const final = rewriteDatabaseUrlForTests(rewritten);
|
|
479
|
+
|
|
480
|
+
// Hostnames should be rewritten to localhost
|
|
481
|
+
expect(final.POSTGRES_HOST).toBe('localhost');
|
|
482
|
+
expect(final.REDIS_HOST).toBe('localhost');
|
|
483
|
+
|
|
484
|
+
// DATABASE_URL should have localhost and _test suffix
|
|
485
|
+
expect(final.DATABASE_URL).toContain('@localhost:');
|
|
486
|
+
expect(final.DATABASE_URL).toMatch(/\/myapp_test$/);
|
|
487
|
+
|
|
488
|
+
// REDIS_URL should have localhost
|
|
489
|
+
expect(final.REDIS_URL).toContain('@localhost:');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should not default to postgres:postgres when secrets are provided', () => {
|
|
493
|
+
const secretsEnv = {
|
|
494
|
+
POSTGRES_USER: 'myapp',
|
|
495
|
+
POSTGRES_PASSWORD: 'strongpass123',
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const env = buildDockerComposeEnv(secretsEnv, {});
|
|
499
|
+
|
|
500
|
+
// The key assertion: secrets are in env so Docker Compose
|
|
501
|
+
// ${POSTGRES_USER:-postgres} resolves to 'myapp', not 'postgres'
|
|
502
|
+
expect(env.POSTGRES_USER).toBe('myapp');
|
|
503
|
+
expect(env.POSTGRES_PASSWORD).toBe('strongpass123');
|
|
504
|
+
});
|
|
505
|
+
});
|
package/src/test/index.ts
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
loadEnvFiles,
|
|
9
9
|
loadPortState,
|
|
10
10
|
parseComposePortMappings,
|
|
11
|
+
resolveServicePorts,
|
|
11
12
|
rewriteUrlsWithPorts,
|
|
13
|
+
startWorkspaceServices,
|
|
12
14
|
} from '../dev/index';
|
|
13
15
|
import { readStageSecrets, toEmbeddableSecrets } from '../secrets/storage';
|
|
14
16
|
import { getDependencyEnvVars } from '../workspace/index';
|
|
@@ -64,28 +66,28 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
// 3.
|
|
68
|
-
const composePath = join(cwd, 'docker-compose.yml');
|
|
69
|
-
const mappings = parseComposePortMappings(composePath);
|
|
70
|
-
if (mappings.length > 0) {
|
|
71
|
-
const ports = await loadPortState(cwd);
|
|
72
|
-
if (Object.keys(ports).length > 0) {
|
|
73
|
-
secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
|
|
74
|
-
dockerEnv: {},
|
|
75
|
-
ports,
|
|
76
|
-
mappings,
|
|
77
|
-
});
|
|
78
|
-
console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 4. Use a separate test database (append _test suffix)
|
|
83
|
-
secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
|
|
84
|
-
|
|
85
|
-
// 5. Load workspace config + dependency URLs + sniff env vars
|
|
69
|
+
// 3. Load workspace config + start Docker services with secrets
|
|
86
70
|
let dependencyEnv: Record<string, string> = {};
|
|
87
71
|
try {
|
|
88
72
|
const appInfo = await loadWorkspaceAppInfo(cwd);
|
|
73
|
+
|
|
74
|
+
// Resolve ports and start Docker services with secrets so that
|
|
75
|
+
// POSTGRES_USER, POSTGRES_PASSWORD, etc. are interpolated correctly
|
|
76
|
+
const resolvedPorts = await resolveServicePorts(appInfo.workspaceRoot);
|
|
77
|
+
await startWorkspaceServices(
|
|
78
|
+
appInfo.workspace,
|
|
79
|
+
resolvedPorts.dockerEnv,
|
|
80
|
+
secretsEnv,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Rewrite URLs with resolved Docker ports and hostnames
|
|
84
|
+
if (resolvedPorts.mappings.length > 0) {
|
|
85
|
+
secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
|
|
86
|
+
console.log(
|
|
87
|
+
` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
89
91
|
dependencyEnv = getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
90
92
|
|
|
91
93
|
if (Object.keys(dependencyEnv).length > 0) {
|
|
@@ -94,7 +96,7 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
|
|
|
94
96
|
);
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
//
|
|
99
|
+
// Sniff to detect which env vars the app needs
|
|
98
100
|
const sniffed = await sniffAppEnvironment(
|
|
99
101
|
appInfo.app,
|
|
100
102
|
appInfo.appName,
|
|
@@ -119,9 +121,27 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
|
|
|
119
121
|
);
|
|
120
122
|
}
|
|
121
123
|
} catch {
|
|
122
|
-
// Not in a workspace —
|
|
124
|
+
// Not in a workspace — fall back to port state file
|
|
125
|
+
const composePath = join(cwd, 'docker-compose.yml');
|
|
126
|
+
const mappings = parseComposePortMappings(composePath);
|
|
127
|
+
if (mappings.length > 0) {
|
|
128
|
+
const ports = await loadPortState(cwd);
|
|
129
|
+
if (Object.keys(ports).length > 0) {
|
|
130
|
+
secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
|
|
131
|
+
dockerEnv: {},
|
|
132
|
+
ports,
|
|
133
|
+
mappings,
|
|
134
|
+
});
|
|
135
|
+
console.log(
|
|
136
|
+
` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
123
140
|
}
|
|
124
141
|
|
|
142
|
+
// 4. Use a separate test database (append _test suffix)
|
|
143
|
+
secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
|
|
144
|
+
|
|
125
145
|
console.log('');
|
|
126
146
|
|
|
127
147
|
// Write combined secrets to JSON and create credentials preload
|