@geekmidas/cli 1.10.11 → 1.10.13

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 (64) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{bundler-wY-XZDl9.cjs → bundler-DVJkwNMQ.cjs} +2 -2
  3. package/dist/{bundler-wY-XZDl9.cjs.map → bundler-DVJkwNMQ.cjs.map} +1 -1
  4. package/dist/{bundler-BvByD6tt.mjs → bundler-Di5Gz9Ou.mjs} +2 -2
  5. package/dist/{bundler-BvByD6tt.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-Bnm_QeZr.mjs → fullstack-secrets-BIFFv4UZ.mjs} +10 -1
  9. package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +1 -0
  10. package/dist/{fullstack-secrets-AUkUyeW0.cjs → fullstack-secrets-D9rjTNyx.cjs} +10 -1
  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 +136 -33
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +136 -33
  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--yaWO052.mjs → reconcile-DxTEausy.mjs} +2 -2
  25. package/dist/{reconcile--yaWO052.mjs.map → reconcile-DxTEausy.mjs.map} +1 -1
  26. package/dist/{reconcile-oktp4i_4.cjs → reconcile-LaaJkFlO.cjs} +2 -2
  27. package/dist/{reconcile-oktp4i_4.cjs.map → reconcile-LaaJkFlO.cjs.map} +1 -1
  28. package/dist/{storage-CEisMvif.cjs → storage-6GBoLCYF.cjs} +7 -1
  29. package/dist/{storage-CEisMvif.cjs.map → storage-6GBoLCYF.cjs.map} +1 -1
  30. package/dist/{storage-D3lh8Xpq.mjs → storage-BFqrVsip.mjs} +1 -1
  31. package/dist/{storage-B3aXQBpI.cjs → storage-DCqjCiDn.cjs} +1 -1
  32. package/dist/{storage-Df-NFMY9.mjs → storage-DMf420PP.mjs} +7 -1
  33. package/dist/{storage-Df-NFMY9.mjs.map → storage-DMf420PP.mjs.map} +1 -1
  34. package/dist/sync-BVNso6AA.cjs +4 -0
  35. package/dist/{sync-CcDvBUu4.cjs → sync-DIGGOxCw.cjs} +2 -2
  36. package/dist/{sync-CcDvBUu4.cjs.map → sync-DIGGOxCw.cjs.map} +1 -1
  37. package/dist/{sync-CIzj1CQe.mjs → sync-DjD_TeNX.mjs} +1 -1
  38. package/dist/{sync-D-7NTKvV.mjs → sync-Do9O7QZ8.mjs} +2 -2
  39. package/dist/{sync-D-7NTKvV.mjs.map → sync-Do9O7QZ8.mjs.map} +1 -1
  40. package/dist/{types-D4MLWXSL.d.cts → 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-DwpLq_fp.d.mts → 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 +4 -4
  47. package/src/dev/index.ts +35 -0
  48. package/src/docker/__tests__/compose.spec.ts +24 -2
  49. package/src/docker/compose.ts +53 -3
  50. package/src/init/__tests__/generators.spec.ts +45 -2
  51. package/src/init/generators/docker.ts +25 -2
  52. package/src/init/index.ts +3 -1
  53. package/src/init/templates/index.ts +6 -0
  54. package/src/init/versions.ts +1 -1
  55. package/src/secrets/generator.ts +10 -0
  56. package/src/secrets/storage.ts +6 -0
  57. package/src/secrets/types.ts +3 -0
  58. package/src/setup/__tests__/reconcile-secrets.spec.ts +35 -0
  59. package/src/setup/index.ts +2 -0
  60. package/src/test/index.ts +21 -9
  61. package/src/types.ts +6 -1
  62. package/dist/fullstack-secrets-AUkUyeW0.cjs.map +0 -1
  63. package/dist/fullstack-secrets-Bnm_QeZr.mjs.map +0 -1
  64. package/dist/sync-QNXT8x13.cjs +0 -4
@@ -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 {
@@ -133,6 +135,14 @@ services:
133
135
  `;
134
136
  }
135
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}}
143
+ `;
144
+ }
145
+
136
146
  yaml += ` healthcheck:
137
147
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
138
148
  interval: 30s
@@ -249,6 +259,28 @@ services:
249
259
  `;
250
260
  }
251
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
+
252
284
  // Add volumes
253
285
  yaml += `
254
286
  volumes:
@@ -371,6 +403,7 @@ services:
371
403
  hasPostgres,
372
404
  hasRedis,
373
405
  hasMinio,
406
+ hasMail,
374
407
  });
375
408
  }
376
409
 
@@ -421,9 +454,16 @@ services:
421
454
  image: axllent/mailpit:latest
422
455
  container_name: ${workspace.name}-mailpit
423
456
  restart: unless-stopped
457
+ environment:
458
+ MP_SMTP_AUTH: \${SMTP_USER:-${workspace.name}}:\${SMTP_PASS:-${workspace.name}}
424
459
  ports:
425
- - "8025:8025" # Web UI
426
- - "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
427
467
  networks:
428
468
  - workspace-network
429
469
  `;
