@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/CHANGELOG.md +6 -0
- package/dist/index.cjs +45 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +45 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/dev/index.ts +23 -8
- package/src/setup/__tests__/reconcile-secrets.spec.ts +248 -0
- package/src/setup/index.ts +46 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
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/
|
|
63
|
-
"@geekmidas/
|
|
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
|
-
|
|
1412
|
+
// Kill turbo process group
|
|
1413
|
+
const pid = turboProcess.pid;
|
|
1414
|
+
if (pid) {
|
|
1413
1415
|
try {
|
|
1414
|
-
|
|
1415
|
-
process.kill(-turboProcess.pid, 'SIGTERM');
|
|
1416
|
+
process.kill(-pid, 'SIGTERM');
|
|
1416
1417
|
} catch {
|
|
1417
|
-
|
|
1418
|
-
|
|
1418
|
+
try {
|
|
1419
|
+
process.kill(pid, 'SIGTERM');
|
|
1420
|
+
} catch {
|
|
1421
|
+
// Process already dead
|
|
1422
|
+
}
|
|
1419
1423
|
}
|
|
1420
1424
|
}
|
|
1421
1425
|
|
|
1422
|
-
//
|
|
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
|
-
},
|
|
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
|
+
});
|
package/src/setup/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
*/
|