@geekmidas/cli 1.10.7 → 1.10.9
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/{bundler-NpfYPBUo.cjs → bundler-Bm3Az_sv.cjs} +2 -2
- package/dist/{bundler-NpfYPBUo.cjs.map → bundler-Bm3Az_sv.cjs.map} +1 -1
- package/dist/{bundler-DQYjKFPm.mjs → bundler-kk_XJTRp.mjs} +2 -2
- package/dist/{bundler-DQYjKFPm.mjs.map → bundler-kk_XJTRp.mjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/{fullstack-secrets-ca0Kyrvt.mjs → fullstack-secrets-C2lbdbLZ.mjs} +15 -1
- package/dist/fullstack-secrets-C2lbdbLZ.mjs.map +1 -0
- package/dist/{fullstack-secrets-BctGaE4E.cjs → fullstack-secrets-CtWIYuI0.cjs} +15 -1
- package/dist/fullstack-secrets-CtWIYuI0.cjs.map +1 -0
- package/dist/{index-9tjTQjFt.d.mts → index-BdJZKXCJ.d.cts} +4 -2
- package/dist/index-BdJZKXCJ.d.cts.map +1 -0
- package/dist/{index-VOKKO-lm.d.cts → index-DB9VbcCD.d.mts} +4 -2
- package/dist/index-DB9VbcCD.d.mts.map +1 -0
- package/dist/index.cjs +177 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +177 -61
- package/dist/index.mjs.map +1 -1
- package/dist/openapi-BYxAWwok.cjs.map +1 -1
- package/dist/openapi-DenF-okj.mjs.map +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/{reconcile-C5OyCA7V.mjs → reconcile-BnM6FA6g.mjs} +2 -2
- package/dist/{reconcile-C5OyCA7V.mjs.map → reconcile-BnM6FA6g.mjs.map} +1 -1
- package/dist/{reconcile-TEBsryVn.cjs → reconcile-D6u4HSg8.cjs} +2 -2
- package/dist/{reconcile-TEBsryVn.cjs.map → reconcile-D6u4HSg8.cjs.map} +1 -1
- package/dist/{storage-DmCbr6DI.mjs → storage-B7H2PPCS.mjs} +8 -1
- package/dist/{storage-DmCbr6DI.mjs.map → storage-B7H2PPCS.mjs.map} +1 -1
- package/dist/{storage-Dx_jZbq6.mjs → storage-C1FNm2EP.mjs} +1 -1
- package/dist/{storage-CoCNe0Pt.cjs → storage-Cs13jkJ9.cjs} +8 -1
- package/dist/{storage-CoCNe0Pt.cjs.map → storage-Cs13jkJ9.cjs.map} +1 -1
- package/dist/{storage-C7pmBq1u.cjs → storage-D6BGLgWf.cjs} +1 -1
- package/dist/{sync-6FoT41G3.mjs → sync-CyGe5f1I.mjs} +1 -1
- package/dist/{sync-CbeKrnQV.mjs → sync-CzXruMzP.mjs} +2 -2
- package/dist/{sync-CbeKrnQV.mjs.map → sync-CzXruMzP.mjs.map} +1 -1
- package/dist/sync-DLlwsrBs.cjs +4 -0
- package/dist/{sync-DdkKaHqP.cjs → sync-oCqELfeA.cjs} +2 -2
- package/dist/{sync-DdkKaHqP.cjs.map → sync-oCqELfeA.cjs.map} +1 -1
- package/dist/{types-C7QJJl9f.d.cts → types-D4MLWXSL.d.cts} +2 -2
- package/dist/{types-C7QJJl9f.d.cts.map → types-D4MLWXSL.d.cts.map} +1 -1
- package/dist/{types-Iqsq_FIG.d.mts → types-DwpLq_fp.d.mts} +2 -2
- package/dist/{types-Iqsq_FIG.d.mts.map → types-DwpLq_fp.d.mts.map} +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
- package/dist/workspace-D4z4A4cq.mjs.map +1 -1
- package/package.json +5 -5
- package/src/dev/__tests__/index.spec.ts +142 -0
- package/src/dev/index.ts +67 -33
- package/src/docker/__tests__/compose.spec.ts +151 -2
- package/src/docker/compose.ts +105 -8
- package/src/init/generators/docker.ts +3 -1
- package/src/init/index.ts +1 -0
- package/src/init/versions.ts +1 -1
- package/src/secrets/__tests__/generator.spec.ts +68 -0
- package/src/secrets/__tests__/storage.spec.ts +30 -0
- package/src/secrets/generator.ts +18 -0
- package/src/secrets/index.ts +9 -0
- package/src/secrets/storage.ts +7 -0
- package/src/secrets/types.ts +4 -0
- package/src/setup/index.ts +1 -0
- package/src/test/__tests__/index.spec.ts +115 -0
- package/src/test/index.ts +41 -21
- package/src/types.ts +1 -1
- package/src/workspace/types.ts +2 -0
- package/dist/fullstack-secrets-BctGaE4E.cjs.map +0 -1
- package/dist/fullstack-secrets-ca0Kyrvt.mjs.map +0 -1
- package/dist/index-9tjTQjFt.d.mts.map +0 -1
- package/dist/index-VOKKO-lm.d.cts.map +0 -1
- package/dist/sync-RsnjXYwG.cjs +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.9",
|
|
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/errors": "~1.0.0",
|
|
60
|
-
"@geekmidas/envkit": "~1.0.3",
|
|
61
59
|
"@geekmidas/logger": "~1.0.0",
|
|
60
|
+
"@geekmidas/envkit": "~1.0.3",
|
|
62
61
|
"@geekmidas/constructs": "~3.0.2",
|
|
63
|
-
"@geekmidas/schema": "~1.0.0"
|
|
62
|
+
"@geekmidas/schema": "~1.0.0",
|
|
63
|
+
"@geekmidas/errors": "~1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"typescript": "^5.8.2",
|
|
71
71
|
"vitest": "^3.2.4",
|
|
72
72
|
"zod": "~4.1.13",
|
|
73
|
-
"@geekmidas/testkit": "1.0.
|
|
73
|
+
"@geekmidas/testkit": "1.0.5"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@geekmidas/telescope": "~1.0.0"
|
|
@@ -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,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
normalizeStudioConfig,
|
|
22
23
|
normalizeTelescopeConfig,
|
|
23
24
|
parseComposePortMappings,
|
|
25
|
+
parseComposeServiceNames,
|
|
24
26
|
replacePortInUrl,
|
|
25
27
|
rewriteUrlsWithPorts,
|
|
26
28
|
savePortState,
|
|
@@ -1706,6 +1708,78 @@ services:
|
|
|
1706
1708
|
});
|
|
1707
1709
|
});
|
|
1708
1710
|
|
|
1711
|
+
describe('parseComposeServiceNames', () => {
|
|
1712
|
+
let testDir: string;
|
|
1713
|
+
|
|
1714
|
+
beforeEach(() => {
|
|
1715
|
+
testDir = join(tmpdir(), `gkm-test-service-names-${Date.now()}`);
|
|
1716
|
+
mkdirSync(testDir, { recursive: true });
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
afterEach(() => {
|
|
1720
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
it('should return all service names from docker-compose.yml', () => {
|
|
1724
|
+
const composePath = join(testDir, 'docker-compose.yml');
|
|
1725
|
+
writeFileSync(
|
|
1726
|
+
composePath,
|
|
1727
|
+
`services:
|
|
1728
|
+
postgres:
|
|
1729
|
+
image: postgres:18-alpine
|
|
1730
|
+
redis:
|
|
1731
|
+
image: redis:7-alpine
|
|
1732
|
+
minio:
|
|
1733
|
+
image: minio/minio:latest
|
|
1734
|
+
`,
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
const names = parseComposeServiceNames(composePath);
|
|
1738
|
+
expect(names).toEqual(['postgres', 'redis', 'minio']);
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
it('should return empty array when file does not exist', () => {
|
|
1742
|
+
const names = parseComposeServiceNames(join(testDir, 'missing.yml'));
|
|
1743
|
+
expect(names).toEqual([]);
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
it('should return empty array when no services defined', () => {
|
|
1747
|
+
const composePath = join(testDir, 'docker-compose.yml');
|
|
1748
|
+
writeFileSync(composePath, 'version: "3.8"\n');
|
|
1749
|
+
|
|
1750
|
+
const names = parseComposeServiceNames(composePath);
|
|
1751
|
+
expect(names).toEqual([]);
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
it('should include app and infrastructure services', () => {
|
|
1755
|
+
const composePath = join(testDir, 'docker-compose.yml');
|
|
1756
|
+
writeFileSync(
|
|
1757
|
+
composePath,
|
|
1758
|
+
`services:
|
|
1759
|
+
api:
|
|
1760
|
+
build: .
|
|
1761
|
+
web:
|
|
1762
|
+
build: .
|
|
1763
|
+
postgres:
|
|
1764
|
+
image: postgres:18-alpine
|
|
1765
|
+
redis:
|
|
1766
|
+
image: redis:7-alpine
|
|
1767
|
+
custom-service:
|
|
1768
|
+
image: custom:latest
|
|
1769
|
+
`,
|
|
1770
|
+
);
|
|
1771
|
+
|
|
1772
|
+
const names = parseComposeServiceNames(composePath);
|
|
1773
|
+
expect(names).toEqual([
|
|
1774
|
+
'api',
|
|
1775
|
+
'web',
|
|
1776
|
+
'postgres',
|
|
1777
|
+
'redis',
|
|
1778
|
+
'custom-service',
|
|
1779
|
+
]);
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1709
1783
|
describe('generateServerEntryContent', () => {
|
|
1710
1784
|
it('should use dynamic import for createApp when secrets are provided', () => {
|
|
1711
1785
|
const content = generateServerEntryContent({
|
|
@@ -1753,3 +1827,71 @@ describe('generateServerEntryContent', () => {
|
|
|
1753
1827
|
expect(content).toContain("await import('./custom-app.js')");
|
|
1754
1828
|
});
|
|
1755
1829
|
});
|
|
1830
|
+
|
|
1831
|
+
describe('buildDockerComposeEnv', () => {
|
|
1832
|
+
it('should include secrets in the env passed to docker compose', () => {
|
|
1833
|
+
const secretsEnv = {
|
|
1834
|
+
POSTGRES_USER: 'app',
|
|
1835
|
+
POSTGRES_PASSWORD: 'supersecret',
|
|
1836
|
+
POSTGRES_DB: 'myproject_dev',
|
|
1837
|
+
};
|
|
1838
|
+
const portEnv = { POSTGRES_HOST_PORT: '5434' };
|
|
1839
|
+
|
|
1840
|
+
const env = buildDockerComposeEnv(secretsEnv, portEnv);
|
|
1841
|
+
|
|
1842
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
1843
|
+
expect(env.POSTGRES_PASSWORD).toBe('supersecret');
|
|
1844
|
+
expect(env.POSTGRES_DB).toBe('myproject_dev');
|
|
1845
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
it('should include multiple service secrets', () => {
|
|
1849
|
+
const secretsEnv = {
|
|
1850
|
+
POSTGRES_USER: 'app',
|
|
1851
|
+
POSTGRES_PASSWORD: 'dbpass',
|
|
1852
|
+
REDIS_PASSWORD: 'redispass',
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
const env = buildDockerComposeEnv(secretsEnv, {});
|
|
1856
|
+
|
|
1857
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
1858
|
+
expect(env.POSTGRES_PASSWORD).toBe('dbpass');
|
|
1859
|
+
expect(env.REDIS_PASSWORD).toBe('redispass');
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
it('should let port env override secrets env for same key', () => {
|
|
1863
|
+
const secretsEnv = { POSTGRES_HOST_PORT: '5432' };
|
|
1864
|
+
const portEnv = { POSTGRES_HOST_PORT: '5434' };
|
|
1865
|
+
|
|
1866
|
+
const env = buildDockerComposeEnv(secretsEnv, portEnv);
|
|
1867
|
+
|
|
1868
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
it('should work without secrets env', () => {
|
|
1872
|
+
const env = buildDockerComposeEnv(undefined, {
|
|
1873
|
+
POSTGRES_HOST_PORT: '5434',
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
expect(env.POSTGRES_HOST_PORT).toBe('5434');
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
it('should work without any arguments', () => {
|
|
1880
|
+
const env = buildDockerComposeEnv();
|
|
1881
|
+
|
|
1882
|
+
// Should at least have process.env
|
|
1883
|
+
expect(env.PATH).toBeDefined();
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
it('should include process.env as base', () => {
|
|
1887
|
+
const env = buildDockerComposeEnv(
|
|
1888
|
+
{ POSTGRES_USER: 'app' },
|
|
1889
|
+
{ POSTGRES_HOST_PORT: '5434' },
|
|
1890
|
+
);
|
|
1891
|
+
|
|
1892
|
+
// process.env values should be present
|
|
1893
|
+
expect(env.PATH).toBeDefined();
|
|
1894
|
+
// Custom values should override
|
|
1895
|
+
expect(env.POSTGRES_USER).toBe('app');
|
|
1896
|
+
});
|
|
1897
|
+
});
|
package/src/dev/index.ts
CHANGED
|
@@ -274,6 +274,8 @@ export async function resolveServicePorts(
|
|
|
274
274
|
const savedState = await loadPortState(workspaceRoot);
|
|
275
275
|
const dockerEnv: Record<string, string> = {};
|
|
276
276
|
const ports: PortState = {};
|
|
277
|
+
// Track ports assigned in this cycle to avoid duplicates
|
|
278
|
+
const assignedPorts = new Set<number>();
|
|
277
279
|
|
|
278
280
|
logger.log('\n🔌 Resolving service ports...');
|
|
279
281
|
|
|
@@ -287,6 +289,7 @@ export async function resolveServicePorts(
|
|
|
287
289
|
if (containerPort !== null) {
|
|
288
290
|
ports[mapping.envVar] = containerPort;
|
|
289
291
|
dockerEnv[mapping.envVar] = String(containerPort);
|
|
292
|
+
assignedPorts.add(containerPort);
|
|
290
293
|
logger.log(
|
|
291
294
|
` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`,
|
|
292
295
|
);
|
|
@@ -295,19 +298,28 @@ export async function resolveServicePorts(
|
|
|
295
298
|
|
|
296
299
|
// 2. Check saved port state
|
|
297
300
|
const savedPort = savedState[mapping.envVar];
|
|
298
|
-
if (
|
|
301
|
+
if (
|
|
302
|
+
savedPort &&
|
|
303
|
+
!assignedPorts.has(savedPort) &&
|
|
304
|
+
(await isPortAvailable(savedPort))
|
|
305
|
+
) {
|
|
299
306
|
ports[mapping.envVar] = savedPort;
|
|
300
307
|
dockerEnv[mapping.envVar] = String(savedPort);
|
|
308
|
+
assignedPorts.add(savedPort);
|
|
301
309
|
logger.log(
|
|
302
310
|
` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`,
|
|
303
311
|
);
|
|
304
312
|
continue;
|
|
305
313
|
}
|
|
306
314
|
|
|
307
|
-
// 3. Find available port
|
|
308
|
-
|
|
315
|
+
// 3. Find available port (skipping ports already assigned this cycle)
|
|
316
|
+
let resolvedPort = await findAvailablePort(mapping.defaultPort);
|
|
317
|
+
while (assignedPorts.has(resolvedPort)) {
|
|
318
|
+
resolvedPort = await findAvailablePort(resolvedPort + 1);
|
|
319
|
+
}
|
|
309
320
|
ports[mapping.envVar] = resolvedPort;
|
|
310
321
|
dockerEnv[mapping.envVar] = String(resolvedPort);
|
|
322
|
+
assignedPorts.add(resolvedPort);
|
|
311
323
|
|
|
312
324
|
if (resolvedPort !== mapping.defaultPort) {
|
|
313
325
|
logger.log(
|
|
@@ -1111,30 +1123,59 @@ export async function loadSecretsForApp(
|
|
|
1111
1123
|
return mapped;
|
|
1112
1124
|
}
|
|
1113
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* Build the environment variables to pass to `docker compose up`.
|
|
1128
|
+
* Merges process.env, secrets, and port mappings so that Docker Compose
|
|
1129
|
+
* can interpolate variables like ${POSTGRES_USER} correctly.
|
|
1130
|
+
* @internal Exported for testing
|
|
1131
|
+
*/
|
|
1132
|
+
export function buildDockerComposeEnv(
|
|
1133
|
+
secretsEnv?: Record<string, string>,
|
|
1134
|
+
portEnv?: Record<string, string>,
|
|
1135
|
+
): Record<string, string | undefined> {
|
|
1136
|
+
return { ...process.env, ...secretsEnv, ...portEnv };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Parse all service names from a docker-compose.yml file.
|
|
1141
|
+
* @internal Exported for testing
|
|
1142
|
+
*/
|
|
1143
|
+
export function parseComposeServiceNames(composePath: string): string[] {
|
|
1144
|
+
if (!existsSync(composePath)) {
|
|
1145
|
+
return [];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const content = readFileSync(composePath, 'utf-8');
|
|
1149
|
+
const compose = parseYaml(content) as {
|
|
1150
|
+
services?: Record<string, unknown>;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
return Object.keys(compose?.services ?? {});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1114
1156
|
/**
|
|
1115
1157
|
* Start docker-compose services for the workspace.
|
|
1158
|
+
* Parses the docker-compose.yml to discover all services and starts
|
|
1159
|
+
* everything except app services (which are managed by turbo).
|
|
1160
|
+
* This ensures manually added services are always started.
|
|
1116
1161
|
* @internal Exported for testing
|
|
1117
1162
|
*/
|
|
1118
1163
|
export async function startWorkspaceServices(
|
|
1119
1164
|
workspace: NormalizedWorkspace,
|
|
1120
1165
|
portEnv?: Record<string, string>,
|
|
1166
|
+
secretsEnv?: Record<string, string>,
|
|
1121
1167
|
): Promise<void> {
|
|
1122
|
-
const
|
|
1123
|
-
if (!
|
|
1168
|
+
const composeFile = join(workspace.root, 'docker-compose.yml');
|
|
1169
|
+
if (!existsSync(composeFile)) {
|
|
1124
1170
|
return;
|
|
1125
1171
|
}
|
|
1126
1172
|
|
|
1127
|
-
|
|
1173
|
+
// Discover all services from docker-compose.yml
|
|
1174
|
+
const allServices = parseComposeServiceNames(composeFile);
|
|
1128
1175
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
if (services.cache) {
|
|
1133
|
-
servicesToStart.push('redis');
|
|
1134
|
-
}
|
|
1135
|
-
if (services.mail) {
|
|
1136
|
-
servicesToStart.push('mailpit');
|
|
1137
|
-
}
|
|
1176
|
+
// Exclude app services (managed by turbo, not docker)
|
|
1177
|
+
const appNames = new Set(Object.keys(workspace.apps));
|
|
1178
|
+
const servicesToStart = allServices.filter((name) => !appNames.has(name));
|
|
1138
1179
|
|
|
1139
1180
|
if (servicesToStart.length === 0) {
|
|
1140
1181
|
return;
|
|
@@ -1143,20 +1184,12 @@ export async function startWorkspaceServices(
|
|
|
1143
1184
|
logger.log(`🐳 Starting services: ${servicesToStart.join(', ')}`);
|
|
1144
1185
|
|
|
1145
1186
|
try {
|
|
1146
|
-
//
|
|
1147
|
-
|
|
1148
|
-
if (!existsSync(composeFile)) {
|
|
1149
|
-
logger.warn(
|
|
1150
|
-
'⚠️ No docker-compose.yml found. Services will not be started.',
|
|
1151
|
-
);
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
// Start services with docker-compose
|
|
1187
|
+
// Start services with docker-compose, passing secrets so that
|
|
1188
|
+
// POSTGRES_USER, POSTGRES_PASSWORD, etc. are interpolated correctly
|
|
1156
1189
|
execSync(`docker compose up -d ${servicesToStart.join(' ')}`, {
|
|
1157
1190
|
cwd: workspace.root,
|
|
1158
1191
|
stdio: 'inherit',
|
|
1159
|
-
env:
|
|
1192
|
+
env: buildDockerComposeEnv(secretsEnv, portEnv),
|
|
1160
1193
|
});
|
|
1161
1194
|
|
|
1162
1195
|
logger.log('✅ Services started');
|
|
@@ -1246,14 +1279,15 @@ async function workspaceDevCommand(
|
|
|
1246
1279
|
// Resolve dynamic service ports from docker-compose.yml
|
|
1247
1280
|
const resolvedPorts = await resolveServicePorts(workspace.root);
|
|
1248
1281
|
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1282
|
+
// Load secrets BEFORE starting Docker so POSTGRES_USER, POSTGRES_PASSWORD,
|
|
1283
|
+
// etc. are available for docker-compose variable interpolation
|
|
1284
|
+
const rawSecrets = await loadDevSecrets(workspace);
|
|
1251
1285
|
|
|
1252
|
-
//
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
);
|
|
1286
|
+
// Start docker-compose services with resolved ports AND secrets
|
|
1287
|
+
await startWorkspaceServices(workspace, resolvedPorts.dockerEnv, rawSecrets);
|
|
1288
|
+
|
|
1289
|
+
// Rewrite URLs with resolved ports (hostnames and port numbers)
|
|
1290
|
+
const secretsEnv = rewriteUrlsWithPorts(rawSecrets, resolvedPorts);
|
|
1257
1291
|
if (Object.keys(secretsEnv).length > 0) {
|
|
1258
1292
|
logger.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
|
|
1259
1293
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ComposeServiceName } from '../../types';
|
|
2
3
|
import type { NormalizedWorkspace } from '../../workspace/types.js';
|
|
3
4
|
import {
|
|
4
5
|
type ComposeOptions,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
} from '../compose';
|
|
11
12
|
|
|
12
13
|
/** Helper to get full default image reference */
|
|
13
|
-
function getDefaultImage(service:
|
|
14
|
+
function getDefaultImage(service: ComposeServiceName): string {
|
|
14
15
|
return `${DEFAULT_SERVICE_IMAGES[service]}:${DEFAULT_SERVICE_VERSIONS[service]}`;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -344,6 +345,90 @@ describe('generateDockerCompose', () => {
|
|
|
344
345
|
});
|
|
345
346
|
});
|
|
346
347
|
|
|
348
|
+
describe('minio service', () => {
|
|
349
|
+
it('should add S3 environment variables', () => {
|
|
350
|
+
const yaml = generateDockerCompose({
|
|
351
|
+
...baseOptions,
|
|
352
|
+
services: { minio: true },
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(yaml).toContain('- S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000}');
|
|
356
|
+
expect(yaml).toContain('- S3_ACCESS_KEY_ID=${MINIO_ACCESS_KEY:-app}');
|
|
357
|
+
expect(yaml).toContain('- S3_SECRET_ACCESS_KEY=${MINIO_SECRET_KEY:-app}');
|
|
358
|
+
expect(yaml).toContain('- S3_BUCKET=${MINIO_BUCKET:-app}');
|
|
359
|
+
expect(yaml).toContain('- S3_REGION=${S3_REGION:-eu-west-1}');
|
|
360
|
+
expect(yaml).toContain('- S3_FORCE_PATH_STYLE=true');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should add minio service definition with default image', () => {
|
|
364
|
+
const yaml = generateDockerCompose({
|
|
365
|
+
...baseOptions,
|
|
366
|
+
services: { minio: true },
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(yaml).toContain('minio:');
|
|
370
|
+
expect(yaml).toContain(`image: ${getDefaultImage('minio')}`);
|
|
371
|
+
expect(yaml).toContain('container_name: minio');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should use custom minio version', () => {
|
|
375
|
+
const yaml = generateDockerCompose({
|
|
376
|
+
...baseOptions,
|
|
377
|
+
services: { minio: { version: 'RELEASE.2024-01-01' } },
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(yaml).toContain('image: minio/minio:RELEASE.2024-01-01');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should configure minio credentials', () => {
|
|
384
|
+
const yaml = generateDockerCompose({
|
|
385
|
+
...baseOptions,
|
|
386
|
+
services: { minio: true },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(yaml).toContain('MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-app}');
|
|
390
|
+
expect(yaml).toContain('MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-app}');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should expose console UI port', () => {
|
|
394
|
+
const yaml = generateDockerCompose({
|
|
395
|
+
...baseOptions,
|
|
396
|
+
services: { minio: true },
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(yaml).toContain('- "9001:9001"');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should add minio volume', () => {
|
|
403
|
+
const yaml = generateDockerCompose({
|
|
404
|
+
...baseOptions,
|
|
405
|
+
services: { minio: true },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(yaml).toContain('- minio_data:/data');
|
|
409
|
+
expect(yaml).toContain('minio_data:');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should include minio healthcheck', () => {
|
|
413
|
+
const yaml = generateDockerCompose({
|
|
414
|
+
...baseOptions,
|
|
415
|
+
services: { minio: true },
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(yaml).toContain('test: ["CMD", "mc", "ready", "local"]');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should add depends_on for minio', () => {
|
|
422
|
+
const yaml = generateDockerCompose({
|
|
423
|
+
...baseOptions,
|
|
424
|
+
services: { minio: true },
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(yaml).toContain('depends_on:');
|
|
428
|
+
expect(yaml).toContain('condition: service_healthy');
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
347
432
|
describe('multiple services', () => {
|
|
348
433
|
it('should include all services when all specified', () => {
|
|
349
434
|
const yaml = generateDockerCompose({
|
|
@@ -548,6 +633,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
548
633
|
path: 'apps/api',
|
|
549
634
|
port: 3000,
|
|
550
635
|
dependencies: [],
|
|
636
|
+
resolvedDeployTarget: 'dokploy',
|
|
551
637
|
},
|
|
552
638
|
web: {
|
|
553
639
|
type: 'frontend',
|
|
@@ -555,6 +641,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
555
641
|
port: 3001,
|
|
556
642
|
dependencies: ['api'],
|
|
557
643
|
framework: 'nextjs',
|
|
644
|
+
resolvedDeployTarget: 'dokploy',
|
|
558
645
|
},
|
|
559
646
|
},
|
|
560
647
|
services: {},
|
|
@@ -577,7 +664,12 @@ describe('generateWorkspaceCompose', () => {
|
|
|
577
664
|
const workspace = createWorkspace();
|
|
578
665
|
const yaml = generateWorkspaceCompose(workspace);
|
|
579
666
|
|
|
580
|
-
expect(yaml).toContain(
|
|
667
|
+
expect(yaml).toContain(
|
|
668
|
+
'# Use "gkm dev" or "gkm test" to start services.',
|
|
669
|
+
);
|
|
670
|
+
expect(yaml).toContain(
|
|
671
|
+
'# Running "docker compose up" directly will not inject secrets or resolve ports.',
|
|
672
|
+
);
|
|
581
673
|
});
|
|
582
674
|
|
|
583
675
|
it('should include services section', () => {
|
|
@@ -680,12 +772,14 @@ describe('generateWorkspaceCompose', () => {
|
|
|
680
772
|
path: 'apps/api',
|
|
681
773
|
port: 3000,
|
|
682
774
|
dependencies: [],
|
|
775
|
+
resolvedDeployTarget: 'dokploy',
|
|
683
776
|
},
|
|
684
777
|
auth: {
|
|
685
778
|
type: 'backend',
|
|
686
779
|
path: 'apps/auth',
|
|
687
780
|
port: 3002,
|
|
688
781
|
dependencies: [],
|
|
782
|
+
resolvedDeployTarget: 'dokploy',
|
|
689
783
|
},
|
|
690
784
|
web: {
|
|
691
785
|
type: 'frontend',
|
|
@@ -693,6 +787,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
693
787
|
port: 3001,
|
|
694
788
|
dependencies: ['api', 'auth'],
|
|
695
789
|
framework: 'nextjs',
|
|
790
|
+
resolvedDeployTarget: 'dokploy',
|
|
696
791
|
},
|
|
697
792
|
},
|
|
698
793
|
});
|
|
@@ -710,6 +805,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
710
805
|
path: 'apps/api',
|
|
711
806
|
port: 3000,
|
|
712
807
|
dependencies: [],
|
|
808
|
+
resolvedDeployTarget: 'dokploy',
|
|
713
809
|
},
|
|
714
810
|
},
|
|
715
811
|
});
|
|
@@ -729,6 +825,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
729
825
|
path: 'apps/api',
|
|
730
826
|
port: 3000,
|
|
731
827
|
dependencies: [],
|
|
828
|
+
resolvedDeployTarget: 'dokploy',
|
|
732
829
|
},
|
|
733
830
|
},
|
|
734
831
|
});
|
|
@@ -746,6 +843,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
746
843
|
port: 3001,
|
|
747
844
|
dependencies: [],
|
|
748
845
|
framework: 'nextjs',
|
|
846
|
+
resolvedDeployTarget: 'dokploy',
|
|
749
847
|
},
|
|
750
848
|
},
|
|
751
849
|
});
|
|
@@ -773,6 +871,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
773
871
|
path: 'apps/api',
|
|
774
872
|
port: 3000,
|
|
775
873
|
dependencies: [],
|
|
874
|
+
resolvedDeployTarget: 'dokploy',
|
|
776
875
|
},
|
|
777
876
|
},
|
|
778
877
|
});
|
|
@@ -889,6 +988,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
889
988
|
port: 3001,
|
|
890
989
|
dependencies: [],
|
|
891
990
|
framework: 'nextjs',
|
|
991
|
+
resolvedDeployTarget: 'dokploy',
|
|
892
992
|
},
|
|
893
993
|
},
|
|
894
994
|
services: { db: true },
|
|
@@ -926,6 +1026,55 @@ describe('generateWorkspaceCompose', () => {
|
|
|
926
1026
|
|
|
927
1027
|
expect(yaml).toContain('image: redis:6-alpine');
|
|
928
1028
|
});
|
|
1029
|
+
|
|
1030
|
+
it('should add minio service when storage is configured', () => {
|
|
1031
|
+
const workspace = createWorkspace({
|
|
1032
|
+
services: { storage: true },
|
|
1033
|
+
});
|
|
1034
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
1035
|
+
|
|
1036
|
+
expect(yaml).toContain('minio:');
|
|
1037
|
+
expect(yaml).toContain('image: minio/minio:latest');
|
|
1038
|
+
expect(yaml).toContain('container_name: test-workspace-minio');
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it('should add S3 env vars for backend apps when minio is enabled', () => {
|
|
1042
|
+
const workspace = createWorkspace({
|
|
1043
|
+
services: { storage: true },
|
|
1044
|
+
});
|
|
1045
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
1046
|
+
|
|
1047
|
+
expect(yaml).toContain('S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000}');
|
|
1048
|
+
expect(yaml).toContain('S3_FORCE_PATH_STYLE=true');
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('should add minio_data volume when minio is enabled', () => {
|
|
1052
|
+
const workspace = createWorkspace({
|
|
1053
|
+
services: { storage: true },
|
|
1054
|
+
});
|
|
1055
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
1056
|
+
|
|
1057
|
+
expect(yaml).toContain('minio_data:');
|
|
1058
|
+
expect(yaml).toContain('minio_data:/data');
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it('should add depends_on minio for backend apps', () => {
|
|
1062
|
+
const workspace = createWorkspace({
|
|
1063
|
+
services: { storage: true },
|
|
1064
|
+
});
|
|
1065
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
1066
|
+
|
|
1067
|
+
expect(yaml).toMatch(/minio:\s+condition: service_healthy/);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it('should support custom minio version', () => {
|
|
1071
|
+
const workspace = createWorkspace({
|
|
1072
|
+
services: { storage: { version: 'RELEASE.2024-01-01' } },
|
|
1073
|
+
});
|
|
1074
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
1075
|
+
|
|
1076
|
+
expect(yaml).toContain('image: minio/minio:RELEASE.2024-01-01');
|
|
1077
|
+
});
|
|
929
1078
|
});
|
|
930
1079
|
|
|
931
1080
|
describe('registry configuration', () => {
|