@@ -532,9 +572,11 @@ function generateAppService(
532
572
  hasPostgres: boolean;
533
573
  hasRedis: boolean;
534
574
  hasMinio: boolean;
575
+ hasMail: boolean;
535
576
  },
536
577
  ): string {
537
- const { registry, projectName, hasPostgres, hasRedis, hasMinio } = options;
578
+ const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail } =
579
+ options;
538
580
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
539
581
 
540
582
  // Health check path - frontends use /, backends use /health
@@ -585,6 +627,13 @@ function generateAppService(
585
627
  - STORAGE_BUCKET=\${STORAGE_BUCKET:-${projectName}}
586
628
  - STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
587
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}}
588
637
  `;
589
638
  }
590
639
  }
@@ -602,6 +651,7 @@ function generateAppService(
602
651
  if (hasPostgres) dependencies.push('postgres');
603
652
  if (hasRedis) dependencies.push('redis');
604
653
  if (hasMinio) dependencies.push('minio');
654
+ if (hasMail) dependencies.push('mailpit');
605
655
  }
606
656
 
607
657
  if (dependencies.length > 0) {
@@ -25,7 +25,7 @@ const baseOptions: TemplateOptions = {
25
25
  apiPath: '',
26
26
  packageManager: 'pnpm',
27
27
  deployTarget: 'dokploy',
28
- services: { db: true, cache: true, mail: false },
28
+ services: { db: true, cache: true, mail: false, storage: false },
29
29
  };
30
30
 
31
31
  describe('generatePackageJson', () => {
@@ -245,7 +245,7 @@ describe('generateDockerFiles', () => {
245
245
  it('should include mailpit with dynamic ports when mail is enabled', () => {
246
246
  const options = {
247
247
  ...baseOptions,
248
- services: { db: true, cache: true, mail: true },
248
+ services: { ...baseOptions.services, mail: true },
249
249
  };
250
250
  const files = generateDockerFiles(options, minimalTemplate);
251
251
  expect(files[0].content).toContain('mailpit');
@@ -253,6 +253,49 @@ 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:');
257
+ });
258
+
259
+ it('should include minio with dynamic ports when storage is enabled', () => {
260
+ const options = {
261
+ ...baseOptions,
262
+ services: { ...baseOptions.services, storage: true },
263
+ };
264
+ const files = generateDockerFiles(options, minimalTemplate);
265
+ expect(files[0].content).toContain('minio');
266
+ expect(files[0].content).toContain('minio/minio:latest');
267
+ expect(files[0].content).toContain("'${MINIO_API_HOST_PORT:-9000}:9000'");
268
+ expect(files[0].content).toContain(
269
+ "'${MINIO_CONSOLE_HOST_PORT:-9001}:9001'",
270
+ );
271
+ expect(files[0].content).toContain('MINIO_ROOT_USER:');
272
+ expect(files[0].content).toContain('MINIO_ROOT_PASSWORD:');
273
+ expect(files[0].content).toContain('minio_data:');
274
+ });
275
+
276
+ it('should include all services when all are enabled', () => {
277
+ const options = {
278
+ ...baseOptions,
279
+ services: { db: true, cache: true, mail: true, storage: true },
280
+ };
281
+ const files = generateDockerFiles(options, minimalTemplate);
282
+ expect(files[0].content).toContain('postgres');
283
+ expect(files[0].content).toContain('redis');
284
+ expect(files[0].content).toContain('mailpit');
285
+ expect(files[0].content).toContain('minio');
286
+ });
287
+
288
+ it('should not include disabled services', () => {
289
+ const options = {
290
+ ...baseOptions,
291
+ database: false,
292
+ services: { db: false, cache: false, mail: false, storage: false },
293
+ };
294
+ const files = generateDockerFiles(options, minimalTemplate);
295
+ expect(files[0].content).not.toContain('postgres');
296
+ expect(files[0].content).toContain('redis'); // redis is always included
297
+ expect(files[0].content).not.toContain('mailpit');
298
+ expect(files[0].content).not.toContain('minio');
256
299
  });
257
300
  });
258
301
 
@@ -156,8 +156,31 @@ 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}}`);
160
+ }
161
+
162
+ // MinIO for S3-compatible object storage
163
+ if (options.services?.storage) {
164
+ services.push(` minio:
165
+ image: minio/minio:latest
166
+ container_name: ${options.name}-minio
167
+ restart: unless-stopped
168
+ entrypoint: sh
169
+ command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${options.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
170
+ environment:
171
+ MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${options.name}}
172
+ MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${options.name}}
173
+ ports:
174
+ - '\${MINIO_API_HOST_PORT:-9000}:9000'
175
+ - '\${MINIO_CONSOLE_HOST_PORT:-9001}:9001'
176
+ volumes:
177
+ - minio_data:/data
178
+ healthcheck:
179
+ test: ['CMD', 'mc', 'ready', 'local']
180
+ interval: 10s
181
+ timeout: 5s
182
+ retries: 5`);
183
+ volumes.push(' minio_data:');
161
184
  }
