@geekmidas/cli 1.10.2 → 1.10.4

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.
@@ -1,4 +1,4 @@
1
- const require_fullstack_secrets = require('./fullstack-secrets-COWz084x.cjs');
1
+ const require_fullstack_secrets = require('./fullstack-secrets-DqxYGrgW.cjs');
2
2
 
3
3
  //#region src/secrets/reconcile.ts
4
4
  /**
@@ -33,4 +33,4 @@ function reconcileMissingSecrets(secrets, workspace) {
33
33
 
34
34
  //#endregion
35
35
  exports.reconcileMissingSecrets = reconcileMissingSecrets;
36
- //# sourceMappingURL=reconcile-7yarEvmK.cjs.map
36
+ //# sourceMappingURL=reconcile-CCtrj-zt.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-7yarEvmK.cjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,yDAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
1
+ {"version":3,"file":"reconcile-CCtrj-zt.cjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,yDAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
@@ -1,4 +1,4 @@
1
- import { generateFullstackCustomSecrets } from "./fullstack-secrets-UZAFWuH4.mjs";
1
+ import { generateFullstackCustomSecrets } from "./fullstack-secrets-odm79Uo1.mjs";
2
2
 
3
3
  //#region src/secrets/reconcile.ts
4
4
  /**
@@ -33,4 +33,4 @@ function reconcileMissingSecrets(secrets, workspace) {
33
33
 
34
34
  //#endregion
35
35
  export { reconcileMissingSecrets };
36
- //# sourceMappingURL=reconcile-D2WCDQue.mjs.map
36
+ //# sourceMappingURL=reconcile-WzC1oAUV.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-D2WCDQue.mjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,+BAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
1
+ {"version":3,"file":"reconcile-WzC1oAUV.mjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,+BAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.2",
3
+ "version": "1.10.4",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -58,9 +58,9 @@
58
58
  "yaml": "~2.8.2",
59
59
  "@geekmidas/constructs": "~3.0.2",
60
60
  "@geekmidas/envkit": "~1.0.3",
61
- "@geekmidas/errors": "~1.0.0",
62
61
  "@geekmidas/schema": "~1.0.0",
63
- "@geekmidas/logger": "~1.0.0"
62
+ "@geekmidas/logger": "~1.0.0",
63
+ "@geekmidas/errors": "~1.0.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/lodash.kebabcase": "^4.1.9",
package/src/dev/index.ts CHANGED
@@ -1315,6 +1315,7 @@ async function workspaceDevCommand(
1315
1315
  cwd: workspace.root,
1316
1316
  stdio: 'inherit',
1317
1317
  env: turboEnv,
1318
+ detached: true,
1318
1319
  });
1319
1320
 
1320
1321
  // Set up file watcher for backend .gkm/openapi.ts changes (auto-copy to frontends)
@@ -1408,21 +1409,35 @@ async function workspaceDevCommand(
1408
1409
  openApiWatcher.close().catch(() => {});
1409
1410
  }
1410
1411
 
1411
- // Kill turbo process
1412
- if (turboProcess.pid) {
1412
+ // Kill turbo process group
1413
+ const pid = turboProcess.pid;
1414
+ if (pid) {
1413
1415
  try {
1414
- // Try to kill the process group
1415
- process.kill(-turboProcess.pid, 'SIGTERM');
1416
+ process.kill(-pid, 'SIGTERM');
1416
1417
  } catch {
1417
- // Fall back to killing just the process
1418
- turboProcess.kill('SIGTERM');
1418
+ try {
1419
+ process.kill(pid, 'SIGTERM');
1420
+ } catch {
1421
+ // Process already dead
1422
+ }
1419
1423
  }
1420
1424
  }
1421
1425
 
1422
- // Give processes time to clean up
1426
+ // Force kill after timeout if processes are still alive
1423
1427
  setTimeout(() => {
1428
+ if (pid) {
1429
+ try {
1430
+ process.kill(-pid, 'SIGKILL');
1431
+ } catch {
1432
+ try {
1433
+ process.kill(pid, 'SIGKILL');
1434
+ } catch {
1435
+ // Process already dead
1436
+ }
1437
+ }
1438
+ }
1424
1439
  process.exit(0);
1425
- }, 2000);
1440
+ }, 3000);
1426
1441
  };
1427
1442
 
1428
1443
  process.on('SIGINT', shutdown);
@@ -364,6 +364,7 @@ export default defineWorkspace({
364
364
  path: 'apps/auth',
365
365
  port: 3002,
366
366
  entry: './src/index.ts',
367
+ framework: 'better-auth',
367
368
  envParser: './src/config/env#envParser',
368
369
  logger: './src/config/logger#logger',
369
370
  },
@@ -418,6 +419,12 @@ export default defineWorkspace({
418
419
  },`;
419
420
  }
420
421
 
422
+ // Always enable secrets for workspace dev environment
423
+ config += `
424
+ secrets: {
425
+ enabled: true,
426
+ },`;
427
+
421
428
  config += `
422
429
  });
423
430
  `;
@@ -40,7 +40,7 @@ describe('generateServiceCredentials', () => {
40
40
  it('should generate postgres credentials with defaults', () => {
41
41
  const creds = generateServiceCredentials('postgres');
42
42
 
43
- expect(creds.host).toBe('postgres');
43
+ expect(creds.host).toBe('localhost');
44
44
  expect(creds.port).toBe(5432);
45
45
  expect(creds.username).toBe('app');
46
46
  expect(creds.database).toBe('app');
@@ -50,7 +50,7 @@ describe('generateServiceCredentials', () => {
50
50
  it('should generate redis credentials with defaults', () => {
51
51
  const creds = generateServiceCredentials('redis');
52
52
 
53
- expect(creds.host).toBe('redis');
53
+ expect(creds.host).toBe('localhost');
54
54
  expect(creds.port).toBe(6379);
55
55
  expect(creds.username).toBe('default');
56
56
  expect(creds.password).toHaveLength(32);
@@ -59,7 +59,7 @@ describe('generateServiceCredentials', () => {
59
59
  it('should generate rabbitmq credentials with defaults', () => {
60
60
  const creds = generateServiceCredentials('rabbitmq');
61
61
 
62
- expect(creds.host).toBe('rabbitmq');
62
+ expect(creds.host).toBe('localhost');
63
63
  expect(creds.port).toBe(5672);
64
64
  expect(creds.username).toBe('app');
65
65
  expect(creds.vhost).toBe('/');
@@ -12,24 +12,24 @@ export function generateSecurePassword(length = 32): string {
12
12
  .slice(0, length);
13
13
  }
14
14
 
15
- /** Default service configurations */
15
+ /** Default service configurations (localhost for local dev via Docker port mapping) */
16
16
  const SERVICE_DEFAULTS: Record<
