@geekmidas/cli 1.10.10 → 1.10.12

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{bundler-Bm3Az_sv.cjs → bundler-DVJkwNMQ.cjs} +2 -2
  3. package/dist/{bundler-Bm3Az_sv.cjs.map → bundler-DVJkwNMQ.cjs.map} +1 -1
  4. package/dist/{bundler-kk_XJTRp.mjs → bundler-Di5Gz9Ou.mjs} +2 -2
  5. package/dist/{bundler-kk_XJTRp.mjs.map → bundler-Di5Gz9Ou.mjs.map} +1 -1
  6. package/dist/config.d.cts +2 -2
  7. package/dist/config.d.mts +2 -2
  8. package/dist/{fullstack-secrets-DmUOfLeX.mjs → fullstack-secrets-BIFFv4UZ.mjs} +18 -3
  9. package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +1 -0
  10. package/dist/{fullstack-secrets-BC9t9wWB.cjs → fullstack-secrets-D9rjTNyx.cjs} +18 -3
  11. package/dist/fullstack-secrets-D9rjTNyx.cjs.map +1 -0
  12. package/dist/{index-BdJZKXCJ.d.cts → index-UCsZ_Vkw.d.cts} +2 -2
  13. package/dist/{index-BdJZKXCJ.d.cts.map → index-UCsZ_Vkw.d.cts.map} +1 -1
  14. package/dist/{index-DB9VbcCD.d.mts → index-gXAGDSGu.d.mts} +2 -2
  15. package/dist/{index-DB9VbcCD.d.mts.map → index-gXAGDSGu.d.mts.map} +1 -1
  16. package/dist/index.cjs +120 -54
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +120 -54
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/openapi-BYxAWwok.cjs.map +1 -1
  21. package/dist/openapi-DenF-okj.mjs.map +1 -1
  22. package/dist/openapi.d.cts +1 -1
  23. package/dist/openapi.d.mts +1 -1
  24. package/dist/{reconcile-C0dsg-Gq.mjs → reconcile-DxTEausy.mjs} +2 -2
  25. package/dist/{reconcile-C0dsg-Gq.mjs.map → reconcile-DxTEausy.mjs.map} +1 -1
  26. package/dist/{reconcile-BZ8j_-0z.cjs → reconcile-LaaJkFlO.cjs} +2 -2
  27. package/dist/{reconcile-BZ8j_-0z.cjs.map → reconcile-LaaJkFlO.cjs.map} +1 -1
  28. package/dist/{storage-Cs13jkJ9.cjs → storage-6GBoLCYF.cjs} +12 -6
  29. package/dist/{storage-B7H2PPCS.mjs.map → storage-6GBoLCYF.cjs.map} +1 -1
  30. package/dist/{storage-C1FNm2EP.mjs → storage-BFqrVsip.mjs} +1 -1
  31. package/dist/{storage-D6BGLgWf.cjs → storage-DCqjCiDn.cjs} +1 -1
  32. package/dist/{storage-B7H2PPCS.mjs → storage-DMf420PP.mjs} +12 -6
  33. package/dist/{storage-Cs13jkJ9.cjs.map → storage-DMf420PP.mjs.map} +1 -1
  34. package/dist/sync-BVNso6AA.cjs +4 -0
  35. package/dist/{sync-oCqELfeA.cjs → sync-DIGGOxCw.cjs} +2 -2
  36. package/dist/{sync-oCqELfeA.cjs.map → sync-DIGGOxCw.cjs.map} +1 -1
  37. package/dist/{sync-CyGe5f1I.mjs → sync-DjD_TeNX.mjs} +1 -1
  38. package/dist/{sync-CzXruMzP.mjs → sync-Do9O7QZ8.mjs} +2 -2
  39. package/dist/{sync-CzXruMzP.mjs.map → sync-Do9O7QZ8.mjs.map} +1 -1
  40. package/dist/{types-DwpLq_fp.d.mts → types-DiV9Mbvc.d.mts} +2 -2
  41. package/dist/{types-D4MLWXSL.d.cts.map → types-DiV9Mbvc.d.mts.map} +1 -1
  42. package/dist/{types-D4MLWXSL.d.cts → types-JvWj5Ckc.d.cts} +2 -2
  43. package/dist/{types-DwpLq_fp.d.mts.map → types-JvWj5Ckc.d.cts.map} +1 -1
  44. package/dist/workspace/index.d.cts +2 -2
  45. package/dist/workspace/index.d.mts +2 -2
  46. package/package.json +2 -2
  47. package/src/dev/index.ts +41 -1
  48. package/src/docker/__tests__/compose.spec.ts +48 -13
  49. package/src/docker/compose.ts +77 -23
  50. package/src/init/__tests__/generators.spec.ts +1 -0
  51. package/src/init/generators/docker.ts +1 -2
  52. package/src/init/index.ts +1 -0
  53. package/src/init/versions.ts +1 -1
  54. package/src/secrets/__tests__/generator.spec.ts +5 -5
  55. package/src/secrets/__tests__/storage.spec.ts +7 -7
  56. package/src/secrets/generator.ts +20 -4
  57. package/src/secrets/index.ts +4 -4
  58. package/src/secrets/storage.ts +11 -5
  59. package/src/secrets/types.ts +4 -1
  60. package/src/setup/__tests__/reconcile-secrets.spec.ts +3 -3
  61. package/src/setup/index.ts +5 -0
  62. package/src/test/index.ts +8 -10
  63. package/src/types.ts +6 -1
  64. package/dist/fullstack-secrets-BC9t9wWB.cjs.map +0 -1
  65. package/dist/fullstack-secrets-DmUOfLeX.mjs.map +0 -1
  66. package/dist/sync-DLlwsrBs.cjs +0 -4
