@geekmidas/cli 1.10.15 → 1.10.17
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/{bundler-BWsVDer6.mjs → bundler-B4AackW5.mjs} +2 -2
- package/dist/{bundler-BWsVDer6.mjs.map → bundler-B4AackW5.mjs.map} +1 -1
- package/dist/{bundler-Drh5KoN5.cjs → bundler-BhhfkI9T.cjs} +2 -2
- package/dist/{bundler-Drh5KoN5.cjs.map → bundler-BhhfkI9T.cjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/{fullstack-secrets-D9rjTNyx.cjs → fullstack-secrets-DOHBU4Rp.cjs} +110 -4
- package/dist/fullstack-secrets-DOHBU4Rp.cjs.map +1 -0
- package/dist/{fullstack-secrets-BIFFv4UZ.mjs → fullstack-secrets-x2Kffx7-.mjs} +99 -5
- package/dist/fullstack-secrets-x2Kffx7-.mjs.map +1 -0
- package/dist/{index-UCsZ_Vkw.d.cts → index-BkibYzso.d.cts} +15 -4
- package/dist/index-BkibYzso.d.cts.map +1 -0
- package/dist/{index-gXAGDSGu.d.mts → index-CY-ieuRp.d.mts} +15 -4
- package/dist/index-CY-ieuRp.d.mts.map +1 -0
- package/dist/index.cjs +332 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +332 -62
- 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-DxTEausy.mjs → reconcile-BLh6rswz.mjs} +2 -2
- package/dist/{reconcile-DxTEausy.mjs.map → reconcile-BLh6rswz.mjs.map} +1 -1
- package/dist/{reconcile-LaaJkFlO.cjs → reconcile-Ch7sIcf8.cjs} +2 -2
- package/dist/{reconcile-LaaJkFlO.cjs.map → reconcile-Ch7sIcf8.cjs.map} +1 -1
- package/dist/{storage-Bu44pwPJ.cjs → storage-B1wvztiJ.cjs} +11 -1
- package/dist/{storage-clMAp4sc.mjs.map → storage-B1wvztiJ.cjs.map} +1 -1
- package/dist/{storage-CauTheT9.mjs → storage-Cs4WBsc4.mjs} +1 -1
- package/dist/{storage-DpqzcjQ5.cjs → storage-DOEtT2Hr.cjs} +1 -1
- package/dist/{storage-clMAp4sc.mjs → storage-dbb9RyBl.mjs} +11 -1
- package/dist/{storage-Bu44pwPJ.cjs.map → storage-dbb9RyBl.mjs.map} +1 -1
- package/dist/{sync-BkalF65h.mjs → sync-COnAugP-.mjs} +1 -1
- package/dist/sync-D1Pa30oV.cjs +4 -0
- package/dist/{sync-BeiI5rFC.cjs → sync-DGXXSk2v.cjs} +2 -2
- package/dist/{sync-BeiI5rFC.cjs.map → sync-DGXXSk2v.cjs.map} +1 -1
- package/dist/{sync-CWJ6tL0s.mjs → sync-D_NowTkZ.mjs} +2 -2
- package/dist/{sync-CWJ6tL0s.mjs.map → sync-D_NowTkZ.mjs.map} +1 -1
- package/dist/{types-DiV9Mbvc.d.mts → types-DdHfUbxk.d.cts} +13 -3
- package/dist/types-DdHfUbxk.d.cts.map +1 -0
- package/dist/{types-JvWj5Ckc.d.cts → types-OszPdw9m.d.mts} +13 -3
- package/dist/types-OszPdw9m.d.mts.map +1 -0
- 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 +4 -4
- package/src/dev/__tests__/entry.spec.ts +3 -5
- package/src/dev/__tests__/index.spec.ts +73 -5
- package/src/dev/index.ts +33 -25
- package/src/docker/compose.ts +130 -2
- package/src/init/__tests__/generators.spec.ts +84 -0
- package/src/init/generators/docker.ts +128 -16
- package/src/init/index.ts +26 -1
- package/src/init/templates/index.ts +28 -0
- package/src/init/versions.ts +1 -1
- package/src/secrets/__tests__/generator.spec.ts +183 -0
- package/src/secrets/generator.ts +116 -4
- package/src/secrets/storage.ts +12 -0
- package/src/secrets/types.ts +11 -1
- package/src/setup/__tests__/reconcile-secrets.spec.ts +86 -0
- package/src/setup/index.ts +64 -1
- package/src/test/__tests__/index.spec.ts +1 -4
- package/src/types.ts +13 -1
- package/src/workspace/types.ts +13 -2
- package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +0 -1
- package/dist/fullstack-secrets-D9rjTNyx.cjs.map +0 -1
- package/dist/index-UCsZ_Vkw.d.cts.map +0 -1
- package/dist/index-gXAGDSGu.d.mts.map +0 -1
- package/dist/sync-Bp8xRcuQ.cjs +0 -4
- package/dist/types-DiV9Mbvc.d.mts.map +0 -1
- package/dist/types-JvWj5Ckc.d.cts.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.17",
|
|
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/envkit": "~1.0.
|
|
59
|
+
"@geekmidas/envkit": "~1.0.4",
|
|
60
60
|
"@geekmidas/constructs": "~3.0.2",
|
|
61
|
-
"@geekmidas/schema": "~1.0.0",
|
|
62
61
|
"@geekmidas/errors": "~1.0.0",
|
|
63
|
-
"@geekmidas/logger": "~1.0.0"
|
|
62
|
+
"@geekmidas/logger": "~1.0.0",
|
|
63
|
+
"@geekmidas/schema": "~1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -97,11 +97,9 @@ describe('createEntryWrapper', () => {
|
|
|
97
97
|
|
|
98
98
|
const content = await readFile(wrapperPath, 'utf-8');
|
|
99
99
|
|
|
100
|
-
expect(content).toContain(
|
|
101
|
-
"import { Credentials } from '@geekmidas/envkit/credentials'",
|
|
102
|
-
);
|
|
100
|
+
expect(content).toContain('globalThis.__gkm_credentials__');
|
|
103
101
|
expect(content).toContain(secretsPath);
|
|
104
|
-
expect(content).toContain('Object.assign(
|
|
102
|
+
expect(content).toContain('Object.assign(process.env');
|
|
105
103
|
expect(content).toContain("await import('/path/to/entry.ts')");
|
|
106
104
|
});
|
|
107
105
|
|
|
@@ -114,7 +112,7 @@ describe('createEntryWrapper', () => {
|
|
|
114
112
|
|
|
115
113
|
const content = await readFile(wrapperPath, 'utf-8');
|
|
116
114
|
|
|
117
|
-
const credentialsIndex = content.indexOf('
|
|
115
|
+
const credentialsIndex = content.indexOf('__gkm_credentials__');
|
|
118
116
|
const importIndex = content.indexOf("await import('/path/to/entry.ts')");
|
|
119
117
|
|
|
120
118
|
expect(credentialsIndex).toBeGreaterThan(-1);
|
|
@@ -1237,6 +1237,28 @@ describe('replacePortInUrl', () => {
|
|
|
1237
1237
|
// Only the :5432 before / should be replaced
|
|
1238
1238
|
expect(result).toBe('postgresql://app:pass5432@localhost:5433/db5432');
|
|
1239
1239
|
});
|
|
1240
|
+
|
|
1241
|
+
it('should replace port followed by query string', () => {
|
|
1242
|
+
const url = 'pgboss://pgboss:pass@localhost:5432/mydb?schema=pgboss';
|
|
1243
|
+
expect(replacePortInUrl(url, 5432, 5433)).toBe(
|
|
1244
|
+
'pgboss://pgboss:pass@localhost:5433/mydb?schema=pgboss',
|
|
1245
|
+
);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it('should replace port in SNS URL with query params and no path', () => {
|
|
1249
|
+
const url = 'sns://key:secret@localhost:4566?region=us-east-1';
|
|
1250
|
+
expect(replacePortInUrl(url, 4566, 4567)).toBe(
|
|
1251
|
+
'sns://key:secret@localhost:4567?region=us-east-1',
|
|
1252
|
+
);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it('should replace URL-encoded port in query params', () => {
|
|
1256
|
+
const url =
|
|
1257
|
+
'sns://key:secret@localhost:4566?region=us-east-1&endpoint=http%3A%2F%2Flocalhost%3A4566';
|
|
1258
|
+
expect(replacePortInUrl(url, 4566, 4567)).toBe(
|
|
1259
|
+
'sns://key:secret@localhost:4567?region=us-east-1&endpoint=http%3A%2F%2Flocalhost%3A4567',
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1240
1262
|
});
|
|
1241
1263
|
|
|
1242
1264
|
describe('rewriteUrlsWithPorts', () => {
|
|
@@ -1384,6 +1406,52 @@ describe('rewriteUrlsWithPorts', () => {
|
|
|
1384
1406
|
expect(result.POSTGRES_HOST).toBe('localhost');
|
|
1385
1407
|
});
|
|
1386
1408
|
|
|
1409
|
+
it('should rewrite EVENT_PUBLISHER_CONNECTION_STRING and EVENT_SUBSCRIBER_CONNECTION_STRING', () => {
|
|
1410
|
+
const secrets = {
|
|
1411
|
+
EVENT_PUBLISHER_CONNECTION_STRING:
|
|
1412
|
+
'pgboss://pgboss:pass@localhost:5432/mydb?schema=pgboss',
|
|
1413
|
+
EVENT_SUBSCRIBER_CONNECTION_STRING:
|
|
1414
|
+
'pgboss://pgboss:pass@localhost:5432/mydb?schema=pgboss',
|
|
1415
|
+
};
|
|
1416
|
+
const result = rewriteUrlsWithPorts(secrets, {
|
|
1417
|
+
dockerEnv: { POSTGRES_HOST_PORT: '5433' },
|
|
1418
|
+
ports: { POSTGRES_HOST_PORT: 5433 },
|
|
1419
|
+
mappings: [pgMapping],
|
|
1420
|
+
});
|
|
1421
|
+
expect(result.EVENT_PUBLISHER_CONNECTION_STRING).toBe(
|
|
1422
|
+
'pgboss://pgboss:pass@localhost:5433/mydb?schema=pgboss',
|
|
1423
|
+
);
|
|
1424
|
+
expect(result.EVENT_SUBSCRIBER_CONNECTION_STRING).toBe(
|
|
1425
|
+
'pgboss://pgboss:pass@localhost:5433/mydb?schema=pgboss',
|
|
1426
|
+
);
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it('should rewrite SNS connection strings with encoded endpoint port', () => {
|
|
1430
|
+
const localstackMapping = {
|
|
1431
|
+
service: 'localstack',
|
|
1432
|
+
envVar: 'LOCALSTACK_PORT',
|
|
1433
|
+
defaultPort: 4566,
|
|
1434
|
+
containerPort: 4566,
|
|
1435
|
+
};
|
|
1436
|
+
const secrets = {
|
|
1437
|
+
EVENT_PUBLISHER_CONNECTION_STRING:
|
|
1438
|
+
'sns://LSIAkey:secret@localhost:4566?region=us-east-1&endpoint=http%3A%2F%2Flocalhost%3A4566',
|
|
1439
|
+
EVENT_SUBSCRIBER_CONNECTION_STRING:
|
|
1440
|
+
'sqs://LSIAkey:secret@localhost:4566?region=us-east-1&endpoint=http%3A%2F%2Flocalhost%3A4566',
|
|
1441
|
+
};
|
|
1442
|
+
const result = rewriteUrlsWithPorts(secrets, {
|
|
1443
|
+
dockerEnv: { LOCALSTACK_PORT: '4567' },
|
|
1444
|
+
ports: { LOCALSTACK_PORT: 4567 },
|
|
1445
|
+
mappings: [localstackMapping],
|
|
1446
|
+
});
|
|
1447
|
+
expect(result.EVENT_PUBLISHER_CONNECTION_STRING).toBe(
|
|
1448
|
+
'sns://LSIAkey:secret@localhost:4567?region=us-east-1&endpoint=http%3A%2F%2Flocalhost%3A4567',
|
|
1449
|
+
);
|
|
1450
|
+
expect(result.EVENT_SUBSCRIBER_CONNECTION_STRING).toBe(
|
|
1451
|
+
'sqs://LSIAkey:secret@localhost:4567?region=us-east-1&endpoint=http%3A%2F%2Flocalhost%3A4567',
|
|
1452
|
+
);
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1387
1455
|
it('should return empty for no mappings', () => {
|
|
1388
1456
|
const result = rewriteUrlsWithPorts(
|
|
1389
1457
|
{},
|
|
@@ -1799,23 +1867,23 @@ describe('generateServerEntryContent', () => {
|
|
|
1799
1867
|
expect(content).not.toMatch(/^import.*createApp/m);
|
|
1800
1868
|
});
|
|
1801
1869
|
|
|
1802
|
-
it('should inject
|
|
1870
|
+
it('should inject credentials via globalThis before dynamic import', () => {
|
|
1803
1871
|
const content = generateServerEntryContent({
|
|
1804
1872
|
secretsJsonPath: '/tmp/dev-secrets.json',
|
|
1805
1873
|
});
|
|
1806
1874
|
|
|
1807
|
-
const
|
|
1875
|
+
const credentialsIdx = content.indexOf('__gkm_credentials__');
|
|
1808
1876
|
const dynamicImportIdx = content.indexOf("await import('./app.js')");
|
|
1809
1877
|
|
|
1810
|
-
expect(
|
|
1878
|
+
expect(credentialsIdx).toBeGreaterThan(-1);
|
|
1811
1879
|
expect(dynamicImportIdx).toBeGreaterThan(-1);
|
|
1812
|
-
expect(
|
|
1880
|
+
expect(credentialsIdx).toBeLessThan(dynamicImportIdx);
|
|
1813
1881
|
});
|
|
1814
1882
|
|
|
1815
1883
|
it('should not include credentials injection when no secrets path', () => {
|
|
1816
1884
|
const content = generateServerEntryContent({});
|
|
1817
1885
|
|
|
1818
|
-
expect(content).not.toContain('
|
|
1886
|
+
expect(content).not.toContain('__gkm_credentials__');
|
|
1819
1887
|
expect(content).not.toContain('existsSync');
|
|
1820
1888
|
});
|
|
1821
1889
|
|
package/src/dev/index.ts
CHANGED
|
@@ -348,7 +348,17 @@ export function replacePortInUrl(
|
|
|
348
348
|
newPort: number,
|
|
349
349
|
): string {
|
|
350
350
|
if (oldPort === newPort) return url;
|
|
351
|
-
|
|
351
|
+
// Replace literal :port (in authority section)
|
|
352
|
+
let result = url.replace(
|
|
353
|
+
new RegExp(`:${oldPort}(?=[/?#]|$)`, 'g'),
|
|
354
|
+
`:${newPort}`,
|
|
355
|
+
);
|
|
356
|
+
// Replace URL-encoded :port (e.g., in query params like endpoint=http%3A%2F%2Flocalhost%3A4566)
|
|
357
|
+
result = result.replace(
|
|
358
|
+
new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, 'gi'),
|
|
359
|
+
`%3A${newPort}`,
|
|
360
|
+
);
|
|
361
|
+
return result;
|
|
352
362
|
}
|
|
353
363
|
|
|
354
364
|
/**
|
|
@@ -402,6 +412,7 @@ export function rewriteUrlsWithPorts(
|
|
|
402
412
|
if (
|
|
403
413
|
!key.endsWith('_URL') &&
|
|
404
414
|
!key.endsWith('_ENDPOINT') &&
|
|
415
|
+
!key.endsWith('_CONNECTION_STRING') &&
|
|
405
416
|
key !== 'DATABASE_URL'
|
|
406
417
|
)
|
|
407
418
|
continue;
|
|
@@ -1623,17 +1634,16 @@ export function findSecretsRoot(startDir: string): string {
|
|
|
1623
1634
|
* @internal
|
|
1624
1635
|
*/
|
|
1625
1636
|
function generateCredentialsInjection(secretsJsonPath: string): string {
|
|
1626
|
-
return `import {
|
|
1627
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1637
|
+
return `import { existsSync, readFileSync } from 'node:fs';
|
|
1628
1638
|
|
|
1629
|
-
// Inject dev secrets
|
|
1639
|
+
// Inject dev secrets via globalThis and process.env
|
|
1640
|
+
// Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
|
|
1641
|
+
// Object.assign on the Credentials export only mutates one module copy.
|
|
1630
1642
|
const secretsPath = '${secretsJsonPath}';
|
|
1631
1643
|
if (existsSync(secretsPath)) {
|
|
1632
1644
|
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1633
|
-
|
|
1645
|
+
globalThis.__gkm_credentials__ = secrets;
|
|
1634
1646
|
Object.assign(process.env, secrets);
|
|
1635
|
-
// Debug: uncomment to verify preload is running
|
|
1636
|
-
// console.log('[gkm preload] Injected', Object.keys(secrets).length, 'credentials');
|
|
1637
1647
|
}
|
|
1638
1648
|
`;
|
|
1639
1649
|
}
|
|
@@ -1958,13 +1968,14 @@ export function generateServerEntryContent(options: {
|
|
|
1958
1968
|
} = options;
|
|
1959
1969
|
|
|
1960
1970
|
const credentialsInjection = secretsJsonPath
|
|
1961
|
-
? `import {
|
|
1962
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1971
|
+
? `import { existsSync, readFileSync } from 'node:fs';
|
|
1963
1972
|
|
|
1964
|
-
// Inject dev secrets
|
|
1973
|
+
// Inject dev secrets via globalThis (must happen before app import)
|
|
1965
1974
|
const secretsPath = '${secretsJsonPath}';
|
|
1966
1975
|
if (existsSync(secretsPath)) {
|
|
1967
|
-
|
|
1976
|
+
const __secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1977
|
+
globalThis.__gkm_credentials__ = __secrets;
|
|
1978
|
+
Object.assign(process.env, __secrets);
|
|
1968
1979
|
}
|
|
1969
1980
|
|
|
1970
1981
|
`
|
|
@@ -2246,20 +2257,17 @@ export async function execCommand(
|
|
|
2246
2257
|
logger.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
2247
2258
|
}
|
|
2248
2259
|
|
|
2249
|
-
//
|
|
2250
|
-
const
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
Object.assign(credentials, rewritten);
|
|
2261
|
-
logger.log(`🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
|
|
2262
|
-
}
|
|
2260
|
+
// Resolve actual Docker ports from running containers (not just saved state)
|
|
2261
|
+
const resolvedPorts = await resolveServicePorts(secretsRoot);
|
|
2262
|
+
if (
|
|
2263
|
+
resolvedPorts.mappings.length > 0 &&
|
|
2264
|
+
Object.keys(resolvedPorts.ports).length > 0
|
|
2265
|
+
) {
|
|
2266
|
+
const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
|
|
2267
|
+
Object.assign(credentials, rewritten);
|
|
2268
|
+
logger.log(
|
|
2269
|
+
`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`,
|
|
2270
|
+
);
|
|
2263
2271
|
}
|
|
2264
2272
|
|
|
2265
2273
|
// Inject dependency URLs (works for both frontend and backend apps)
|
package/src/docker/compose.ts
CHANGED
|
@@ -15,6 +15,7 @@ export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
|
|
|
15
15
|
rabbitmq: 'rabbitmq',
|
|
16
16
|
minio: 'minio/minio',
|
|
17
17
|
mailpit: 'axllent/mailpit',
|
|
18
|
+
localstack: 'localstack/localstack',
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/** Default Docker image versions for services */
|
|
@@ -24,6 +25,7 @@ export const DEFAULT_SERVICE_VERSIONS: Record<ComposeServiceName, string> = {
|
|
|
24
25
|
rabbitmq: '3-management-alpine',
|
|
25
26
|
minio: 'latest',
|
|
26
27
|
mailpit: 'latest',
|
|
28
|
+
localstack: 'latest',
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
export interface ComposeOptions {
|
|
@@ -145,6 +147,14 @@ services:
|
|
|
145
147
|
`;
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
if (serviceMap.has('localstack')) {
|
|
151
|
+
yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
|
|
152
|
+
- AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
153
|
+
- AWS_REGION=\${AWS_REGION:-us-east-1}
|
|
154
|
+
- AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
yaml += ` healthcheck:
|
|
149
159
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
|
|
150
160
|
interval: 30s
|
|
@@ -283,6 +293,32 @@ services:
|
|
|
283
293
|
`;
|
|
284
294
|
}
|
|
285
295
|
|
|
296
|
+
const localstackImage = serviceMap.get('localstack');
|
|
297
|
+
if (localstackImage) {
|
|
298
|
+
yaml += `
|
|
299
|
+
localstack:
|
|
300
|
+
image: ${localstackImage}
|
|
301
|
+
container_name: localstack
|
|
302
|
+
restart: unless-stopped
|
|
303
|
+
environment:
|
|
304
|
+
SERVICES: sns,sqs
|
|
305
|
+
AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
|
|
306
|
+
AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
|
|
307
|
+
AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
308
|
+
ports:
|
|
309
|
+
- "\${LOCALSTACK_PORT:-4566}:4566"
|
|
310
|
+
volumes:
|
|
311
|
+
- localstack_data:/var/lib/localstack
|
|
312
|
+
healthcheck:
|
|
313
|
+
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
|
314
|
+
interval: 10s
|
|
315
|
+
timeout: 5s
|
|
316
|
+
retries: 5
|
|
317
|
+
networks:
|
|
318
|
+
- app-network
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
|
|
286
322
|
// Add volumes
|
|
287
323
|
yaml += `
|
|
288
324
|
volumes:
|
|
@@ -308,6 +344,11 @@ volumes:
|
|
|
308
344
|
`;
|
|
309
345
|
}
|
|
310
346
|
|
|
347
|
+
if (serviceMap.has('localstack')) {
|
|
348
|
+
yaml += ` localstack_data:
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
|
|
311
352
|
// Add networks
|
|
312
353
|
yaml += `
|
|
313
354
|
networks:
|
|
@@ -384,6 +425,11 @@ export function generateWorkspaceCompose(
|
|
|
384
425
|
const hasRedis = services.cache !== undefined && services.cache !== false;
|
|
385
426
|
const hasMail = services.mail !== undefined && services.mail !== false;
|
|
386
427
|
const hasMinio = services.storage !== undefined && services.storage !== false;
|
|
428
|
+
const eventsBackend = services.events;
|
|
429
|
+
const hasLocalStack = eventsBackend === 'sns';
|
|
430
|
+
const hasRabbitMQ =
|
|
431
|
+
eventsBackend === 'rabbitmq' ||
|
|
432
|
+
(services as Record<string, unknown>).rabbitmq !== undefined;
|
|
387
433
|
|
|
388
434
|
// Get image versions from config
|
|
389
435
|
const postgresImage = getInfraServiceImage('postgres', services.db);
|
|
@@ -406,6 +452,7 @@ services:
|
|
|
406
452
|
hasRedis,
|
|
407
453
|
hasMinio,
|
|
408
454
|
hasMail,
|
|
455
|
+
eventsBackend,
|
|
409
456
|
});
|
|
410
457
|
}
|
|
411
458
|
|
|
@@ -497,6 +544,55 @@ services:
|
|
|
497
544
|
`;
|
|
498
545
|
}
|
|
499
546
|
|
|
547
|
+
if (hasLocalStack) {
|
|
548
|
+
yaml += `
|
|
549
|
+
localstack:
|
|
550
|
+
image: localstack/localstack:latest
|
|
551
|
+
container_name: ${workspace.name}-localstack
|
|
552
|
+
restart: unless-stopped
|
|
553
|
+
environment:
|
|
554
|
+
SERVICES: sns,sqs
|
|
555
|
+
AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
|
|
556
|
+
AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
|
|
557
|
+
AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
558
|
+
ports:
|
|
559
|
+
- "\${LOCALSTACK_PORT:-4566}:4566"
|
|
560
|
+
volumes:
|
|
561
|
+
- localstack_data:/var/lib/localstack
|
|
562
|
+
healthcheck:
|
|
563
|
+
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
|
564
|
+
interval: 10s
|
|
565
|
+
timeout: 5s
|
|
566
|
+
retries: 5
|
|
567
|
+
networks:
|
|
568
|
+
- workspace-network
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (hasRabbitMQ) {
|
|
573
|
+
yaml += `
|
|
574
|
+
rabbitmq:
|
|
575
|
+
image: rabbitmq:3-management-alpine
|
|
576
|
+
container_name: ${workspace.name}-rabbitmq
|
|
577
|
+
restart: unless-stopped
|
|
578
|
+
environment:
|
|
579
|
+
RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
|
|
580
|
+
RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
|
|
581
|
+
ports:
|
|
582
|
+
- "\${RABBITMQ_HOST_PORT:-5672}:5672"
|
|
583
|
+
- "\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672"
|
|
584
|
+
volumes:
|
|
585
|
+
- rabbitmq_data:/var/lib/rabbitmq
|
|
586
|
+
healthcheck:
|
|
587
|
+
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
|
|
588
|
+
interval: 10s
|
|
589
|
+
timeout: 5s
|
|
590
|
+
retries: 5
|
|
591
|
+
networks:
|
|
592
|
+
- workspace-network
|
|
593
|
+
`;
|
|
594
|
+
}
|
|
595
|
+
|
|
500
596
|
// Add volumes section
|
|
501
597
|
yaml += `
|
|
502
598
|
volumes:
|
|
@@ -517,6 +613,16 @@ volumes:
|
|
|
517
613
|
`;
|
|
518
614
|
}
|
|
519
615
|
|
|
616
|
+
if (hasLocalStack) {
|
|
617
|
+
yaml += ` localstack_data:
|
|
618
|
+
`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (hasRabbitMQ) {
|
|
622
|
+
yaml += ` rabbitmq_data:
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
|
|
520
626
|
// Add networks section
|
|
521
627
|
yaml += `
|
|
522
628
|
networks:
|
|
@@ -575,10 +681,18 @@ function generateAppService(
|
|
|
575
681
|
hasRedis: boolean;
|
|
576
682
|
hasMinio: boolean;
|
|
577
683
|
hasMail: boolean;
|
|
684
|
+
eventsBackend?: import('../types').EventsBackend;
|
|
578
685
|
},
|
|
579
686
|
): string {
|
|
580
|
-
const {
|
|
581
|
-
|
|
687
|
+
const {
|
|
688
|
+
registry,
|
|
689
|
+
projectName,
|
|
690
|
+
hasPostgres,
|
|
691
|
+
hasRedis,
|
|
692
|
+
hasMinio,
|
|
693
|
+
hasMail,
|
|
694
|
+
eventsBackend,
|
|
695
|
+
} = options;
|
|
582
696
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
583
697
|
|
|
584
698
|
// Health check path - frontends use /, backends use /health
|
|
@@ -640,6 +754,18 @@ function generateAppService(
|
|
|
640
754
|
- MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
|
|
641
755
|
`;
|
|
642
756
|
}
|
|
757
|
+
if (eventsBackend) {
|
|
758
|
+
yaml += ` - EVENT_PUBLISHER_CONNECTION_STRING=\${EVENT_PUBLISHER_CONNECTION_STRING}
|
|
759
|
+
- EVENT_SUBSCRIBER_CONNECTION_STRING=\${EVENT_SUBSCRIBER_CONNECTION_STRING}
|
|
760
|
+
`;
|
|
761
|
+
if (eventsBackend === 'sns') {
|
|
762
|
+
yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
|
|
763
|
+
- AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
764
|
+
- AWS_REGION=\${AWS_REGION:-us-east-1}
|
|
765
|
+
- AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
643
769
|
}
|
|
644
770
|
|
|
645
771
|
yaml += ` healthcheck:
|
|
@@ -656,6 +782,8 @@ function generateAppService(
|
|
|
656
782
|
if (hasRedis) dependencies.push('redis');
|
|
657
783
|
if (hasMinio) dependencies.push('minio');
|
|
658
784
|
if (hasMail) dependencies.push('mailpit');
|
|
785
|
+
if (eventsBackend === 'sns') dependencies.push('localstack');
|
|
786
|
+
if (eventsBackend === 'rabbitmq') dependencies.push('rabbitmq');
|
|
659
787
|
}
|
|
660
788
|
|
|
661
789
|
if (dependencies.length > 0) {
|
|
@@ -295,6 +295,90 @@ describe('generateDockerFiles', () => {
|
|
|
295
295
|
expect(files[0].content).not.toContain('mailpit');
|
|
296
296
|
expect(files[0].content).not.toContain('minio');
|
|
297
297
|
});
|
|
298
|
+
|
|
299
|
+
it('should include localstack when events is sns', () => {
|
|
300
|
+
const options = {
|
|
301
|
+
...baseOptions,
|
|
302
|
+
services: {
|
|
303
|
+
db: true,
|
|
304
|
+
cache: true,
|
|
305
|
+
mail: false,
|
|
306
|
+
storage: false,
|
|
307
|
+
events: 'sns' as const,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const files = generateDockerFiles(options, minimalTemplate);
|
|
311
|
+
const compose = files[0].content;
|
|
312
|
+
expect(compose).toContain('localstack');
|
|
313
|
+
expect(compose).toContain('SERVICES: sns,sqs');
|
|
314
|
+
expect(compose).toContain('LOCALSTACK_PORT');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should include rabbitmq when events is rabbitmq', () => {
|
|
318
|
+
const options = {
|
|
319
|
+
...baseOptions,
|
|
320
|
+
services: {
|
|
321
|
+
db: true,
|
|
322
|
+
cache: true,
|
|
323
|
+
mail: false,
|
|
324
|
+
storage: false,
|
|
325
|
+
events: 'rabbitmq' as const,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
const files = generateDockerFiles(options, minimalTemplate);
|
|
329
|
+
const compose = files[0].content;
|
|
330
|
+
expect(compose).toContain('rabbitmq');
|
|
331
|
+
expect(compose).toContain('RABBITMQ_HOST_PORT');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should not add extra container for pgboss events', () => {
|
|
335
|
+
const options = {
|
|
336
|
+
...baseOptions,
|
|
337
|
+
services: {
|
|
338
|
+
db: true,
|
|
339
|
+
cache: true,
|
|
340
|
+
mail: false,
|
|
341
|
+
storage: false,
|
|
342
|
+
events: 'pgboss' as const,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
const files = generateDockerFiles(options, minimalTemplate);
|
|
346
|
+
const compose = files[0].content;
|
|
347
|
+
expect(compose).not.toContain('localstack');
|
|
348
|
+
// pgboss reuses postgres, no separate container
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should generate idempotent postgres init script with pgboss', () => {
|
|
352
|
+
const options = {
|
|
353
|
+
...baseOptions,
|
|
354
|
+
template: 'fullstack' as const,
|
|
355
|
+
monorepo: true,
|
|
356
|
+
apiPath: 'apps/api',
|
|
357
|
+
services: {
|
|
358
|
+
db: true,
|
|
359
|
+
cache: true,
|
|
360
|
+
mail: false,
|
|
361
|
+
storage: false,
|
|
362
|
+
events: 'pgboss' as const,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
const dbApps = [
|
|
366
|
+
{ name: 'api', password: 'api-pass' },
|
|
367
|
+
{ name: 'auth', password: 'auth-pass' },
|
|
368
|
+
];
|
|
369
|
+
const files = generateDockerFiles(options, apiTemplate, dbApps);
|
|
370
|
+
const initScript = files.find((f) => f.path === 'docker/postgres/init.sh');
|
|
371
|
+
expect(initScript).toBeDefined();
|
|
372
|
+
expect(initScript!.content).toContain('IF NOT EXISTS');
|
|
373
|
+
expect(initScript!.content).toContain('pgboss');
|
|
374
|
+
expect(initScript!.content).toContain('PGBOSS_DB_PASSWORD');
|
|
375
|
+
expect(initScript!.content).toContain('CREATE SCHEMA IF NOT EXISTS pgboss');
|
|
376
|
+
|
|
377
|
+
// Docker env should include pgboss password
|
|
378
|
+
const envFile = files.find((f) => f.path === 'docker/.env');
|
|
379
|
+
expect(envFile).toBeDefined();
|
|
380
|
+
expect(envFile!.content).toContain('PGBOSS_DB_PASSWORD');
|
|
381
|
+
});
|
|
298
382
|
});
|
|
299
383
|
|
|
300
384
|
describe('generateMonorepoFiles', () => {
|