@geekmidas/cli 1.10.8 → 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 +6 -0
- 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 +126 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +126 -37
- 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 +6 -6
- package/src/dev/__tests__/index.spec.ts +73 -0
- package/src/dev/index.ts +42 -26
- package/src/docker/__tests__/compose.spec.ts +145 -1
- package/src/docker/compose.ts +97 -5
- 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/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/constructs": "~3.0.2",
|
|
60
|
-
"@geekmidas/envkit": "~1.0.3",
|
|
61
|
-
"@geekmidas/errors": "~1.0.0",
|
|
62
59
|
"@geekmidas/logger": "~1.0.0",
|
|
63
|
-
"@geekmidas/
|
|
60
|
+
"@geekmidas/envkit": "~1.0.3",
|
|
61
|
+
"@geekmidas/constructs": "~3.0.2",
|
|
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"
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
normalizeStudioConfig,
|
|
23
23
|
normalizeTelescopeConfig,
|
|
24
24
|
parseComposePortMappings,
|
|
25
|
+
parseComposeServiceNames,
|
|
25
26
|
replacePortInUrl,
|
|
26
27
|
rewriteUrlsWithPorts,
|
|
27
28
|
savePortState,
|
|
@@ -1707,6 +1708,78 @@ services:
|
|
|
1707
1708
|
});
|
|
1708
1709
|
});
|
|
1709
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
|
+
|
|
1710
1783
|
describe('generateServerEntryContent', () => {
|
|
1711
1784
|
it('should use dynamic import for createApp when secrets are provided', () => {
|
|
1712
1785
|
const content = generateServerEntryContent({
|
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(
|
|
@@ -1124,10 +1136,28 @@ export function buildDockerComposeEnv(
|
|
|
1124
1136
|
return { ...process.env, ...secretsEnv, ...portEnv };
|
|
1125
1137
|
}
|
|
1126
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
|
+
|
|
1127
1156
|
/**
|
|
1128
1157
|
* Start docker-compose services for the workspace.
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
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.
|
|
1131
1161
|
* @internal Exported for testing
|
|
1132
1162
|
*/
|
|
1133
1163
|
export async function startWorkspaceServices(
|
|
@@ -1135,22 +1165,17 @@ export async function startWorkspaceServices(
|
|
|
1135
1165
|
portEnv?: Record<string, string>,
|
|
1136
1166
|
secretsEnv?: Record<string, string>,
|
|
1137
1167
|
): Promise<void> {
|
|
1138
|
-
const
|
|
1139
|
-
if (!
|
|
1168
|
+
const composeFile = join(workspace.root, 'docker-compose.yml');
|
|
1169
|
+
if (!existsSync(composeFile)) {
|
|
1140
1170
|
return;
|
|
1141
1171
|
}
|
|
1142
1172
|
|
|
1143
|
-
|
|
1173
|
+
// Discover all services from docker-compose.yml
|
|
1174
|
+
const allServices = parseComposeServiceNames(composeFile);
|
|
1144
1175
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
if (services.cache) {
|
|
1149
|
-
servicesToStart.push('redis');
|
|
1150
|
-
}
|
|
1151
|
-
if (services.mail) {
|
|
1152
|
-
servicesToStart.push('mailpit');
|
|
1153
|
-
}
|
|
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));
|
|
1154
1179
|
|
|
1155
1180
|
if (servicesToStart.length === 0) {
|
|
1156
1181
|
return;
|
|
@@ -1159,15 +1184,6 @@ export async function startWorkspaceServices(
|
|
|
1159
1184
|
logger.log(`🐳 Starting services: ${servicesToStart.join(', ')}`);
|
|
1160
1185
|
|
|
1161
1186
|
try {
|
|
1162
|
-
// Check if docker-compose.yml exists
|
|
1163
|
-
const composeFile = join(workspace.root, 'docker-compose.yml');
|
|
1164
|
-
if (!existsSync(composeFile)) {
|
|
1165
|
-
logger.warn(
|
|
1166
|
-
'⚠️ No docker-compose.yml found. Services will not be started.',
|
|
1167
|
-
);
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
1187
|
// Start services with docker-compose, passing secrets so that
|
|
1172
1188
|
// POSTGRES_USER, POSTGRES_PASSWORD, etc. are interpolated correctly
|
|
1173
1189
|
execSync(`docker compose up -d ${servicesToStart.join(' ')}`, {
|
|
@@ -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: {},
|
|
@@ -685,12 +772,14 @@ describe('generateWorkspaceCompose', () => {
|
|
|
685
772
|
path: 'apps/api',
|
|
686
773
|
port: 3000,
|
|
687
774
|
dependencies: [],
|
|
775
|
+
resolvedDeployTarget: 'dokploy',
|
|
688
776
|
},
|
|
689
777
|
auth: {
|
|
690
778
|
type: 'backend',
|
|
691
779
|
path: 'apps/auth',
|
|
692
780
|
port: 3002,
|
|
693
781
|
dependencies: [],
|
|
782
|
+
resolvedDeployTarget: 'dokploy',
|
|
694
783
|
},
|
|
695
784
|
web: {
|
|
696
785
|
type: 'frontend',
|
|
@@ -698,6 +787,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
698
787
|
port: 3001,
|
|
699
788
|
dependencies: ['api', 'auth'],
|
|
700
789
|
framework: 'nextjs',
|
|
790
|
+
resolvedDeployTarget: 'dokploy',
|
|
701
791
|
},
|
|
702
792
|
},
|
|
703
793
|
});
|
|
@@ -715,6 +805,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
715
805
|
path: 'apps/api',
|
|
716
806
|
port: 3000,
|
|
717
807
|
dependencies: [],
|
|
808
|
+
resolvedDeployTarget: 'dokploy',
|
|
718
809
|
},
|
|
719
810
|
},
|
|
720
811
|
});
|
|
@@ -734,6 +825,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
734
825
|
path: 'apps/api',
|
|
735
826
|
port: 3000,
|
|
736
827
|
dependencies: [],
|
|
828
|
+
resolvedDeployTarget: 'dokploy',
|
|
737
829
|
},
|
|
738
830
|
},
|
|
739
831
|
});
|
|
@@ -751,6 +843,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
751
843
|
port: 3001,
|
|
752
844
|
dependencies: [],
|
|
753
845
|
framework: 'nextjs',
|
|
846
|
+
resolvedDeployTarget: 'dokploy',
|
|
754
847
|
},
|
|
755
848
|
},
|
|
756
849
|
});
|
|
@@ -778,6 +871,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
778
871
|
path: 'apps/api',
|
|
779
872
|
port: 3000,
|
|
780
873
|
dependencies: [],
|
|
874
|
+
resolvedDeployTarget: 'dokploy',
|
|
781
875
|
},
|
|
782
876
|
},
|
|
783
877
|
});
|
|
@@ -894,6 +988,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
894
988
|
port: 3001,
|
|
895
989
|
dependencies: [],
|
|
896
990
|
framework: 'nextjs',
|
|
991
|
+
resolvedDeployTarget: 'dokploy',
|
|
897
992
|
},
|
|
898
993
|
},
|
|
899
994
|
services: { db: true },
|
|
@@ -931,6 +1026,55 @@ describe('generateWorkspaceCompose', () => {
|
|
|
931
1026
|
|
|
932
1027
|
expect(yaml).toContain('image: redis:6-alpine');
|
|
933
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
|
+
});
|
|
934
1078
|
});
|
|
935
1079
|
|
|
936
1080
|
describe('registry configuration', () => {
|
package/src/docker/compose.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
|
|
|
13
13
|
postgres: 'postgres',
|
|
14
14
|
redis: 'redis',
|
|
15
15
|
rabbitmq: 'rabbitmq',
|
|
16
|
+
minio: 'minio/minio',
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
/** Default Docker image versions for services */
|
|
@@ -20,6 +21,7 @@ export const DEFAULT_SERVICE_VERSIONS: Record<ComposeServiceName, string> = {
|
|
|
20
21
|
postgres: '18-alpine',
|
|
21
22
|
redis: '7-alpine',
|
|
22
23
|
rabbitmq: '3-management-alpine',
|
|
24
|
+
minio: 'latest',
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export interface ComposeOptions {
|
|
@@ -121,6 +123,16 @@ services:
|
|
|
121
123
|
`;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
if (serviceMap.has('minio')) {
|
|
127
|
+
yaml += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
|
|
128
|
+
- S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
|
|
129
|
+
- S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
|
|
130
|
+
- S3_BUCKET=\${MINIO_BUCKET:-app}
|
|
131
|
+
- S3_REGION=\${S3_REGION:-eu-west-1}
|
|
132
|
+
- S3_FORCE_PATH_STYLE=true
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
yaml += ` healthcheck:
|
|
125
137
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
|
|
126
138
|
interval: 30s
|
|
@@ -210,6 +222,32 @@ services:
|
|
|
210
222
|
`;
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
const minioImage = serviceMap.get('minio');
|
|
226
|
+
if (minioImage) {
|
|
227
|
+
yaml += `
|
|
228
|
+
minio:
|
|
229
|
+
image: ${minioImage}
|
|
230
|
+
container_name: minio
|
|
231
|
+
restart: unless-stopped
|
|
232
|
+
entrypoint: sh
|
|
233
|
+
command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
|
|
234
|
+
environment:
|
|
235
|
+
MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
|
|
236
|
+
MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
|
|
237
|
+
ports:
|
|
238
|
+
- "9001:9001" # Console UI
|
|
239
|
+
volumes:
|
|
240
|
+
- minio_data:/data
|
|
241
|
+
healthcheck:
|
|
242
|
+
test: ["CMD", "mc", "ready", "local"]
|
|
243
|
+
interval: 10s
|
|
244
|
+
timeout: 5s
|
|
245
|
+
retries: 5
|
|
246
|
+
networks:
|
|
247
|
+
- app-network
|
|
248
|
+
`;
|
|
249
|
+
}
|
|
250
|
+
|
|
213
251
|
// Add volumes
|
|
214
252
|
yaml += `
|
|
215
253
|
volumes:
|
|
@@ -230,6 +268,11 @@ volumes:
|
|
|
230
268
|
`;
|
|
231
269
|
}
|
|
232
270
|
|
|
271
|
+
if (serviceMap.has('minio')) {
|
|
272
|
+
yaml += ` minio_data:
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
|
|
233
276
|
// Add networks
|
|
234
277
|
yaml += `
|
|
235
278
|
networks:
|
|
@@ -305,10 +348,12 @@ export function generateWorkspaceCompose(
|
|
|
305
348
|
const hasPostgres = services.db !== undefined && services.db !== false;
|
|
306
349
|
const hasRedis = services.cache !== undefined && services.cache !== false;
|
|
307
350
|
const hasMail = services.mail !== undefined && services.mail !== false;
|
|
351
|
+
const hasMinio = services.storage !== undefined && services.storage !== false;
|
|
308
352
|
|
|
309
353
|
// Get image versions from config
|
|
310
354
|
const postgresImage = getInfraServiceImage('postgres', services.db);
|
|
311
355
|
const redisImage = getInfraServiceImage('redis', services.cache);
|
|
356
|
+
const minioImage = getInfraServiceImage('minio', services.storage);
|
|
312
357
|
|
|
313
358
|
let yaml = `# Docker Compose for ${workspace.name} workspace
|
|
314
359
|
# Use "gkm dev" or "gkm test" to start services.
|
|
@@ -323,6 +368,7 @@ services:
|
|
|
323
368
|
registry,
|
|
324
369
|
hasPostgres,
|
|
325
370
|
hasRedis,
|
|
371
|
+
hasMinio,
|
|
326
372
|
});
|
|
327
373
|
}
|
|
328
374
|
|
|
@@ -381,6 +427,31 @@ services:
|
|
|
381
427
|
`;
|
|
382
428
|
}
|
|
383
429
|
|
|
430
|
+
if (hasMinio) {
|
|
431
|
+
yaml += `
|
|
432
|
+
minio:
|
|
433
|
+
image: ${minioImage}
|
|
434
|
+
container_name: ${workspace.name}-minio
|
|
435
|
+
restart: unless-stopped
|
|
436
|
+
entrypoint: sh
|
|
437
|
+
command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
|
|
438
|
+
environment:
|
|
439
|
+
MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
|
|
440
|
+
MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
|
|
441
|
+
ports:
|
|
442
|
+
- "9001:9001" # Console UI
|
|
443
|
+
volumes:
|
|
444
|
+
- minio_data:/data
|
|
445
|
+
healthcheck:
|
|
446
|
+
test: ["CMD", "mc", "ready", "local"]
|
|
447
|
+
interval: 10s
|
|
448
|
+
timeout: 5s
|
|
449
|
+
retries: 5
|
|
450
|
+
networks:
|
|
451
|
+
- workspace-network
|
|
452
|
+
`;
|
|
453
|
+
}
|
|
454
|
+
|
|
384
455
|
// Add volumes section
|
|
385
456
|
yaml += `
|
|
386
457
|
volumes:
|
|
@@ -396,6 +467,11 @@ volumes:
|
|
|
396
467
|
`;
|
|
397
468
|
}
|
|
398
469
|
|
|
470
|
+
if (hasMinio) {
|
|
471
|
+
yaml += ` minio_data:
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
|
|
399
475
|
// Add networks section
|
|
400
476
|
yaml += `
|
|
401
477
|
networks:
|
|
@@ -410,12 +486,13 @@ networks:
|
|
|
410
486
|
* Get infrastructure service image with version.
|
|
411
487
|
*/
|
|
412
488
|
function getInfraServiceImage(
|
|
413
|
-
serviceName: 'postgres' | 'redis',
|
|
489
|
+
serviceName: 'postgres' | 'redis' | 'minio',
|
|
414
490
|
config: boolean | { version?: string; image?: string } | undefined,
|
|
415
491
|
): string {
|
|
416
|
-
const defaults: Record<'postgres' | 'redis', string> = {
|
|
492
|
+
const defaults: Record<'postgres' | 'redis' | 'minio', string> = {
|
|
417
493
|
postgres: 'postgres:18-alpine',
|
|
418
494
|
redis: 'redis:7-alpine',
|
|
495
|
+
minio: 'minio/minio:latest',
|
|
419
496
|
};
|
|
420
497
|
|
|
421
498
|
if (!config || config === true) {
|
|
@@ -427,8 +504,12 @@ function getInfraServiceImage(
|
|
|
427
504
|
return config.image;
|
|
428
505
|
}
|
|
429
506
|
if (config.version) {
|
|
430
|
-
const
|
|
431
|
-
|
|
507
|
+
const baseImages: Record<'postgres' | 'redis' | 'minio', string> = {
|
|
508
|
+
postgres: 'postgres',
|
|
509
|
+
redis: 'redis',
|
|
510
|
+
minio: 'minio/minio',
|
|
511
|
+
};
|
|
512
|
+
return `${baseImages[serviceName]}:${config.version}`;
|
|
432
513
|
}
|
|
433
514
|
}
|
|
434
515
|
|
|
@@ -446,9 +527,10 @@ function generateAppService(
|
|
|
446
527
|
registry?: string;
|
|
447
528
|
hasPostgres: boolean;
|
|
448
529
|
hasRedis: boolean;
|
|
530
|
+
hasMinio: boolean;
|
|
449
531
|
},
|
|
450
532
|
): string {
|
|
451
|
-
const { registry, hasPostgres, hasRedis } = options;
|
|
533
|
+
const { registry, hasPostgres, hasRedis, hasMinio } = options;
|
|
452
534
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
453
535
|
|
|
454
536
|
// Health check path - frontends use /, backends use /health
|
|
@@ -490,6 +572,15 @@ function generateAppService(
|
|
|
490
572
|
}
|
|
491
573
|
if (hasRedis) {
|
|
492
574
|
yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
if (hasMinio) {
|
|
578
|
+
yaml += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
|
|
579
|
+
- S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
|
|
580
|
+
- S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
|
|
581
|
+
- S3_BUCKET=\${MINIO_BUCKET:-app}
|
|
582
|
+
- S3_REGION=\${S3_REGION:-eu-west-1}
|
|
583
|
+
- S3_FORCE_PATH_STYLE=true
|
|
493
584
|
`;
|
|
494
585
|
}
|
|
495
586
|
}
|
|
@@ -506,6 +597,7 @@ function generateAppService(
|
|
|
506
597
|
if (app.type === 'backend') {
|
|
507
598
|
if (hasPostgres) dependencies.push('postgres');
|
|
508
599
|
if (hasRedis) dependencies.push('redis');
|
|
600
|
+
if (hasMinio) dependencies.push('minio');
|
|
509
601
|
}
|
|
510
602
|
|
|
511
603
|
if (dependencies.length > 0) {
|
package/src/init/index.ts
CHANGED
|
@@ -330,6 +330,7 @@ export async function initCommand(
|
|
|
330
330
|
const secretServices: ComposeServiceName[] = [];
|
|
331
331
|
if (services.db) secretServices.push('postgres');
|
|
332
332
|
if (services.cache) secretServices.push('redis');
|
|
333
|
+
if (services.storage) secretServices.push('minio');
|
|
333
334
|
|
|
334
335
|
const devSecrets = createStageSecrets('development', secretServices, {
|
|
335
336
|
projectName: name,
|
package/src/init/versions.ts
CHANGED
|
@@ -45,7 +45,7 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
45
45
|
'@geekmidas/storage': '~2.0.0',
|
|
46
46
|
'@geekmidas/studio': '~1.0.0',
|
|
47
47
|
'@geekmidas/telescope': '~1.0.0',
|
|
48
|
-
'@geekmidas/testkit': '~1.0.
|
|
48
|
+
'@geekmidas/testkit': '~1.0.5',
|
|
49
49
|
'@geekmidas/cli': CLI_VERSION,
|
|
50
50
|
} as const;
|
|
51
51
|
|