@@ -352,12 +352,18 @@ describe('generateDockerCompose', () => {
352
352
  services: { minio: true },
353
353
  });
354
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');
355
+ expect(yaml).toContain(
356
+ '- STORAGE_ENDPOINT=${STORAGE_ENDPOINT:-http://minio:9000}',
357
+ );
358
+ expect(yaml).toContain(
359
+ '- STORAGE_ACCESS_KEY_ID=${STORAGE_ACCESS_KEY_ID:-my-api}',
360
+ );
361
+ expect(yaml).toContain(
362
+ '- STORAGE_SECRET_ACCESS_KEY=${STORAGE_SECRET_ACCESS_KEY:-my-api}',
363
+ );
364
+ expect(yaml).toContain('- STORAGE_BUCKET=${STORAGE_BUCKET:-my-api}');
365
+ expect(yaml).toContain('- STORAGE_REGION=${STORAGE_REGION:-eu-west-1}');
366
+ expect(yaml).toContain('- STORAGE_FORCE_PATH_STYLE=true');
361
367
  });
362
368
 
363
369
  it('should add minio service definition with default image', () => {
@@ -386,8 +392,12 @@ describe('generateDockerCompose', () => {
386
392
  services: { minio: true },
387
393
  });
388
394
 
389
- expect(yaml).toContain('MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-app}');
390
- expect(yaml).toContain('MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-app}');
395
+ expect(yaml).toContain(
396
+ 'MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY_ID:-my-api}',
397
+ );
398
+ expect(yaml).toContain(
399
+ 'MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_ACCESS_KEY:-my-api}',
400
+ );
391
401
  });
392
402
 
393
403
  it('should expose console UI port', () => {
@@ -396,7 +406,8 @@ describe('generateDockerCompose', () => {
396
406
  services: { minio: true },
397
407
  });
398
408
 
399
- expect(yaml).toContain('- "9001:9001"');
409
+ expect(yaml).toContain('- "${MINIO_API_PORT:-9000}:9000"');
410
+ expect(yaml).toContain('- "${MINIO_CONSOLE_PORT:-9001}:9001"');
400
411
  });
401
412
 
