@geekmidas/cli 1.10.2 → 1.10.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.2",
3
+ "version": "1.10.3",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,11 +56,11 @@
56
56
  "prompts": "~2.4.2",
57
57
  "tsx": "~4.20.3",
58
58
  "yaml": "~2.8.2",
59
- "@geekmidas/constructs": "~3.0.2",
60
59
  "@geekmidas/envkit": "~1.0.3",
60
+ "@geekmidas/constructs": "~3.0.2",
61
61
  "@geekmidas/errors": "~1.0.0",
62
- "@geekmidas/schema": "~1.0.0",
63
- "@geekmidas/logger": "~1.0.0"
62
+ "@geekmidas/logger": "~1.0.0",
63
+ "@geekmidas/schema": "~1.0.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/lodash.kebabcase": "^4.1.9",
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);
@@ -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
  */