162
185
 
163
186
  // Build docker-compose.yml
package/src/init/index.ts CHANGED
@@ -181,12 +181,13 @@ export async function initCommand(
181
181
 
182
182
  // Parse services selection
183
183
  const servicesArray: string[] = options.yes
184
- ? ['db', 'cache', 'mail']
184
+ ? ['db', 'cache', 'mail', 'storage']
185
185
  : answers.services || [];
186
186
  const services: ServicesSelection = {
187
187
  db: servicesArray.includes('db'),
188
188
  cache: servicesArray.includes('cache'),
189
189
  mail: servicesArray.includes('mail'),
190
+ storage: servicesArray.includes('storage'),
190
191
  };
191
192
 
192
193
  const pkgManager: PackageManager = options.pm
@@ -331,6 +332,7 @@ export async function initCommand(
331
332
  if (services.db) secretServices.push('postgres');
332
333
  if (services.cache) secretServices.push('redis');
333
334
  if (services.storage) secretServices.push('minio');
335
+ if (services.mail) secretServices.push('mailpit');
334
336
 
335
337
  const devSecrets = createStageSecrets('development', secretServices, {
336
338
  projectName: name,
@@ -38,6 +38,7 @@ export interface ServicesSelection {
38
38
  db: boolean;
39
39
  cache: boolean;
40
40
  mail: boolean;
41
+ storage: boolean;
41
42
  }
42
43
 
43
44
  /**
@@ -247,6 +248,11 @@ export const servicesChoices = [
247
248
  value: 'mail',
248
249
  description: 'Email testing service (dev only)',
249
250
  },
251
+ {
252
+ title: 'MinIO',
253
+ value: 'storage',
254
+ description: 'S3-compatible object storage (dev only)',
255
+ },
250
256
  ];
251
257
 
252
258
  /**
@@ -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',
@@ -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
  /**
@@ -127,6 +132,11 @@ export function generateConnectionUrls(
127
132
  urls.STORAGE_ENDPOINT = generateMinioEndpoint(services.minio);
128
133
  }
129
134
 
135
+ if (services.mailpit) {
136
+ urls.SMTP_HOST = services.mailpit.host;
137
+ urls.SMTP_PORT = String(services.mailpit.port);
138
+ }
139
+
130
140
  return urls;
131
141
  }
132
142
 
@@ -215,6 +215,12 @@ export function toEmbeddableSecrets(secrets: StageSecrets): EmbeddableSecrets {
215
215
  STORAGE_REGION: 'eu-west-1',
216
216
  STORAGE_FORCE_PATH_STYLE: 'true',
217
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,
223
+ }),
218
224
  };
219
225
  }
220
226
 
@@ -28,6 +28,7 @@ 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: {
@@ -35,6 +36,8 @@ export interface StageSecrets {
35
36
  REDIS_URL?: string;
36
37
  RABBITMQ_URL?: string;
37
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>;
@@ -158,6 +158,41 @@ describe('reconcileSecrets', () => {
158
158
  expect(result!.services.postgres).toEqual(secrets.services.postgres);
159
159
  });
160
160
 
161
+ it('should add missing mailpit credentials when mail service is enabled', () => {
162
+ const workspace = createWorkspace({
163
+ services: { db: true, mail: true },
164
+ });
165
+ // Secrets only have postgres, not mailpit
166
+ const secrets = createSecrets({
167
+ NODE_ENV: 'development',
168
+ PORT: '3000',
169
+ API_DATABASE_URL: 'postgresql://api:pass@localhost:5432/test_dev',
170
+ API_DB_PASSWORD: 'pass',
171
+ AUTH_DATABASE_URL: 'postgresql://auth:pass@localhost:5432/test_dev',
172
+ AUTH_DB_PASSWORD: 'pass',
173
+ WEB_URL: 'http://localhost:3002',
174
+ BETTER_AUTH_SECRET: 'existing',
175
+ BETTER_AUTH_URL: 'http://localhost:3001',
176
+ BETTER_AUTH_TRUSTED_ORIGINS:
177
+ 'http://localhost:3000,http://localhost:3001,http://localhost:3002',
178
+ AUTH_PORT: '3001',
179
+ AUTH_URL: 'http://localhost:3001',
180
+ });
181
+
182
+ const result = reconcileSecrets(secrets, workspace);
183
+
184
+ expect(result).not.toBeNull();
185
+ expect(result!.services.mailpit).toBeDefined();
186
+ expect(result!.services.mailpit!.host).toBe('localhost');
187
+ expect(result!.services.mailpit!.port).toBe(1025);
188
+ expect(result!.services.mailpit!.username).toBeDefined();
189
+ expect(result!.services.mailpit!.password).toHaveLength(32);
190
+ expect(result!.urls.SMTP_HOST).toBe('localhost');
191
+ expect(result!.urls.SMTP_PORT).toBe('1025');
192
+ // Existing postgres should be preserved
193
+ expect(result!.services.postgres).toEqual(secrets.services.postgres);
194
+ });
195
+
161
196
  it('should not regenerate credentials for existing services', () => {
162
197
  const workspace = createWorkspace({
163
198
  services: { db: true, storage: true },
@@ -168,6 +168,7 @@ export function reconcileSecrets(
168
168
  { key: 'db', name: 'postgres' },
169
169
  { key: 'cache', name: 'redis' },
170
170
  { key: 'storage', name: 'minio' },
171
+ { key: 'mail', name: 'mailpit' },
171
172
  ];
172
173
 
173
174
  for (const { key, name } of serviceMap) {
@@ -235,6 +236,7 @@ async function generateFreshSecrets(
235
236
  if (workspace.services.db) serviceNames.push('postgres');
236
237
  if (workspace.services.cache) serviceNames.push('redis');
237
238
  if (workspace.services.storage) serviceNames.push('minio');
239
+ if (workspace.services.mail) serviceNames.push('mailpit');
238
240
 
239
241
  // Create base secrets with service credentials
240
242
  const secrets = createStageSecrets(stage, serviceNames, {
package/src/test/index.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  parseComposePortMappings,
11
11
  resolveServicePorts,
12
12
  rewriteUrlsWithPorts,
13
+ startComposeServices,
13
14
  startWorkspaceServices,
14
15
  } from '../dev/index';
15
16
  import { readStageSecrets, toEmbeddableSecrets } from '../secrets/storage';
@@ -121,20 +122,31 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
121
122
  );
122
123
  }
123
124
  } catch {
124
- // Not in a workspace — fall back to port state file
125
+ // Not in a workspace — start Docker services from local docker-compose.yml
125
126
  const composePath = join(cwd, 'docker-compose.yml');
126
127
  const mappings = parseComposePortMappings(composePath);
127
128
  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
- });
129
+ const resolvedPorts = await resolveServicePorts(cwd);
130
+ await startComposeServices(cwd, resolvedPorts.dockerEnv, secretsEnv);
131
+
132
+ if (resolvedPorts.mappings.length > 0) {
133
+ secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
135
134
  console.log(
136
- ` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`,
135
+ ` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`,
137
136
  );
137
+ } else {
138
+ // Fallback to saved port state from a previous gkm dev run
139
+ const ports = await loadPortState(cwd);
140
+ if (Object.keys(ports).length > 0) {
141
+ secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
142
+ dockerEnv: {},
143
+ ports,
144
+ mappings,
145
+ });
146
+ console.log(
147
+ ` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`,
148
+ );
149
+ }
138
150
  }
139
151
  }
140
152
  }
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-AUkUyeW0.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.STORAGE_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 service defaults with project-derived names if provided\n\tif (options?.projectName) {\n\t\tif (serviceCredentials.postgres) {\n\t\t\tserviceCredentials.postgres.database = `${options.projectName.replace(/-/g, '_')}_dev`;\n\t\t}\n\t\tif (serviceCredentials.minio) {\n\t\t\tserviceCredentials.minio.bucket = options.projectName;\n\t\t\tserviceCredentials.minio.username = options.projectName;\n\t\t}\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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"fullstack-secrets-Bnm_QeZr.mjs","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.STORAGE_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 service defaults with project-derived names if provided\n\tif (options?.projectName) {\n\t\tif (serviceCredentials.postgres) {\n\t\t\tserviceCredentials.postgres.database = `${options.projectName.replace(/-/g, '_')}_dev`;\n\t\t}\n\t\tif (serviceCredentials.minio) {\n\t\t\tserviceCredentials.minio.bucket = options.projectName;\n\t\t\tserviceCredentials.minio.username = options.projectName;\n\t\t}\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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"}
@@ -1,4 +0,0 @@
1
- const require_sync = require('./sync-CcDvBUu4.cjs');
2
-
3
- exports.pullSecrets = require_sync.pullSecrets;
4
- exports.pushSecrets = require_sync.pushSecrets;