402
413
  it('should add minio volume', () => {
@@ -934,8 +945,30 @@ describe('generateWorkspaceCompose', () => {
934
945
 
935
946
  expect(yaml).toContain('mailpit:');
936
947
  expect(yaml).toContain('image: axllent/mailpit:latest');
937
- expect(yaml).toContain('- "8025:8025"'); // Web UI
938
- expect(yaml).toContain('- "1025:1025"'); // SMTP
948
+ expect(yaml).toContain('MP_SMTP_AUTH:');
949
+ expect(yaml).toContain('${MAILPIT_UI_PORT:-8025}:8025'); // Web UI
950
+ expect(yaml).toContain('${MAILPIT_SMTP_PORT:-1025}:1025'); // SMTP
951
+ });
952
+
953
+ it('should add SMTP env vars for backend apps when mail is enabled', () => {
954
+ const workspace = createWorkspace({
955
+ services: { mail: true },
956
+ });
957
+ const yaml = generateWorkspaceCompose(workspace);
958
+
959
+ expect(yaml).toContain('SMTP_HOST=${SMTP_HOST:-mailpit}');
960
+ expect(yaml).toContain('SMTP_PORT=${SMTP_PORT:-1025}');
961
+ expect(yaml).toContain('SMTP_USER=');
962
+ expect(yaml).toContain('SMTP_PASS=');
963
+ });
964
+
965
+ it('should add mailpit to depends_on for backend apps', () => {
966
+ const workspace = createWorkspace({
967
+ services: { mail: true },
968
+ });
969
+ const yaml = generateWorkspaceCompose(workspace);
970
+
971
+ expect(yaml).toMatch(/mailpit:\s+condition: service_healthy/);
939
972
  });
940
973
 
941
974
  it('should add postgres_data volume when postgres is enabled', () => {
@@ -1044,8 +1077,10 @@ describe('generateWorkspaceCompose', () => {
1044
1077
  });
1045
1078
  const yaml = generateWorkspaceCompose(workspace);
1046
1079
 
1047
- expect(yaml).toContain('S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000}');
1048
- expect(yaml).toContain('S3_FORCE_PATH_STYLE=true');
1080
+ expect(yaml).toContain(
1081
+ 'STORAGE_ENDPOINT=${STORAGE_ENDPOINT:-http://minio:9000}',
1082
+ );
1083
+ expect(yaml).toContain('STORAGE_FORCE_PATH_STYLE=true');
1049
1084
  });
1050
1085
 
1051
1086
  it('should add minio_data volume when minio is enabled', () => {
@@ -14,6 +14,7 @@ export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
14
14
  redis: 'redis',
15
15
  rabbitmq: 'rabbitmq',
16
16
  minio: 'minio/minio',
17
+ mailpit: 'axllent/mailpit',
17
18
  };
18
19
 
19
20
  /** Default Docker image versions for services */
@@ -22,6 +23,7 @@ export const DEFAULT_SERVICE_VERSIONS: Record<ComposeServiceName, string> = {
22
23
  redis: '7-alpine',
23
24
  rabbitmq: '3-management-alpine',
24
25
  minio: 'latest',
26
+ mailpit: 'latest',
25
27
  };
26
28
 
27
29
  export interface ComposeOptions {
@@ -124,12 +126,20 @@ services:
124
126
  }
125
127
 
126
128
  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
129
+ yaml += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
130
+ - STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${imageName}}
131
+ - STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
132
+ - STORAGE_BUCKET=\${STORAGE_BUCKET:-${imageName}}
133
+ - STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
134
+ - STORAGE_FORCE_PATH_STYLE=true
135
+ `;
136
+ }
137
+
138
+ if (serviceMap.has('mailpit')) {
139
+ yaml += ` - SMTP_HOST=\${SMTP_HOST:-mailpit}
140
+ - SMTP_PORT=\${SMTP_PORT:-1025}
141
+ - SMTP_USER=\${SMTP_USER:-${imageName}}
142
+ - SMTP_PASS=\${SMTP_PASS:-${imageName}}
133
143
  `;
134
144
  }
135
145
 
@@ -230,12 +240,13 @@ services:
230
240
  container_name: minio
231
241
  restart: unless-stopped
232
242
  entrypoint: sh
233
- command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
243
+ command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${imageName}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
234
244
  environment:
235
- MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
236
- MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
245
+ MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${imageName}}
246
+ MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
237
247
  ports:
238
- - "9001:9001" # Console UI
248
+ - "\${MINIO_API_PORT:-9000}:9000"
249
+ - "\${MINIO_CONSOLE_PORT:-9001}:9001"
239
250
  volumes:
240
251
  - minio_data:/data
241
252
  healthcheck:
@@ -248,6 +259,28 @@ services:
248
259
  `;
249
260
  }
250
261
 
262
+ const mailpitImage = serviceMap.get('mailpit');
263
+ if (mailpitImage) {
264
+ yaml += `
265
+ mailpit:
266
+ image: ${mailpitImage}
267
+ container_name: mailpit
268
+ restart: unless-stopped
269
+ environment:
270
+ MP_SMTP_AUTH: \${SMTP_USER:-${imageName}}:\${SMTP_PASS:-${imageName}}
271
+ ports:
272
+ - "\${MAILPIT_UI_PORT:-8025}:8025" # Web UI
273
+ - "\${MAILPIT_SMTP_PORT:-1025}:1025" # SMTP
274
+ healthcheck:
275
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
276
+ interval: 5s
277
+ timeout: 5s
278
+ retries: 5
279
+ networks:
280
+ - app-network
281
+ `;
282
+ }
283
+
251
284
  // Add volumes
252
285
  yaml += `
253
286
  volumes:
@@ -366,9 +399,11 @@ services:
366
399
  for (const [appName, app] of apps) {
367
400
  yaml += generateAppService(appName, app, apps, {
368
401
  registry,
402
+ projectName: workspace.name,
369
403
  hasPostgres,
370
404
  hasRedis,
371
405
  hasMinio,
406
+ hasMail,
372
407
  });
373
408
  }
374
409
 
@@ -419,9 +454,16 @@ services:
419
454
  image: axllent/mailpit:latest
420
455
  container_name: ${workspace.name}-mailpit
421
456
  restart: unless-stopped
457
+ environment:
458
+ MP_SMTP_AUTH: \${SMTP_USER:-${workspace.name}}:\${SMTP_PASS:-${workspace.name}}
422
459
  ports:
423
- - "8025:8025" # Web UI
424
- - "1025:1025" # SMTP
460
+ - "\${MAILPIT_UI_PORT:-8025}:8025" # Web UI
461
+ - "\${MAILPIT_SMTP_PORT:-1025}:1025" # SMTP
462
+ healthcheck:
463
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
464
+ interval: 5s
465
+ timeout: 5s
466
+ retries: 5
425
467
  networks:
426
468
  - workspace-network
427
469
  `;
@@ -434,12 +476,13 @@ services:
434
476
  container_name: ${workspace.name}-minio
435
477
  restart: unless-stopped
436
478
  entrypoint: sh
437
- command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
479
+ command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${workspace.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
438
480
  environment:
439
- MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
440
- MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
481
+ MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${workspace.name}}
482
+ MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${workspace.name}}
441
483
  ports:
442
- - "9001:9001" # Console UI
484
+ - "\${MINIO_API_PORT:-9000}:9000"
485
+ - "\${MINIO_CONSOLE_PORT:-9001}:9001"
443
486
  volumes:
444
487
  - minio_data:/data
445
488
  healthcheck:
@@ -525,12 +568,15 @@ function generateAppService(
525
568
  allApps: [string, NormalizedAppConfig][],
526
569
  options: {
527
570
  registry?: string;
571
+ projectName: string;
528
572
  hasPostgres: boolean;
529
573
  hasRedis: boolean;
530
574
  hasMinio: boolean;
575
+ hasMail: boolean;
531
576
  },
532
577
  ): string {
533
- const { registry, hasPostgres, hasRedis, hasMinio } = options;
578
+ const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail } =
579
+ options;
534
580
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
535
581
 
536
582
  // Health check path - frontends use /, backends use /health
