@geekmidas/cli 0.36.0 → 0.37.0

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": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -49,10 +49,10 @@
49
49
  "openapi-typescript": "^7.4.2",
50
50
  "prompts": "~2.4.2",
51
51
  "@geekmidas/constructs": "~0.6.0",
52
- "@geekmidas/envkit": "~0.5.0",
53
52
  "@geekmidas/errors": "~0.1.0",
53
+ "@geekmidas/schema": "~0.1.0",
54
54
  "@geekmidas/logger": "~0.4.0",
55
- "@geekmidas/schema": "~0.1.0"
55
+ "@geekmidas/envkit": "~0.5.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/lodash.kebabcase": "^4.1.9",
@@ -0,0 +1,262 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { prepareEntryCredentials } from '../index';
6
+
7
+ describe('prepareEntryCredentials', () => {
8
+ let workspaceDir: string;
9
+
10
+ beforeEach(async () => {
11
+ workspaceDir = join(tmpdir(), `gkm-entry-test-${Date.now()}`);
12
+ await mkdir(workspaceDir, { recursive: true });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await rm(workspaceDir, { recursive: true, force: true });
17
+ });
18
+
19
+ describe('workspace with port config', () => {
20
+ beforeEach(async () => {
21
+ // Create workspace structure:
22
+ // workspace/
23
+ // gkm.config.ts
24
+ // apps/
25
+ // api/
26
+ // package.json
27
+ // auth/
28
+ // package.json
29
+
30
+ // Create gkm.config.ts
31
+ const gkmConfig = `
32
+ import { defineWorkspace } from '@geekmidas/cli/config';
33
+
34
+ export default defineWorkspace({
35
+ name: 'test-workspace',
36
+ apps: {
37
+ api: {
38
+ type: 'backend',
39
+ path: 'apps/api',
40
+ port: 3000,
41
+ routes: './src/endpoints/**/*.ts',
42
+ envParser: './src/config/env#envParser',
43
+ logger: './src/config/logger#logger',
44
+ },
45
+ auth: {
46
+ type: 'backend',
47
+ path: 'apps/auth',
48
+ port: 3002,
49
+ envParser: './src/config/env#envParser',
50
+ logger: './src/config/logger#logger',
51
+ },
52
+ },
53
+ services: {},
54
+ });
55
+ `;
56
+ await writeFile(join(workspaceDir, 'gkm.config.ts'), gkmConfig);
57
+
58
+ // Create apps directories
59
+ await mkdir(join(workspaceDir, 'apps', 'api', 'src'), {
60
+ recursive: true,
61
+ });
62
+ await mkdir(join(workspaceDir, 'apps', 'auth', 'src'), {
63
+ recursive: true,
64
+ });
65
+
66
+ // Create package.json for api app
67
+ await writeFile(
68
+ join(workspaceDir, 'apps', 'api', 'package.json'),
69
+ JSON.stringify({ name: '@test/api', version: '0.0.1' }, null, 2),
70
+ );
71
+
72
+ // Create package.json for auth app
73
+ await writeFile(
74
+ join(workspaceDir, 'apps', 'auth', 'package.json'),
75
+ JSON.stringify({ name: '@test/auth', version: '0.0.1' }, null, 2),
76
+ );
77
+
78
+ // Create entry files
79
+ await writeFile(
80
+ join(workspaceDir, 'apps', 'api', 'src', 'index.ts'),
81
+ 'console.log("api");',
82
+ );
83
+ await writeFile(
84
+ join(workspaceDir, 'apps', 'auth', 'src', 'index.ts'),
85
+ 'console.log("auth");',
86
+ );
87
+ });
88
+
89
+ it('should inject PORT from workspace config for api app (port 3000)', async () => {
90
+ const apiDir = join(workspaceDir, 'apps', 'api');
91
+
92
+ const result = await prepareEntryCredentials({ cwd: apiDir });
93
+
94
+ expect(result.resolvedPort).toBe(3000);
95
+ expect(result.credentials.PORT).toBe('3000');
96
+ expect(result.appName).toBe('api');
97
+ expect(result.secretsRoot).toBe(workspaceDir);
98
+ });
99
+
100
+ it('should inject PORT from workspace config for auth app (port 3002)', async () => {
101
+ const authDir = join(workspaceDir, 'apps', 'auth');
102
+
103
+ const result = await prepareEntryCredentials({ cwd: authDir });
104
+
105
+ expect(result.resolvedPort).toBe(3002);
106
+ expect(result.credentials.PORT).toBe('3002');
107
+ expect(result.appName).toBe('auth');
108
+ expect(result.secretsRoot).toBe(workspaceDir);
109
+ });
110
+
111
+ it('should use explicit --port over workspace config', async () => {
112
+ const authDir = join(workspaceDir, 'apps', 'auth');
113
+
114
+ const result = await prepareEntryCredentials({
115
+ cwd: authDir,
116
+ explicitPort: 4000,
117
+ });
118
+
119
+ expect(result.resolvedPort).toBe(4000);
120
+ expect(result.credentials.PORT).toBe('4000');
121
+ expect(result.appName).toBe('auth');
122
+ });
123
+
124
+ it('should write credentials to dev-secrets.json at workspace root', async () => {
125
+ const apiDir = join(workspaceDir, 'apps', 'api');
126
+
127
+ const result = await prepareEntryCredentials({ cwd: apiDir });
128
+
129
+ // Verify the file was written at workspace root
130
+ expect(result.secretsJsonPath).toBe(
131
+ join(workspaceDir, '.gkm', 'dev-secrets.json'),
132
+ );
133
+
134
+ // Verify file contents
135
+ const content = await readFile(result.secretsJsonPath, 'utf-8');
136
+ const parsed = JSON.parse(content);
137
+
138
+ expect(parsed.PORT).toBe('3000');
139
+ });
140
+ });
141
+
142
+ describe('without workspace config', () => {
143
+ beforeEach(async () => {
144
+ // Create a simple directory without workspace config
145
+ await mkdir(join(workspaceDir, 'src'), { recursive: true });
146
+ await writeFile(
147
+ join(workspaceDir, 'package.json'),
148
+ JSON.stringify({ name: 'standalone-app', version: '0.0.1' }, null, 2),
149
+ );
150
+ await writeFile(
151
+ join(workspaceDir, 'src', 'index.ts'),
152
+ 'console.log("standalone");',
153
+ );
154
+ });
155
+
156
+ it('should fallback to port 3000 when no workspace config exists', async () => {
157
+ const result = await prepareEntryCredentials({ cwd: workspaceDir });
158
+
159
+ expect(result.resolvedPort).toBe(3000);
160
+ expect(result.credentials.PORT).toBe('3000');
161
+ });
162
+
163
+ it('should use explicit --port when no workspace config exists', async () => {
164
+ const result = await prepareEntryCredentials({
165
+ cwd: workspaceDir,
166
+ explicitPort: 5000,
167
+ });
168
+
169
+ expect(result.resolvedPort).toBe(5000);
170
+ expect(result.credentials.PORT).toBe('5000');
171
+ });
172
+
173
+ it('should write credentials to current directory when not in workspace', async () => {
174
+ const result = await prepareEntryCredentials({ cwd: workspaceDir });
175
+
176
+ expect(result.secretsJsonPath).toBe(
177
+ join(workspaceDir, '.gkm', 'dev-secrets.json'),
178
+ );
179
+ });
180
+ });
181
+
182
+ describe('with secrets', () => {
183
+ beforeEach(async () => {
184
+ // Create workspace with secrets
185
+ const gkmConfig = `
186
+ import { defineWorkspace } from '@geekmidas/cli/config';
187
+
188
+ export default defineWorkspace({
189
+ name: 'test-workspace',
190
+ apps: {
191
+ api: {
192
+ type: 'backend',
193
+ path: 'apps/api',
194
+ port: 3000,
195
+ routes: './src/endpoints/**/*.ts',
196
+ envParser: './src/config/env#envParser',
197
+ logger: './src/config/logger#logger',
198
+ },
199
+ },
200
+ services: {},
201
+ });
202
+ `;
203
+ await writeFile(join(workspaceDir, 'gkm.config.ts'), gkmConfig);
204
+
205
+ await mkdir(join(workspaceDir, 'apps', 'api', 'src'), {
206
+ recursive: true,
207
+ });
208
+ await writeFile(
209
+ join(workspaceDir, 'apps', 'api', 'package.json'),
210
+ JSON.stringify({ name: '@test/api', version: '0.0.1' }, null, 2),
211
+ );
212
+ await writeFile(
213
+ join(workspaceDir, 'apps', 'api', 'src', 'index.ts'),
214
+ 'console.log("api");',
215
+ );
216
+
217
+ // Create unencrypted secrets (legacy format for testing)
218
+ await mkdir(join(workspaceDir, '.gkm', 'secrets'), { recursive: true });
219
+ const secrets = {
220
+ stage: 'development',
221
+ createdAt: new Date().toISOString(),
222
+ updatedAt: new Date().toISOString(),
223
+ services: {},
224
+ urls: {
225
+ DATABASE_URL: 'postgresql://localhost:5432/test',
226
+ },
227
+ custom: {
228
+ API_KEY: 'test-api-key',
229
+ },
230
+ };
231
+ await writeFile(
232
+ join(workspaceDir, '.gkm', 'secrets', 'development.json'),
233
+ JSON.stringify(secrets, null, 2),
234
+ );
235
+ });
236
+
237
+ it('should load secrets and inject PORT', async () => {
238
+ const apiDir = join(workspaceDir, 'apps', 'api');
239
+
240
+ const result = await prepareEntryCredentials({ cwd: apiDir });
241
+
242
+ expect(result.credentials.PORT).toBe('3000');
243
+ expect(result.credentials.DATABASE_URL).toBe(
244
+ 'postgresql://localhost:5432/test',
245
+ );
246
+ expect(result.credentials.API_KEY).toBe('test-api-key');
247
+ });
248
+
249
+ it('should write both secrets and PORT to dev-secrets.json', async () => {
250
+ const apiDir = join(workspaceDir, 'apps', 'api');
251
+
252
+ const result = await prepareEntryCredentials({ cwd: apiDir });
253
+
254
+ const content = await readFile(result.secretsJsonPath, 'utf-8');
255
+ const parsed = JSON.parse(content);
256
+
257
+ expect(parsed.PORT).toBe('3000');
258
+ expect(parsed.DATABASE_URL).toBe('postgresql://localhost:5432/test');
259
+ expect(parsed.API_KEY).toBe('test-api-key');
260
+ });
261
+ });
262
+ });
package/src/dev/index.ts CHANGED
@@ -1307,6 +1307,73 @@ await import('${entryPath}');
1307
1307
  await writeFile(wrapperPath, content);
