@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.
- package/CHANGELOG.md +12 -0
- package/dist/{bundler-wY-XZDl9.cjs → bundler-DVJkwNMQ.cjs} +2 -2
- package/dist/{bundler-wY-XZDl9.cjs.map → bundler-DVJkwNMQ.cjs.map} +1 -1
- package/dist/{bundler-BvByD6tt.mjs → bundler-Di5Gz9Ou.mjs} +2 -2
- package/dist/{bundler-BvByD6tt.mjs.map → bundler-Di5Gz9Ou.mjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/{fullstack-secrets-Bnm_QeZr.mjs → fullstack-secrets-BIFFv4UZ.mjs} +10 -1
- package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +1 -0
- package/dist/{fullstack-secrets-AUkUyeW0.cjs → fullstack-secrets-D9rjTNyx.cjs} +10 -1
- package/dist/fullstack-secrets-D9rjTNyx.cjs.map +1 -0
- package/dist/{index-BdJZKXCJ.d.cts → index-UCsZ_Vkw.d.cts} +2 -2
- package/dist/{index-BdJZKXCJ.d.cts.map → index-UCsZ_Vkw.d.cts.map} +1 -1
- package/dist/{index-DB9VbcCD.d.mts → index-gXAGDSGu.d.mts} +2 -2
- package/dist/{index-DB9VbcCD.d.mts.map → index-gXAGDSGu.d.mts.map} +1 -1
- package/dist/index.cjs +136 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +136 -33
- 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--yaWO052.mjs → reconcile-DxTEausy.mjs} +2 -2
- package/dist/{reconcile--yaWO052.mjs.map → reconcile-DxTEausy.mjs.map} +1 -1
- package/dist/{reconcile-oktp4i_4.cjs → reconcile-LaaJkFlO.cjs} +2 -2
- package/dist/{reconcile-oktp4i_4.cjs.map → reconcile-LaaJkFlO.cjs.map} +1 -1
- package/dist/{storage-CEisMvif.cjs → storage-6GBoLCYF.cjs} +7 -1
- package/dist/{storage-CEisMvif.cjs.map → storage-6GBoLCYF.cjs.map} +1 -1
- package/dist/{storage-D3lh8Xpq.mjs → storage-BFqrVsip.mjs} +1 -1
- package/dist/{storage-B3aXQBpI.cjs → storage-DCqjCiDn.cjs} +1 -1
- package/dist/{storage-Df-NFMY9.mjs → storage-DMf420PP.mjs} +7 -1
- package/dist/{storage-Df-NFMY9.mjs.map → storage-DMf420PP.mjs.map} +1 -1
- package/dist/sync-BVNso6AA.cjs +4 -0
- package/dist/{sync-CcDvBUu4.cjs → sync-DIGGOxCw.cjs} +2 -2
- package/dist/{sync-CcDvBUu4.cjs.map → sync-DIGGOxCw.cjs.map} +1 -1
- package/dist/{sync-CIzj1CQe.mjs → sync-DjD_TeNX.mjs} +1 -1
- package/dist/{sync-D-7NTKvV.mjs → sync-Do9O7QZ8.mjs} +2 -2
- package/dist/{sync-D-7NTKvV.mjs.map → sync-Do9O7QZ8.mjs.map} +1 -1
- package/dist/{types-D4MLWXSL.d.cts → types-DiV9Mbvc.d.mts} +2 -2
- package/dist/{types-D4MLWXSL.d.cts.map → types-DiV9Mbvc.d.mts.map} +1 -1
- package/dist/{types-DwpLq_fp.d.mts → types-JvWj5Ckc.d.cts} +2 -2
- package/dist/{types-DwpLq_fp.d.mts.map → types-JvWj5Ckc.d.cts.map} +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/package.json +4 -4
- package/src/dev/index.ts +35 -0
- package/src/docker/__tests__/compose.spec.ts +24 -2
- package/src/docker/compose.ts +53 -3
- package/src/init/__tests__/generators.spec.ts +45 -2
- package/src/init/generators/docker.ts +25 -2
- package/src/init/index.ts +3 -1
- package/src/init/templates/index.ts +6 -0
- package/src/init/versions.ts +1 -1
- package/src/secrets/generator.ts +10 -0
- package/src/secrets/storage.ts +6 -0
- package/src/secrets/types.ts +3 -0
- package/src/setup/__tests__/reconcile-secrets.spec.ts +35 -0
- package/src/setup/index.ts +2 -0
- package/src/test/index.ts +21 -9
- package/src/types.ts +6 -1
- package/dist/fullstack-secrets-AUkUyeW0.cjs.map +0 -1
- package/dist/fullstack-secrets-Bnm_QeZr.mjs.map +0 -1
- package/dist/sync-QNXT8x13.cjs +0 -4
package/src/docker/compose.ts
CHANGED
|
@@ -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 } =
|
|
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: {
|
|
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
|
-
|
|
160
|
-
|
|
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
|
/**
|
package/src/init/versions.ts
CHANGED
|
@@ -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.
|
|
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',
|
package/src/secrets/generator.ts
CHANGED
|
@@ -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
|
|
package/src/secrets/storage.ts
CHANGED
|
@@ -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
|
|
package/src/secrets/types.ts
CHANGED
|
@@ -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 },
|
package/src/setup/index.ts
CHANGED
|
@@ -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 —
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 =
|
|
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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"}
|
package/dist/sync-QNXT8x13.cjs
DELETED