@@ -575,12 +621,19 @@ function generateAppService(
575
621
  `;
576
622
  }
577
623
  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
624
+ yaml += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
625
+ - STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${projectName}}
626
+ - STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${projectName}}
627
+ - STORAGE_BUCKET=\${STORAGE_BUCKET:-${projectName}}
628
+ - STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
629
+ - STORAGE_FORCE_PATH_STYLE=true
630
+ `;
631
+ }
632
+ if (hasMail) {
633
+ yaml += ` - SMTP_HOST=\${SMTP_HOST:-mailpit}
634
+ - SMTP_PORT=\${SMTP_PORT:-1025}
635
+ - SMTP_USER=\${SMTP_USER:-${projectName}}
636
+ - SMTP_PASS=\${SMTP_PASS:-${projectName}}
584
637
  `;
585
638
  }
586
639
  }
@@ -598,6 +651,7 @@ function generateAppService(
598
651
  if (hasPostgres) dependencies.push('postgres');
599
652
  if (hasRedis) dependencies.push('redis');
600
653
  if (hasMinio) dependencies.push('minio');
654
+ if (hasMail) dependencies.push('mailpit');
601
655
  }
602
656
 
603
657
  if (dependencies.length > 0) {
@@ -253,6 +253,7 @@ describe('generateDockerFiles', () => {
253
253
  "'${MAILPIT_SMTP_HOST_PORT:-1025}:1025'",
254
254
  );
255
255
  expect(files[0].content).toContain("'${MAILPIT_UI_HOST_PORT:-8025}:8025'");
256
+ expect(files[0].content).toContain('MP_SMTP_AUTH:');
256
257
  });
257
258
  });
258
259
 