1308
1308
  }
1309
1309
 
1310
+ /**
1311
+ * Result of preparing entry credentials for dev mode.
1312
+ */
1313
+ export interface EntryCredentialsResult {
1314
+ /** Credentials to inject (secrets + PORT) */
1315
+ credentials: Record<string, string>;
1316
+ /** Resolved port (from --port, workspace config, or default 3000) */
1317
+ resolvedPort: number;
1318
+ /** Path where credentials JSON was written */
1319
+ secretsJsonPath: string;
1320
+ /** Resolved app name (if in workspace) */
1321
+ appName: string | undefined;
1322
+ /** Secrets root directory */
1323
+ secretsRoot: string;
1324
+ }
1325
+
1326
+ /**
1327
+ * Prepare credentials for entry dev mode.
1328
+ * Loads workspace config, secrets, and injects PORT.
1329
+ * @internal Exported for testing
1330
+ */
1331
+ export async function prepareEntryCredentials(options: {
1332
+ explicitPort?: number;
1333
+ cwd?: string;
1334
+ }): Promise<EntryCredentialsResult> {
1335
+ const cwd = options.cwd ?? process.cwd();
1336
+
1337
+ // Try to get workspace app config for port and secrets
1338
+ let workspaceAppPort: number | undefined;
1339
+ let secretsRoot: string = cwd;
1340
+ let appName: string | undefined;
1341
+
1342
+ try {
1343
+ const appConfig = await loadAppConfig(cwd);
1344
+ workspaceAppPort = appConfig.app.port;
1345
+ secretsRoot = appConfig.workspaceRoot;
1346
+ appName = appConfig.appName;
1347
+ } catch {
1348
+ // Not in a workspace - use defaults
1349
+ secretsRoot = findSecretsRoot(cwd);
1350
+ appName = getAppNameFromCwd(cwd) ?? undefined;
1351
+ }
1352
+
1353
+ // Determine port: explicit --port > workspace config > default 3000
1354
+ const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3000;
1355
+
1356
+ // Load secrets and inject PORT
1357
+ const credentials = await loadSecretsForApp(secretsRoot, appName);
1358
+
1359
+ // Always inject PORT into credentials so apps can read it
1360
+ credentials.PORT = String(resolvedPort);
1361
+
1362
+ // Write secrets to temp JSON file (always write since we have PORT)
1363
+ const secretsDir = join(secretsRoot, '.gkm');
1364
+ await mkdir(secretsDir, { recursive: true });
1365
+ const secretsJsonPath = join(secretsDir, 'dev-secrets.json');
1366
+ await writeFile(secretsJsonPath, JSON.stringify(credentials, null, 2));
1367
+
1368
+ return {
1369
+ credentials,
1370
+ resolvedPort,
1371
+ secretsJsonPath,
1372
+ appName,
1373
+ secretsRoot,
1374
+ };
1375
+ }
1376
+
1310
1377
  /**
1311
1378
  * Run any TypeScript file with secret injection.
1312
1379
  * Does not require gkm.config.ts.
@@ -1330,46 +1397,18 @@ async function entryDevCommand(options: DevOptions): Promise<void> {
1330
1397
  logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
1331
1398
  }
1332
1399
 
1333
- // Try to get workspace app config for port and secrets
1334
- let workspaceAppPort: number | undefined;
1335
- let secretsRoot: string = process.cwd();
1336
- let appName: string | undefined;
1400
+ // Prepare credentials (loads workspace config, secrets, injects PORT)
1401
+ const { credentials, resolvedPort, secretsJsonPath, appName } =
1402
+ await prepareEntryCredentials({ explicitPort: options.port });
1337
1403
 
1338
- try {
1339
- const appConfig = await loadAppConfig();
1340
- workspaceAppPort = appConfig.app.port;
1341
- secretsRoot = appConfig.workspaceRoot;
1342
- appName = appConfig.appName;
1343
- logger.log(`📦 App: ${appName} (port ${workspaceAppPort})`);
1344
- } catch {
1345
- // Not in a workspace - use defaults
1346
- secretsRoot = findSecretsRoot(process.cwd());
1347
- appName = getAppNameFromCwd() ?? undefined;
1348
- if (appName) {
1349
- logger.log(`📦 App name: ${appName}`);
1350
- }
1404
+ if (appName) {
1405
+ logger.log(`📦 App: ${appName} (port ${resolvedPort})`);
1351
1406
  }
1352
1407
 
1353
- // Determine port: explicit --port > workspace config > default 3000
1354
- const port = options.port ?? workspaceAppPort ?? 3000;
1355
-
1356
- logger.log(`🚀 Starting entry file: ${entry} on port ${port}`);
1357
-
1358
- // Load secrets
1359
- const appSecrets = await loadSecretsForApp(secretsRoot, appName);
1360
- if (Object.keys(appSecrets).length > 0) {
1361
- logger.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
1362
- } else {
1363
- logger.log(`⚠️ No secrets found in ${secretsRoot}/.gkm/secrets/`);
1364
- }
1408
+ logger.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
1365
1409
 
1366
- // Write secrets to temp JSON file
1367
- let secretsJsonPath: string | undefined;
1368
- if (Object.keys(appSecrets).length > 0) {
1369
- const secretsDir = join(secretsRoot, '.gkm');
1370
- await mkdir(secretsDir, { recursive: true });
1371
- secretsJsonPath = join(secretsDir, 'dev-secrets.json');
1372
- await writeFile(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
1410
+ if (Object.keys(credentials).length > 1) {
1411
+ logger.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
1373
1412
  }
1374
1413
 
1375
1414
  // Create wrapper entry that injects secrets before importing user's file
@@ -1379,7 +1418,7 @@ async function entryDevCommand(options: DevOptions): Promise<void> {
1379
1418
  await createEntryWrapper(wrapperPath, entryPath, secretsJsonPath);
1380
1419
 
1381
1420
  // Start with tsx
1382
- const runner = new EntryRunner(wrapperPath, entryPath, watch, port);
1421
+ const runner = new EntryRunner(wrapperPath, entryPath, watch, resolvedPort);
1383
1422
  await runner.start();
1384
1423
 
1385
1424
  // Handle graceful shutdown
@@ -148,7 +148,6 @@ docker/.env
148
148
 
149
149
  # IDE
150
150
  .idea/
151
- .vscode/
152
151
  *.swp
153
152
  *.swo
154
153
 
@@ -207,6 +206,56 @@ export default defineConfig({
207
206
  });
208
207
  `;
209
208
 
209
+ // VSCode settings for consistent development experience
210
+ const vscodeSettings = {
211
+ 'search.exclude': {
212
+ '**/.sst': true,
213
+ '**/.gkm': true,
214
+ '**/.turbo': true,
215
+ },
216
+ 'editor.formatOnSave': true,
217
+ 'editor.defaultFormatter': 'biomejs.biome',
218
+ 'editor.codeActionsOnSave': {
219
+ 'source.fixAll.biome': 'always',
220
+ 'source.organizeImports.biome': 'always',
221
+ 'source.organizeImports': 'always',
222
+ },
223
+ '[typescriptreact]': {
224
+ 'editor.defaultFormatter': 'biomejs.biome',
225
+ },
226
+ '[typescript]': {
227
+ 'editor.defaultFormatter': 'biomejs.biome',
228
+ },
229
+ '[javascript]': {
230
+ 'editor.defaultFormatter': 'biomejs.biome',
231
+ },
232
+ '[json]': {
233
+ 'editor.defaultFormatter': 'biomejs.biome',
234
+ },
235
+ 'cSpell.words': [
236
+ 'betterauth',
237
+ 'dokploy',
238
+ 'envkit',
239
+ 'geekmidas',
240
+ 'healthcheck',
241
+ 'kysely',
242
+ 'testkit',
243
+ 'timestamptz',
244
+ 'turborepo',
245
+ options.name,
246
+ ],
247
+ };
248
+
249
+ // VSCode extensions recommendations
250
+ const vscodeExtensions = {
251
+ recommendations: [
252
+ 'biomejs.biome',
253
+ 'streetsidesoftware.code-spell-checker',
254
+ 'dbaeumer.vscode-eslint',
255
+ 'ms-azuretools.vscode-docker',
256
+ ],
257
+ };
258
+
210
259
  const files: GeneratedFile[] = [
211
260
  {
212
261
  path: 'package.json',
@@ -236,6 +285,14 @@ export default defineConfig({
236
285
  path: '.gitignore',
237
286
  content: gitignore,
238
287
  },
288
+ {
289
+ path: '.vscode/settings.json',
290
+ content: `${JSON.stringify(vscodeSettings, null, '\t')}\n`,
291
+ },
292
+ {
293
+ path: '.vscode/extensions.json',
294
+ content: `${JSON.stringify(vscodeExtensions, null, '\t')}\n`,
295
+ },
239
296
  ];
240
297
 
241
298
  // Add workspace config for fullstack template