@geekmidas/cli 0.35.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.35.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,12 +1307,79 @@ 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.
1313
1380
  */
1314
1381
  async function entryDevCommand(options: DevOptions): Promise<void> {
1315
- const { entry, port = 3000, watch = true } = options;
1382
+ const { entry, watch = true } = options;
1316
1383
 
1317
1384
  if (!entry) {
1318
1385
  throw new Error('--entry requires a file path');
@@ -1324,39 +1391,24 @@ async function entryDevCommand(options: DevOptions): Promise<void> {
1324
1391
  throw new Error(`Entry file not found: ${entryPath}`);
1325
1392
  }
1326
1393
 
1327
- logger.log(`🚀 Starting entry file: ${entry}`);
1328
-
1329
1394
  // Load .env files
1330
1395
  const defaultEnv = loadEnvFiles('.env');
1331
1396
  if (defaultEnv.loaded.length > 0) {
1332
1397
  logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
1333
1398
  }
1334
1399
 
1335
- // Determine secrets root (current dir or workspace root)
1336
- const secretsRoot = findSecretsRoot(process.cwd());
1337
- logger.log(`🔍 Secrets root: ${secretsRoot}`);
1400
+ // Prepare credentials (loads workspace config, secrets, injects PORT)
1401
+ const { credentials, resolvedPort, secretsJsonPath, appName } =
1402
+ await prepareEntryCredentials({ explicitPort: options.port });
1338
1403
 
1339
- // Determine app name for per-app secret mapping
1340
- const appName = getAppNameFromCwd() ?? undefined;
1341
1404
  if (appName) {
1342
- logger.log(`📦 App name: ${appName}`);
1405
+ logger.log(`📦 App: ${appName} (port ${resolvedPort})`);
1343
1406
  }
1344
1407
 
1345
- // Load secrets
1346
- const appSecrets = await loadSecretsForApp(secretsRoot, appName);
1347
- if (Object.keys(appSecrets).length > 0) {
1348
- logger.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
1349
- } else {
1350
- logger.log(`⚠️ No secrets found in ${secretsRoot}/.gkm/secrets/`);
1351
- }
1408
+ logger.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
1352
1409
 
1353
- // Write secrets to temp JSON file
1354
- let secretsJsonPath: string | undefined;
1355
- if (Object.keys(appSecrets).length > 0) {
1356
- const secretsDir = join(secretsRoot, '.gkm');
1357
- await mkdir(secretsDir, { recursive: true });
1358
- secretsJsonPath = join(secretsDir, 'dev-secrets.json');
1359
- 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`);
1360
1412
  }
1361
1413
 
1362
1414
  // Create wrapper entry that injects secrets before importing user's file
@@ -1366,7 +1418,7 @@ async function entryDevCommand(options: DevOptions): Promise<void> {
1366
1418
  await createEntryWrapper(wrapperPath, entryPath, secretsJsonPath);
1367
1419
 
1368
1420
  // Start with tsx
1369
- const runner = new EntryRunner(wrapperPath, entryPath, watch, port);
1421
+ const runner = new EntryRunner(wrapperPath, entryPath, watch, resolvedPort);
1370
1422
  await runner.start();
1371
1423
 
1372
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