@@ -156,8 +156,7 @@ export function generateDockerFiles(
156
156
  - '\${MAILPIT_SMTP_HOST_PORT:-1025}:1025'
157
157
  - '\${MAILPIT_UI_HOST_PORT:-8025}:8025'
158
158
  environment:
159
- MP_SMTP_AUTH_ACCEPT_ANY: 1
160
- MP_SMTP_AUTH_ALLOW_INSECURE: 1`);
159
+ MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
161
160
  }
162
161
 
163
162
  // Build docker-compose.yml
package/src/init/index.ts CHANGED
@@ -331,6 +331,7 @@ export async function initCommand(
331
331
  if (services.db) secretServices.push('postgres');
332
332
  if (services.cache) secretServices.push('redis');
333
333
  if (services.storage) secretServices.push('minio');
334
+ if (services.mail) secretServices.push('mailpit');
334
335
 
335
336
  const devSecrets = createStageSecrets('development', secretServices, {
336
337
  projectName: name,
@@ -42,7 +42,7 @@ export const GEEKMIDAS_VERSIONS = {
42
42
  '@geekmidas/rate-limit': '~2.0.0',
43
43
  '@geekmidas/schema': '~1.0.0',
44
44
  '@geekmidas/services': '~1.0.1',
45
- '@geekmidas/storage': '~2.0.0',
45
+ '@geekmidas/storage': '~2.0.1',
46
46
  '@geekmidas/studio': '~1.0.0',
47
47
  '@geekmidas/telescope': '~1.0.0',
48
48
  '@geekmidas/testkit': '~1.0.5',
@@ -259,10 +259,10 @@ describe('generateConnectionUrls', () => {
259
259
  expect(urls.DATABASE_URL).toBeDefined();
260
260
  expect(urls.REDIS_URL).toBeDefined();
261
261
  expect(urls.RABBITMQ_URL).toBeDefined();
262
- expect(urls.S3_ENDPOINT).toBe('http://minio:9000');
262
+ expect(urls.STORAGE_ENDPOINT).toBe('http://minio:9000');
263
263
  });
264
264
 
265
- it('should generate S3_ENDPOINT for minio', () => {
265
+ it('should generate STORAGE_ENDPOINT for minio', () => {
266
266
  const urls = generateConnectionUrls({
267
267
  minio: {
268
268
  host: 'localhost',
@@ -273,7 +273,7 @@ describe('generateConnectionUrls', () => {
273
273
  },
274
274
  });
275
275
 
276
- expect(urls.S3_ENDPOINT).toBe('http://localhost:9000');
276
+ expect(urls.STORAGE_ENDPOINT).toBe('http://localhost:9000');
277
277
  expect(urls.DATABASE_URL).toBeUndefined();
278
278
  });
279
279
  });
@@ -308,11 +308,11 @@ describe('createStageSecrets', () => {
308
308
  expect(secrets.urls.RABBITMQ_URL).toBeDefined();
309
309
  });
310
310
 
311
- it('should generate S3_ENDPOINT for minio', () => {
311
+ it('should generate STORAGE_ENDPOINT for minio', () => {
312
312
  const secrets = createStageSecrets('production', ['minio']);
313
313
 
314
314
  expect(secrets.services.minio).toBeDefined();
315
- expect(secrets.urls.S3_ENDPOINT).toBe('http://localhost:9000');
315
+ expect(secrets.urls.STORAGE_ENDPOINT).toBe('http://localhost:9000');
316
316
  });
317
317
  });
318
318
 
@@ -353,19 +353,19 @@ describe('toEmbeddableSecrets', () => {
353
353
  },
354
354
  },
355
355
  urls: {
356
- S3_ENDPOINT: 'http://localhost:9000',
356
+ STORAGE_ENDPOINT: 'http://localhost:9000',
357
357
  },
358
358
  custom: {},
359
359
  };
360
360
 
361
361
  const embeddable = toEmbeddableSecrets(secrets);
362
362
 
363
- expect(embeddable.S3_ENDPOINT).toBe('http://localhost:9000');
364
- expect(embeddable.S3_ACCESS_KEY_ID).toBe('myaccesskey');
365
- expect(embeddable.S3_SECRET_ACCESS_KEY).toBe('mysecretkey');
366
- expect(embeddable.S3_BUCKET).toBe('my-bucket');
367
- expect(embeddable.S3_REGION).toBe('eu-west-1');
368
- expect(embeddable.S3_FORCE_PATH_STYLE).toBe('true');
363
+ expect(embeddable.STORAGE_ENDPOINT).toBe('http://localhost:9000');
364
+ expect(embeddable.STORAGE_ACCESS_KEY_ID).toBe('myaccesskey');
365
+ expect(embeddable.STORAGE_SECRET_ACCESS_KEY).toBe('mysecretkey');
366
+ expect(embeddable.STORAGE_BUCKET).toBe('my-bucket');
367
+ expect(embeddable.STORAGE_REGION).toBe('eu-west-1');
368
+ expect(embeddable.STORAGE_FORCE_PATH_STYLE).toBe('true');
369
369
  });
370
370
 
371
371
  it('should handle all services and custom secrets together', () => {
@@ -40,6 +40,11 @@ const SERVICE_DEFAULTS: Record<
40
40
  username: 'app',
41
41
  bucket: 'app',
42
42
  },
43
+ mailpit: {
44
+ host: 'localhost',
45
+ port: 1025,
46
+ username: 'app',
47
+ },
43
48
  };
44
49
 
45
50
  /**
@@ -124,7 +129,12 @@ export function generateConnectionUrls(
124
129
  }
125
130
 
126
131
  if (services.minio) {
127
- urls.S3_ENDPOINT = generateMinioEndpoint(services.minio);
132
+ urls.STORAGE_ENDPOINT = generateMinioEndpoint(services.minio);
133
+ }
134
+
135
+ if (services.mailpit) {
136
+ urls.SMTP_HOST = services.mailpit.host;
137
+ urls.SMTP_PORT = String(services.mailpit.port);
128
138
  }
129
139
 
130
140
  return urls;
@@ -145,9 +155,15 @@ export function createStageSecrets(
145
155
  const now = new Date().toISOString();
146
156
  const serviceCredentials = generateServicesCredentials(services);
147
157
 
148
- // Override postgres database name with project-derived name if provided
149
- if (options?.projectName && serviceCredentials.postgres) {
150
- serviceCredentials.postgres.database = `${options.projectName.replace(/-/g, '_')}_dev`;
158
+ // Override service defaults with project-derived names if provided
159
+ if (options?.projectName) {
160
+ if (serviceCredentials.postgres) {
161
+ serviceCredentials.postgres.database = `${options.projectName.replace(/-/g, '_')}_dev`;
162
+ }
163
+ if (serviceCredentials.minio) {
164
+ serviceCredentials.minio.bucket = options.projectName;
165
+ serviceCredentials.minio.username = options.projectName;
166
+ }
151
167
  }
152
168
 
153
169
  const urls = generateConnectionUrls(serviceCredentials);
@@ -129,8 +129,8 @@ export async function secretsInitCommand(
129
129
  if (secrets.urls.RABBITMQ_URL) {
130
130
  logger.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
131
131
  }
132
- if (secrets.urls.S3_ENDPOINT) {
133
- logger.log(` S3_ENDPOINT: ${secrets.urls.S3_ENDPOINT}`);
132
+ if (secrets.urls.STORAGE_ENDPOINT) {
133
+ logger.log(` STORAGE_ENDPOINT: ${secrets.urls.STORAGE_ENDPOINT}`);
134
134
  }
135
135
 
136
136
  if (Object.keys(secrets.custom).length > 0) {
@@ -260,8 +260,8 @@ export async function secretsShowCommand(
260
260
  ` RABBITMQ_URL: ${reveal ? secrets.urls.RABBITMQ_URL : maskUrl(secrets.urls.RABBITMQ_URL)}`,
261
261
  );
262
262
  }
263
- if (secrets.urls.S3_ENDPOINT) {
264
- logger.log(` S3_ENDPOINT: ${secrets.urls.S3_ENDPOINT}`);
263
+ if (secrets.urls.STORAGE_ENDPOINT) {
264
+ logger.log(` STORAGE_ENDPOINT: ${secrets.urls.STORAGE_ENDPOINT}`);
265
265
  }
266
266
 
267
267
  // Show custom secrets
@@ -209,11 +209,17 @@ export function toEmbeddableSecrets(secrets: StageSecrets): EmbeddableSecrets {
209
209
  RABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? '/',
210
210
  }),
211
211
  ...(secrets.services.minio && {
212
- S3_ACCESS_KEY_ID: secrets.services.minio.username,
213
- S3_SECRET_ACCESS_KEY: secrets.services.minio.password,
214
- S3_BUCKET: secrets.services.minio.bucket ?? 'app',
215
- S3_REGION: 'eu-west-1',
216
- S3_FORCE_PATH_STYLE: 'true',
212
+ STORAGE_ACCESS_KEY_ID: secrets.services.minio.username,
213
+ STORAGE_SECRET_ACCESS_KEY: secrets.services.minio.password,
214
+ STORAGE_BUCKET: secrets.services.minio.bucket ?? 'app',
215
+ STORAGE_REGION: 'eu-west-1',
216
+ STORAGE_FORCE_PATH_STYLE: 'true',
217
+ }),
218
+ ...(secrets.services.mailpit && {
219
+ SMTP_HOST: secrets.services.mailpit.host,
220
+ SMTP_PORT: String(secrets.services.mailpit.port),
221
+ SMTP_USER: secrets.services.mailpit.username,
222
+ SMTP_PASS: secrets.services.mailpit.password,
217
223
  }),
218
224
  };
219
225
  }
@@ -28,13 +28,16 @@ export interface StageSecrets {
28
28
  redis?: ServiceCredentials;
29
29
  rabbitmq?: ServiceCredentials;
30
30
  minio?: ServiceCredentials;
31
+ mailpit?: ServiceCredentials;
31
32
  };
32
33
  /** Generated connection URLs */
33
34
  urls: {
34
35
  DATABASE_URL?: string;
35
36
  REDIS_URL?: string;
36
37
  RABBITMQ_URL?: string;
37
- S3_ENDPOINT?: string;
38
+ STORAGE_ENDPOINT?: string;
39
+ SMTP_HOST?: string;
40
+ SMTP_PORT?: string;
38
41
  };
39
42
  /** Custom user-defined secrets */
40
43
  custom: Record<string, string>;
@@ -151,9 +151,9 @@ describe('reconcileSecrets', () => {
151
151
  expect(result!.services.minio).toBeDefined();
152
152
  expect(result!.services.minio!.host).toBe('localhost');
153
153
  expect(result!.services.minio!.port).toBe(9000);
154
- expect(result!.services.minio!.bucket).toBe('app');
154
+ expect(result!.services.minio!.bucket).toBe('test-project');
155
155
  expect(result!.services.minio!.password).toHaveLength(32);
156
- expect(result!.urls.S3_ENDPOINT).toBe('http://localhost:9000');
156
+ expect(result!.urls.STORAGE_ENDPOINT).toBe('http://localhost:9000');
157
157
  // Existing postgres should be preserved
158
158
  expect(result!.services.postgres).toEqual(secrets.services.postgres);
159
159
  });
@@ -175,7 +175,7 @@ describe('reconcileSecrets', () => {
175
175
  password: 'mysecret',
176
176
  bucket: 'my-bucket',
177
177
  };
178
- secrets.urls.S3_ENDPOINT = 'http://localhost:9000';
178
+ secrets.urls.STORAGE_ENDPOINT = 'http://localhost:9000';
179
179
 
180
180
  const result = reconcileSecrets(secrets, workspace);
181
181
 
@@ -173,6 +173,11 @@ export function reconcileSecrets(
173
173
  for (const { key, name } of serviceMap) {
174
174
  if (workspace.services[key] && !result.services[name]) {
175
175
  const creds = generateServiceCredentials(name);
176
+ // Override defaults with project-derived names
177
+ if (name === 'minio') {
178
+ creds.bucket = workspace.name;
179
+ creds.username = workspace.name;
180
+ }
176
181
  result = {
177
182
  ...result,
178
183
  services: { ...result.services, [name]: creds },
package/src/test/index.ts CHANGED
@@ -6,10 +6,10 @@ import { sniffAppEnvironment } from '../deploy/sniffer';
6
6
  import {
7
7
  createCredentialsPreload,
8
8
  loadEnvFiles,
9
- loadPortState,
10
9
  parseComposePortMappings,
11
10
  resolveServicePorts,
12
11
  rewriteUrlsWithPorts,
12
+ startComposeServices,
13
13
  startWorkspaceServices,
14
14
  } from '../dev/index';
15
15
  import { readStageSecrets, toEmbeddableSecrets } from '../secrets/storage';
@@ -121,19 +121,17 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
121
121
  );
122
122
  }
123
123
  } catch {
124
- // Not in a workspace — fall back to port state file
124
+ // Not in a workspace — start Docker services from local docker-compose.yml
125
125
  const composePath = join(cwd, 'docker-compose.yml');
126
126
  const mappings = parseComposePortMappings(composePath);
127
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
- });
128
+ const resolvedPorts = await resolveServicePorts(cwd);
129
+ await startComposeServices(cwd, resolvedPorts.dockerEnv, secretsEnv);
130
+
131
+ if (resolvedPorts.mappings.length > 0) {
132
+ secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
135
133
  console.log(
136
- ` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`,
134
+ ` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`,
137
135
  );
138
136
  }
139
137
  }
package/src/types.ts CHANGED
@@ -78,7 +78,12 @@ export interface ServiceConfig {
78
78
  }
79
79
 
80
80
  /** Supported docker-compose service names */
81
- export type ComposeServiceName = 'postgres' | 'redis' | 'rabbitmq' | 'minio';
81
+ export type ComposeServiceName =
82
+ | 'postgres'
83
+ | 'redis'
84
+ | 'rabbitmq'
85
+ | 'minio'
86
+ | 'mailpit';
82
87
 
83
88
  /** Services configuration - can be boolean (use defaults) or object with version */
84
89
  export type ComposeServicesConfig = {
@@ -1 +0,0 @@
1
- {"version":3,"file":"fullstack-secrets-BC9t9wWB.cjs","names":["SERVICE_DEFAULTS: Record<\n\tComposeServiceName,\n\tOmit<ServiceCredentials, 'password'>\n>","service: ComposeServiceName","services: ComposeServiceName[]","result: StageSecrets['services']","creds: ServiceCredentials","services: StageSecrets['services']","urls: StageSecrets['urls']","stage: string","options?: { projectName?: string }","secrets: StageSecrets","newCreds: ServiceCredentials","appName: string","password: string","projectName: string","workspace: NormalizedWorkspace","customs: Record<string, string>","frontendPorts: number[]","upperName","secrets: StageSecrets","workspaceRoot: string"],"sources":["../src/secrets/generator.ts","../src/setup/fullstack-secrets.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport type { ComposeServiceName } from '../types';\nimport type { ServiceCredentials, StageSecrets } from './types';\n\n/**\n * Generate a secure random password using URL-safe base64 characters.\n * @param length Password length (default: 32)\n */\nexport function generateSecurePassword(length = 32): string {\n\treturn randomBytes(Math.ceil((length * 3) / 4))\n\t\t.toString('base64url')\n\t\t.slice(0, length);\n}\n\n/** Default service configurations (localhost for local dev via Docker port mapping) */\nconst SERVICE_DEFAULTS: Record<\n\tComposeServiceName,\n\tOmit<ServiceCredentials, 'password'>\n> = {\n\tpostgres: {\n\t\thost: 'localhost',\n\t\tport: 5432,\n\t\tusername: 'app',\n\t\tdatabase: 'app',\n\t},\n\tredis: {\n\t\thost: 'localhost',\n\t\tport: 6379,\n\t\tusername: 'default',\n\t},\n\trabbitmq: {\n\t\thost: 'localhost',\n\t\tport: 5672,\n\t\tusername: 'app',\n\t\tvhost: '/',\n\t},\n\tminio: {\n\t\thost: 'localhost',\n\t\tport: 9000,\n\t\tusername: 'app',\n\t\tbucket: 'app',\n\t},\n};\n\n/**\n * Generate credentials for a specific service.\n */\nexport function generateServiceCredentials(\n\tservice: ComposeServiceName,\n): ServiceCredentials {\n\tconst defaults = SERVICE_DEFAULTS[service];\n\treturn {\n\t\t...defaults,\n\t\tpassword: generateSecurePassword(),\n\t};\n}\n\n/**\n * Generate credentials for multiple services.\n */\nexport function generateServicesCredentials(\n\tservices: ComposeServiceName[],\n): StageSecrets['services'] {\n\tconst result: StageSecrets['services'] = {};\n\n\tfor (const service of services) {\n\t\tresult[service] = generateServiceCredentials(service);\n\t}\n\n\treturn result;\n}\n\n/**\n * Generate connection URL for PostgreSQL.\n */\nexport function generatePostgresUrl(creds: ServiceCredentials): string {\n\tconst { username, password, host, port, database } = creds;\n\treturn `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}`;\n}\n\n/**\n * Generate connection URL for Redis.\n */\nexport function generateRedisUrl(creds: ServiceCredentials): string {\n\tconst { password, host, port } = creds;\n\treturn `redis://:${encodeURIComponent(password)}@${host}:${port}`;\n}\n\n/**\n * Generate connection URL for RabbitMQ.\n */\nexport function generateRabbitmqUrl(creds: ServiceCredentials): string {\n\tconst { username, password, host, port, vhost } = creds;\n\tconst encodedVhost = encodeURIComponent(vhost ?? '/');\n\treturn `amqp://${username}:${encodeURIComponent(password)}@${host}:${port}/${encodedVhost}`;\n}\n\n/**\n * Generate endpoint URL for MinIO (S3-compatible).\n */\nexport function generateMinioEndpoint(creds: ServiceCredentials): string {\n\tconst { host, port } = creds;\n\treturn `http://${host}:${port}`;\n}\n\n/**\n * Generate connection URLs from service credentials.\n */\nexport function generateConnectionUrls(\n\tservices: StageSecrets['services'],\n): StageSecrets['urls'] {\n\tconst urls: StageSecrets['urls'] = {};\n\n\tif (services.postgres) {\n\t\turls.DATABASE_URL = generatePostgresUrl(services.postgres);\n\t}\n\n\tif (services.redis) {\n\t\turls.REDIS_URL = generateRedisUrl(services.redis);\n\t}\n\n\tif (services.rabbitmq) {\n\t\turls.RABBITMQ_URL = generateRabbitmqUrl(services.rabbitmq);\n\t}\n\n\tif (services.minio) {\n\t\turls.S3_ENDPOINT = generateMinioEndpoint(services.minio);\n\t}\n\n\treturn urls;\n}\n\n/**\n * Create a new StageSecrets object with generated credentials.\n * @param stage - The deployment stage (e.g., 'development', 'production')\n * @param services - List of services to generate credentials for\n * @param options - Optional configuration\n * @param options.projectName - Project name used to derive the database name (e.g., 'myapp' → 'myapp_dev')\n */\nexport function createStageSecrets(\n\tstage: string,\n\tservices: ComposeServiceName[],\n\toptions?: { projectName?: string },\n): StageSecrets {\n\tconst now = new Date().toISOString();\n\tconst serviceCredentials = generateServicesCredentials(services);\n\n\t// Override postgres database name with project-derived name if provided\n\tif (options?.projectName && serviceCredentials.postgres) {\n\t\tserviceCredentials.postgres.database = `${options.projectName.replace(/-/g, '_')}_dev`;\n\t}\n\n\tconst urls = generateConnectionUrls(serviceCredentials);\n\n\treturn {\n\t\tstage,\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t\tservices: serviceCredentials,\n\t\turls,\n\t\tcustom: {},\n\t};\n}\n\n/**\n * Rotate password for a specific service.\n */\nexport function rotateServicePassword(\n\tsecrets: StageSecrets,\n\tservice: ComposeServiceName,\n): StageSecrets {\n\tconst currentCreds = secrets.services[service];\n\tif (!currentCreds) {\n\t\tthrow new Error(`Service \"${service}\" not configured in secrets`);\n\t}\n\n\tconst newCreds: ServiceCredentials = {\n\t\t...currentCreds,\n\t\tpassword: generateSecurePassword(),\n\t};\n\n\tconst newServices = {\n\t\t...secrets.services,\n\t\t[service]: newCreds,\n\t};\n\n\treturn {\n\t\t...secrets,\n\t\tupdatedAt: new Date().toISOString(),\n\t\tservices: newServices,\n\t\turls: generateConnectionUrls(newServices),\n\t};\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { generateSecurePassword } from '../secrets/generator.js';\nimport type { StageSecrets } from '../secrets/types.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\n/**\n * Generate a secure random password for database users.\n * Uses a combination of timestamp and random bytes for uniqueness.\n */\nexport function generateDbPassword(): string {\n\treturn `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Generate database URL for an app.\n * All apps connect to the same database, but use different users/schemas.\n */\nexport function generateDbUrl(\n\tappName: string,\n\tpassword: string,\n\tprojectName: string,\n\thost = 'localhost',\n\tport = 5432,\n): string {\n\tconst userName = appName.replace(/-/g, '_');\n\tconst dbName = `${projectName.replace(/-/g, '_')}_dev`;\n\treturn `postgresql://${userName}:${password}@${host}:${port}/${dbName}`;\n}\n\n/**\n * Generate fullstack-aware custom secrets for a workspace.\n *\n * Generates:\n * - Common secrets: NODE_ENV, PORT, LOG_LEVEL, JWT_SECRET\n * - Per-app database passwords and URLs for backend apps with db service\n * - Better-auth secrets for apps using the better-auth framework\n */\nexport function generateFullstackCustomSecrets(\n\tworkspace: NormalizedWorkspace,\n): Record<string, string> {\n\tconst hasDb = !!workspace.services.db;\n\tconst customs: Record<string, string> = {\n\t\tNODE_ENV: 'development',\n\t\tPORT: '3000',\n\t\tLOG_LEVEL: 'debug',\n\t\tJWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n\t};\n\n\tif (!hasDb) {\n\t\treturn customs;\n\t}\n\n\t// Collect all frontend ports for trusted origins\n\tconst frontendPorts: number[] = [];\n\n\tfor (const [appName, appConfig] of Object.entries(workspace.apps)) {\n\t\tif (appConfig.type === 'frontend') {\n\t\t\tfrontendPorts.push(appConfig.port);\n\t\t\tconst upperName = appName.toUpperCase();\n\t\t\tcustoms[`${upperName}_URL`] = `http://localhost:${appConfig.port}`;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Backend apps with database: generate per-app DB passwords and URLs\n\t\tconst password = generateDbPassword();\n\t\tconst upperName = appName.toUpperCase();\n\n\t\tcustoms[`${upperName}_DATABASE_URL`] = generateDbUrl(\n\t\t\tappName,\n\t\t\tpassword,\n\t\t\tworkspace.name,\n\t\t);\n\t\tcustoms[`${upperName}_DB_PASSWORD`] = password;\n\n\t\t// Better-auth framework secrets\n\t\tif (appConfig.framework === 'better-auth') {\n\t\t\tcustoms.AUTH_PORT = String(appConfig.port);\n\t\t\tcustoms.AUTH_URL = `http://localhost:${appConfig.port}`;\n\t\t\tcustoms.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${generateSecurePassword(16)}`;\n\t\t\tcustoms.BETTER_AUTH_URL = `http://localhost:${appConfig.port}`;\n\t\t}\n\t}\n\n\t// Generate trusted origins for better-auth (all app ports)\n\tif (customs.BETTER_AUTH_SECRET) {\n\t\tconst allPorts = Object.values(workspace.apps).map((a) => a.port);\n\t\tcustoms.BETTER_AUTH_TRUSTED_ORIGINS = allPorts\n\t\t\t.map((p) => `http://localhost:${p}`)\n\t\t\t.join(',');\n\t}\n\n\treturn customs;\n}\n\n/**\n * Extract *_DB_PASSWORD keys from secrets and write docker/.env.\n *\n * The docker/.env file contains database passwords that the PostgreSQL\n * init script reads to create per-app database users.\n */\nexport async function writeDockerEnvFromSecrets(\n\tsecrets: StageSecrets,\n\tworkspaceRoot: string,\n): Promise<void> {\n\tconst dbPasswordEntries = Object.entries(secrets.custom).filter(([key]) =>\n\t\tkey.endsWith('_DB_PASSWORD'),\n\t);\n\n\tif (dbPasswordEntries.length === 0) {\n\t\treturn;\n\t}\n\n\tconst envContent = `# Auto-generated docker environment file\n# Contains database passwords for docker-compose postgres init\n# This file is gitignored - do not commit to version control\n${dbPasswordEntries.map(([key, value]) => `${key}=${value}`).join('\\n')}\n`;\n\n\tconst envPath = join(workspaceRoot, 'docker', '.env');\n\tawait mkdir(dirname(envPath), { recursive: true });\n\tawait writeFile(envPath, envContent);\n}\n"],"mappings":"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"}