17
17
  ComposeServiceName,
18
18
  Omit<ServiceCredentials, 'password'>
19
19
  > = {
20
20
  postgres: {
21
- host: 'postgres',
21
+ host: 'localhost',
22
22
  port: 5432,
23
23
  username: 'app',
24
24
  database: 'app',
25
25
  },
26
26
  redis: {
27
- host: 'redis',
27
+ host: 'localhost',
28
28
  port: 6379,
29
29
  username: 'default',
30
30
  },
31
31
  rabbitmq: {
32
- host: 'rabbitmq',
32
+ host: 'localhost',
33
33
  port: 5672,
34
34
  username: 'app',
35
35
  vhost: '/',
@@ -0,0 +1,248 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { StageSecrets } from '../../secrets/types.js';
3
+ import type { NormalizedWorkspace } from '../../workspace/types.js';
4
+ import { generateFullstackCustomSecrets } from '../fullstack-secrets.js';
5
+ import { reconcileSecrets } from '../index.js';
6
+
7
+ function createWorkspace(
8
+ overrides: Partial<NormalizedWorkspace> = {},
9
+ ): NormalizedWorkspace {
10
+ return {
11
+ name: 'test-project',
12
+ root: '/tmp/test-project',
13
+ apps: {
14
+ api: {
15
+ type: 'backend',
16
+ port: 3000,
17
+ root: '/tmp/test-project/apps/api',
18
+ packageName: '@test/api',
19
+ routes: './src/endpoints/**/*.ts',
20
+ dependencies: [],
21
+ },
22
+ auth: {
23
+ type: 'backend',
24
+ port: 3001,
25
+ root: '/tmp/test-project/apps/auth',
26
+ packageName: '@test/auth',
27
+ entry: './src/index.ts',
28
+ framework: 'better-auth',
29
+ dependencies: [],
30
+ },
31
+ web: {
32
+ type: 'frontend',
33
+ port: 3002,
34
+ root: '/tmp/test-project/apps/web',
35
+ packageName: '@test/web',
36
+ dependencies: ['api', 'auth'],
37
+ },
38
+ },
39
+ services: {
40
+ db: true,
41
+ cache: false,
42
+ mail: false,
43
+ },
44
+ ...overrides,
45
+ } as NormalizedWorkspace;
46
+ }
47
+
48
+ function createSecrets(custom: Record<string, string> = {}): StageSecrets {
49
+ return {
50
+ stage: 'development',
51
+ createdAt: '2025-01-01T00:00:00.000Z',
52
+ updatedAt: '2025-01-01T00:00:00.000Z',
53
+ services: {
54
+ postgres: {
55
+ host: 'localhost',
56
+ port: 5432,
57
+ username: 'postgres',
58
+ password: 'postgres',
59
+ database: 'test_dev',
60
+ },
61
+ },
62
+ urls: {
63
+ DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/test_dev',
64
+ },
65
+ custom,
66
+ };
67
+ }
68
+
69
+ describe('reconcileSecrets', () => {
70
+ it('should add missing BETTER_AUTH_* keys to existing secrets', () => {
71
+ const workspace = createWorkspace();
72
+ const secrets = createSecrets({
73
+ NODE_ENV: 'development',
74
+ PORT: '3000',
75
+ LOG_LEVEL: 'debug',
76
+ JWT_SECRET: 'existing-jwt-secret',
77
+ API_DATABASE_URL: 'postgresql://api:pass@localhost:5432/test_dev',
78
+ API_DB_PASSWORD: 'pass',
79
+ AUTH_DATABASE_URL: 'postgresql://auth:pass@localhost:5432/test_dev',
80
+ AUTH_DB_PASSWORD: 'pass',
81
+ WEB_URL: 'http://localhost:3002',
82
+ });
83
+
84
+ const result = reconcileSecrets(secrets, workspace);
85
+
86
+ expect(result).not.toBeNull();
87
+ expect(result!.custom.BETTER_AUTH_SECRET).toBeDefined();
88
+ expect(result!.custom.BETTER_AUTH_URL).toBe('http://localhost:3001');
89
+ expect(result!.custom.BETTER_AUTH_TRUSTED_ORIGINS).toContain(
90
+ 'http://localhost:3000',
91
+ );
92
+ expect(result!.custom.BETTER_AUTH_TRUSTED_ORIGINS).toContain(
93
+ 'http://localhost:3001',
94
+ );
95
+ expect(result!.custom.BETTER_AUTH_TRUSTED_ORIGINS).toContain(
96
+ 'http://localhost:3002',
97
+ );
98
+ expect(result!.custom.AUTH_PORT).toBe('3001');
99
+ expect(result!.custom.AUTH_URL).toBe('http://localhost:3001');
100
+ });
101
+
102
+ it('should not overwrite existing secret values', () => {
103
+ const workspace = createWorkspace();
104
+ const secrets = createSecrets({
105
+ NODE_ENV: 'development',
106
+ PORT: '3000',
107
+ LOG_LEVEL: 'debug',
108
+ JWT_SECRET: 'my-custom-jwt',
109
+ API_DATABASE_URL: 'postgresql://api:custom@localhost:5432/test_dev',
110
+ API_DB_PASSWORD: 'custom',
111
+ AUTH_DATABASE_URL: 'postgresql://auth:custom@localhost:5432/test_dev',
112
+ AUTH_DB_PASSWORD: 'custom',
113
+ WEB_URL: 'http://localhost:3002',
114
+ BETTER_AUTH_SECRET: 'my-existing-secret',
115
+ BETTER_AUTH_URL: 'http://localhost:3001',
116
+ BETTER_AUTH_TRUSTED_ORIGINS:
117
+ 'http://localhost:3000,http://localhost:3001',
118
+ AUTH_PORT: '3001',
119
+ AUTH_URL: 'http://localhost:3001',
120
+ });
121
+
122
+ const result = reconcileSecrets(secrets, workspace);
123
+
124
+ expect(result).toBeNull();
125
+ });
126
+
127
+ it('should return null for single-app workspaces', () => {
128
+ const workspace = createWorkspace({
129
+ apps: {
130
+ api: {
131
+ type: 'backend',
132
+ port: 3000,
133
+ root: '/tmp/test-project/apps/api',
134
+ path: 'apps/api',
135
+ resolvedDeployTarget: 'dokploy',
136
+ packageName: '@test/api',
137
+ routes: './src/endpoints/**/*.ts',
138
+ dependencies: [],
139
+ },
140
+ },
141
+ } as Partial<NormalizedWorkspace>);
142
+
143
+ const secrets = createSecrets({ NODE_ENV: 'development' });
144
+
145
+ const result = reconcileSecrets(secrets, workspace);
146
+
147
+ expect(result).toBeNull();
148
+ });
149
+
150
+ it('should preserve all existing custom secrets when adding new ones', () => {
151
+ const workspace = createWorkspace();
152
+ const secrets = createSecrets({
153
+ NODE_ENV: 'development',
154
+ PORT: '3000',
155
+ LOG_LEVEL: 'debug',
156
+ JWT_SECRET: 'keep-this',
157
+ MY_CUSTOM_VAR: 'user-added',
158
+ API_DATABASE_URL: 'postgresql://api:pass@localhost:5432/test_dev',
159
+ API_DB_PASSWORD: 'pass',
160
+ AUTH_DATABASE_URL: 'postgresql://auth:pass@localhost:5432/test_dev',
161
+ AUTH_DB_PASSWORD: 'pass',
162
+ WEB_URL: 'http://localhost:3002',
163
+ });
164
+
165
+ const result = reconcileSecrets(secrets, workspace);
166
+
167
+ expect(result).not.toBeNull();
168
+ expect(result!.custom.JWT_SECRET).toBe('keep-this');
169
+ expect(result!.custom.MY_CUSTOM_VAR).toBe('user-added');
170
+ expect(result!.custom.BETTER_AUTH_SECRET).toBeDefined();
171
+ });
172
+
173
+ it('should update updatedAt timestamp when reconciling', () => {
174
+ const workspace = createWorkspace();
175
+ const secrets = createSecrets({
176
+ NODE_ENV: 'development',
177
+ PORT: '3000',
178
+ API_DATABASE_URL: 'postgresql://api:pass@localhost:5432/test_dev',
179
+ API_DB_PASSWORD: 'pass',
180
+ AUTH_DATABASE_URL: 'postgresql://auth:pass@localhost:5432/test_dev',
181
+ AUTH_DB_PASSWORD: 'pass',
182
+ WEB_URL: 'http://localhost:3002',
183
+ });
184
+
185
+ const result = reconcileSecrets(secrets, workspace);
186
+
187
+ expect(result).not.toBeNull();
188
+ expect(result!.updatedAt).not.toBe(secrets.updatedAt);
189
+ expect(result!.createdAt).toBe(secrets.createdAt);
190
+ });
191
+ });
192
+
193
+ describe('generateFullstackCustomSecrets', () => {
194
+ it('should generate BETTER_AUTH_* secrets for better-auth framework apps', () => {
195
+ const workspace = createWorkspace();
196
+
197
+ const result = generateFullstackCustomSecrets(workspace);
198
+
199
+ expect(result.BETTER_AUTH_SECRET).toBeDefined();
200
+ expect(result.BETTER_AUTH_SECRET).toMatch(/^better-auth-/);
201
+ expect(result.BETTER_AUTH_URL).toBe('http://localhost:3001');
202
+ expect(result.AUTH_PORT).toBe('3001');
203
+ expect(result.AUTH_URL).toBe('http://localhost:3001');
204
+ });
205
+
206
+ it('should include all app ports in BETTER_AUTH_TRUSTED_ORIGINS', () => {
207
+ const workspace = createWorkspace();
208
+
209
+ const result = generateFullstackCustomSecrets(workspace);
210
+
211
+ const origins = result.BETTER_AUTH_TRUSTED_ORIGINS.split(',');
212
+ expect(origins).toContain('http://localhost:3000');
213
+ expect(origins).toContain('http://localhost:3001');
214
+ expect(origins).toContain('http://localhost:3002');
215
+ });
216
+
217
+ it('should not generate BETTER_AUTH_* for non-better-auth apps', () => {
218
+ const workspace = createWorkspace({
219
+ apps: {
220
+ api: {
221
+ type: 'backend',
222
+ port: 3000,
223
+ root: '/tmp/test-project/apps/api',
224
+ path: 'apps/api',
225
+ resolvedDeployTarget: 'dokploy',
226
+ packageName: '@test/api',
227
+ routes: './src/endpoints/**/*.ts',
228
+ dependencies: [],
229
+ },
230
+ web: {
231
+ type: 'frontend',
232
+ port: 3001,
233
+ root: '/tmp/test-project/apps/web',
234
+ path: 'apps/web',
235
+ resolvedDeployTarget: 'dokploy',
236
+ packageName: '@test/web',
237
+ dependencies: ['api'],
238
+ },
239
+ },
240
+ } as Partial<NormalizedWorkspace>);
241
+
242
+ const result = generateFullstackCustomSecrets(workspace);
243
+
244
+ expect(result.BETTER_AUTH_SECRET).toBeUndefined();
245
+ expect(result.BETTER_AUTH_URL).toBeUndefined();
246
+ expect(result.BETTER_AUTH_TRUSTED_ORIGINS).toBeUndefined();
247
+ });
248
+ });
@@ -10,6 +10,7 @@ import {
10
10
  writeStageSecrets,
11
11
  } from '../secrets/storage.js';
12
12
  import { isSSMConfigured, pullSecrets, pushSecrets } from '../secrets/sync.js';
13
+ import type { StageSecrets } from '../secrets/types.js';
13
14
  import type { ComposeServiceName } from '../types.js';
14
15
  import type { LoadedConfig, NormalizedWorkspace } from '../workspace/types.js';
15
16
  import {
@@ -112,7 +113,12 @@ async function resolveSecrets(
112
113
  logger.log('🔐 Using existing local secrets');
113
114
  const secrets = await readStageSecrets(stage, workspace.root);
114
115
  if (secrets) {
115
- return secrets;
116
+ // Reconcile: add any missing workspace-derived keys without overwriting
117
+ const reconciled = reconcileSecrets(secrets, workspace);
118
+ if (reconciled) {
119
+ await writeStageSecrets(reconciled, workspace.root);
120
+ }
121
+ return reconciled ?? secrets;
116
122
  }
117
123
  }
118
124
 
@@ -137,6 +143,45 @@ async function resolveSecrets(
137
143
  return generateFreshSecrets(stage, workspace, options);
138
144
  }
139
145
 
146
+ /**
147
+ * Reconcile existing secrets with expected workspace-derived keys.
148
+ * Adds missing keys (e.g. BETTER_AUTH_*) without overwriting existing values.
149
+ * Returns the updated secrets if changes were made, or null if no changes needed.
150
+ * @internal Exported for testing
151
+ */
152
+ export function reconcileSecrets(
153
+ secrets: StageSecrets,
154
+ workspace: NormalizedWorkspace,
155
+ ): StageSecrets | null {
156
+ const isMultiApp = Object.keys(workspace.apps).length > 1;
157
+ if (!isMultiApp) {
158
+ return null;
159
+ }
160
+
161
+ const expected = generateFullstackCustomSecrets(workspace);
162
+ const missing: Record<string, string> = {};
163
+
164
+ for (const [key, value] of Object.entries(expected)) {
165
+ if (!(key in secrets.custom)) {
166
+ missing[key] = value;
167
+ }
168
+ }
169
+
170
+ if (Object.keys(missing).length === 0) {
171
+ return null;
172
+ }
173
+
174
+ logger.log(
175
+ ` 🔄 Adding missing secrets: ${Object.keys(missing).join(', ')}`,
176
+ );
177
+
178
+ return {
179
+ ...secrets,
180
+ updatedAt: new Date().toISOString(),
181
+ custom: { ...secrets.custom, ...missing },
182
+ };
183
+ }
184
+
140
185
  /**
141
186
  * Generate fresh secrets for the workspace.
142
187
  */
@@ -1,4 +1,10 @@
1
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs';
2
8
  import { tmpdir } from 'node:os';
3
9
  import { join } from 'node:path';
4
10
  import {
@@ -12,6 +18,7 @@ import {
12
18
  vi,
13
19
  } from 'vitest';
14
20
  import {
21
+ createCredentialsPreload,
15
22
  loadPortState,
16
23
  parseComposePortMappings,
17
24
  rewriteUrlsWithPorts,
@@ -274,6 +281,38 @@ services:
274
281
  );
275
282
  });
276
283
 
284
+ it('should write test-secrets.json and credentials preload', async () => {
285
+ const gkmDir = join(testDir, '.gkm');
286
+ mkdirSync(gkmDir, { recursive: true });
287
+
288
+ const secrets = {
289
+ DATABASE_URL: 'postgresql://app:secret@localhost:5433/myapp_test',
290
+ JWT_SECRET: 'test-jwt-secret',
291
+ };
292
+
293
+ const secretsJsonPath = join(gkmDir, 'test-secrets.json');
294
+ writeFileSync(secretsJsonPath, JSON.stringify(secrets, null, 2));
295
+
296
+ const preloadPath = join(gkmDir, 'test-credentials-preload.ts');
297
+ await createCredentialsPreload(preloadPath, secretsJsonPath);
298
+
299
+ // Verify secrets JSON was written
300
+ expect(existsSync(secretsJsonPath)).toBe(true);
301
+ const written = JSON.parse(readFileSync(secretsJsonPath, 'utf-8'));
302
+ expect(written.DATABASE_URL).toBe(secrets.DATABASE_URL);
303
+ expect(written.JWT_SECRET).toBe(secrets.JWT_SECRET);
304
+
305
+ // Verify preload script was created with correct content
306
+ expect(existsSync(preloadPath)).toBe(true);
307
+ const preloadContent = readFileSync(preloadPath, 'utf-8');
308
+ expect(preloadContent).toContain(
309
+ "import { Credentials } from '@geekmidas/envkit/credentials'",
310
+ );
311
+ expect(preloadContent).toContain('Object.assign(Credentials');
312
+ expect(preloadContent).toContain('Object.assign(process.env');
313
+ expect(preloadContent).toContain(secretsJsonPath);
314
+ });
315
+
277
316
  it('should handle worker template with rabbitmq ports', async () => {
278
317
  writeFileSync(
279
318
  join(testDir, 'docker-compose.yml'),
package/src/test/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { loadWorkspaceAppInfo } from '../config';
4
5
  import { sniffAppEnvironment } from '../deploy/sniffer';
5
6
  import {
7
+ createCredentialsPreload,
6
8
  loadEnvFiles,
7
9
  loadPortState,
8
10
  parseComposePortMappings,
@@ -123,6 +125,24 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
123
125
 
124
126
  console.log('');
125
127
 
128
+ // Write combined secrets to JSON and create credentials preload
129
+ const allSecrets = { ...secretsEnv, ...dependencyEnv };
130
+ const gkmDir = join(cwd, '.gkm');
131
+ await mkdir(gkmDir, { recursive: true });
132
+ const secretsJsonPath = join(gkmDir, 'test-secrets.json');
133
+ await writeFile(secretsJsonPath, JSON.stringify(allSecrets, null, 2));
134
+
135
+ const preloadPath = join(gkmDir, 'test-credentials-preload.ts');
136
+ await createCredentialsPreload(preloadPath, secretsJsonPath);
137
+
138
+ // Merge NODE_OPTIONS with existing value (if any)
139
+ const existingNodeOptions = process.env.NODE_OPTIONS ?? '';
140
+ const tsxImport = '--import=tsx';
141
+ const preloadImport = `--import=${preloadPath}`;
142
+ const nodeOptions = [existingNodeOptions, tsxImport, preloadImport]
143
+ .filter(Boolean)
144
+ .join(' ');
145
+
126
146
  // Build vitest args
127
147
  const args: string[] = [];
128
148
 
@@ -144,16 +164,15 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
144
164
  args.push(options.pattern);
145
165
  }
146
166
 
147
- // Run vitest with combined environment
167
+ // Run vitest with combined environment and credentials preload
148
168
  const vitestProcess = spawn('npx', ['vitest', ...args], {
149
169
  cwd,
150
170
  stdio: 'inherit',
151
171
  env: {
152
172
  ...process.env,
153
- ...secretsEnv,
154
- ...dependencyEnv,
155
- // Ensure NODE_ENV is set to test
173
+ ...allSecrets,
156
174
  NODE_ENV: 'test',
175
+ NODE_OPTIONS: nodeOptions,
157
176
  },
158
177
  });
159
178
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"fullstack-secrets-COWz084x.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","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 */\nconst SERVICE_DEFAULTS: Record<\n\tComposeServiceName,\n\tOmit<ServiceCredentials, 'password'>\n> = {\n\tpostgres: {\n\t\thost: 'postgres',\n\t\tport: 5432,\n\t\tusername: 'app',\n\t\tdatabase: 'app',\n\t},\n\tredis: {\n\t\thost: 'redis',\n\t\tport: 6379,\n\t\tusername: 'default',\n\t},\n\trabbitmq: {\n\t\thost: 'rabbitmq',\n\t\tport: 5672,\n\t\tusername: 'app',\n\t\tvhost: '/',\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 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\treturn urls;\n}\n\n/**\n * Create a new StageSecrets object with generated credentials.\n */\nexport function createStageSecrets(\n\tstage: string,\n\tservices: ComposeServiceName[],\n): StageSecrets {\n\tconst now = new Date().toISOString();\n\tconst serviceCredentials = generateServicesCredentials(services);\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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"}