@geekmidas/cli 0.29.0 → 0.31.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/dist/{config-BhryDQEq.cjs → config-BAE9LFC1.cjs} +2 -2
- package/dist/{config-BhryDQEq.cjs.map → config-BAE9LFC1.cjs.map} +1 -1
- package/dist/{config-C9bdq0l-.mjs → config-BC5n1a2D.mjs} +2 -2
- package/dist/{config-C9bdq0l-.mjs.map → config-BC5n1a2D.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/{index-CWN-bgrO.d.mts → index-C7TkoYmt.d.mts} +5 -1
- package/dist/index-C7TkoYmt.d.mts.map +1 -0
- package/dist/{index-DEWYvYvg.d.cts → index-CpchsC9w.d.cts} +5 -1
- package/dist/index-CpchsC9w.d.cts.map +1 -0
- package/dist/index.cjs +58 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +58 -9
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BCEFhkLh.mjs → openapi-CjYeF-Tg.mjs} +2 -2
- package/dist/{openapi-BCEFhkLh.mjs.map → openapi-CjYeF-Tg.mjs.map} +1 -1
- package/dist/{openapi-D82bBqG7.cjs → openapi-a-e3Y8WA.cjs} +2 -2
- package/dist/{openapi-D82bBqG7.cjs.map → openapi-a-e3Y8WA.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-DQjmv9lk.mjs → workspace-DFJ3sWfY.mjs} +19 -3
- package/dist/{workspace-DQjmv9lk.mjs.map → workspace-DFJ3sWfY.mjs.map} +1 -1
- package/dist/{workspace-CiZBOjf9.cjs → workspace-My0A4IRO.cjs} +19 -3
- package/dist/{workspace-CiZBOjf9.cjs.map → workspace-My0A4IRO.cjs.map} +1 -1
- package/package.json +4 -4
- package/src/dev/__tests__/entry.spec.ts +140 -0
- package/src/dev/__tests__/index.spec.ts +223 -0
- package/src/dev/index.ts +329 -5
- package/src/index.ts +30 -16
- package/src/init/__tests__/generators.spec.ts +17 -9
- package/src/init/versions.ts +1 -1
- package/src/workspace/__tests__/schema.spec.ts +114 -0
- package/src/workspace/schema.ts +23 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/index-CWN-bgrO.d.mts.map +0 -1
- package/dist/index-DEWYvYvg.d.cts.map +0 -1
package/src/dev/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess, execSync, spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { createServer } from 'node:net';
|
|
5
|
-
import { join, resolve } from 'node:path';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import chokidar from 'chokidar';
|
|
7
7
|
import { config as dotenvConfig } from 'dotenv';
|
|
8
8
|
import fg from 'fast-glob';
|
|
@@ -304,9 +304,18 @@ export interface DevOptions {
|
|
|
304
304
|
app?: string;
|
|
305
305
|
/** Filter apps by pattern (passed to turbo --filter) */
|
|
306
306
|
filter?: string;
|
|
307
|
+
/** Entry file to run (bypasses gkm config) */
|
|
308
|
+
entry?: string;
|
|
309
|
+
/** Watch for file changes (default: true with --entry) */
|
|
310
|
+
watch?: boolean;
|
|
307
311
|
}
|
|
308
312
|
|
|
309
313
|
export async function devCommand(options: DevOptions): Promise<void> {
|
|
314
|
+
// Handle --entry mode: run any file with secret injection
|
|
315
|
+
if (options.entry) {
|
|
316
|
+
return entryDevCommand(options);
|
|
317
|
+
}
|
|
318
|
+
|
|
310
319
|
// Load default .env file BEFORE loading config
|
|
311
320
|
// This ensures env vars are available when config and its dependencies are loaded
|
|
312
321
|
const defaultEnv = loadEnvFiles('.env');
|
|
@@ -318,6 +327,8 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
318
327
|
const appName = getAppNameFromCwd();
|
|
319
328
|
let config: GkmConfig;
|
|
320
329
|
let appRoot: string = process.cwd();
|
|
330
|
+
let secretsRoot: string = process.cwd(); // Where .gkm/secrets/ lives
|
|
331
|
+
let workspaceAppName: string | undefined; // Set if in workspace mode
|
|
321
332
|
|
|
322
333
|
if (appName) {
|
|
323
334
|
// Try to load app-specific config from workspace
|
|
@@ -325,6 +336,8 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
325
336
|
const appConfig = await loadAppConfig();
|
|
326
337
|
config = appConfig.gkmConfig;
|
|
327
338
|
appRoot = appConfig.appRoot;
|
|
339
|
+
secretsRoot = appConfig.workspaceRoot;
|
|
340
|
+
workspaceAppName = appConfig.appName;
|
|
328
341
|
logger.log(`📦 Running app: ${appConfig.appName}`);
|
|
329
342
|
} catch {
|
|
330
343
|
// Not in a workspace or app not found in workspace - fall back to regular loading
|
|
@@ -438,6 +451,17 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
438
451
|
// Determine runtime (default to node)
|
|
439
452
|
const runtime: Runtime = config.runtime ?? 'node';
|
|
440
453
|
|
|
454
|
+
// Load secrets for dev mode and write to JSON file
|
|
455
|
+
let secretsJsonPath: string | undefined;
|
|
456
|
+
const appSecrets = await loadSecretsForApp(secretsRoot, workspaceAppName);
|
|
457
|
+
if (Object.keys(appSecrets).length > 0) {
|
|
458
|
+
const secretsDir = join(secretsRoot, '.gkm');
|
|
459
|
+
await mkdir(secretsDir, { recursive: true });
|
|
460
|
+
secretsJsonPath = join(secretsDir, 'dev-secrets.json');
|
|
461
|
+
await writeFile(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
|
|
462
|
+
logger.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
|
|
463
|
+
}
|
|
464
|
+
|
|
441
465
|
// Start the dev server
|
|
442
466
|
const devServer = new DevServer(
|
|
443
467
|
resolved.providers[0] as LegacyProvider,
|
|
@@ -448,6 +472,7 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
448
472
|
studio,
|
|
449
473
|
runtime,
|
|
450
474
|
appRoot,
|
|
475
|
+
secretsJsonPath,
|
|
451
476
|
);
|
|
452
477
|
|
|
453
478
|
await devServer.start();
|
|
@@ -754,6 +779,54 @@ export async function loadDevSecrets(
|
|
|
754
779
|
return {};
|
|
755
780
|
}
|
|
756
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Load secrets from a path for dev mode.
|
|
784
|
+
* For single app: returns secrets as-is.
|
|
785
|
+
* For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
|
|
786
|
+
* @internal Exported for testing
|
|
787
|
+
*/
|
|
788
|
+
export async function loadSecretsForApp(
|
|
789
|
+
secretsRoot: string,
|
|
790
|
+
appName?: string,
|
|
791
|
+
): Promise<Record<string, string>> {
|
|
792
|
+
// Try 'dev' stage first, then 'development'
|
|
793
|
+
const stages = ['dev', 'development'];
|
|
794
|
+
|
|
795
|
+
let secrets: Record<string, string> = {};
|
|
796
|
+
|
|
797
|
+
for (const stage of stages) {
|
|
798
|
+
if (secretsExist(stage, secretsRoot)) {
|
|
799
|
+
const stageSecrets = await readStageSecrets(stage, secretsRoot);
|
|
800
|
+
if (stageSecrets) {
|
|
801
|
+
logger.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
802
|
+
secrets = toEmbeddableSecrets(stageSecrets);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (Object.keys(secrets).length === 0) {
|
|
809
|
+
return {};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Single app mode - no mapping needed
|
|
813
|
+
if (!appName) {
|
|
814
|
+
return secrets;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Workspace app mode - map {APP}_* to generic names
|
|
818
|
+
const prefix = appName.toUpperCase();
|
|
819
|
+
const mapped = { ...secrets };
|
|
820
|
+
|
|
821
|
+
// Map {APP}_DATABASE_URL → DATABASE_URL
|
|
822
|
+
const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
|
|
823
|
+
if (appDbUrl) {
|
|
824
|
+
mapped.DATABASE_URL = appDbUrl;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return mapped;
|
|
828
|
+
}
|
|
829
|
+
|
|
757
830
|
/**
|
|
758
831
|
* Start docker-compose services for the workspace.
|
|
759
832
|
* @internal Exported for testing
|
|
@@ -1178,6 +1251,242 @@ async function buildServer(
|
|
|
1178
1251
|
]);
|
|
1179
1252
|
}
|
|
1180
1253
|
|
|
1254
|
+
/**
|
|
1255
|
+
* Find the directory containing .gkm/secrets/.
|
|
1256
|
+
* Walks up from cwd until it finds one, or returns cwd.
|
|
1257
|
+
* @internal Exported for testing
|
|
1258
|
+
*/
|
|
1259
|
+
export function findSecretsRoot(startDir: string): string {
|
|
1260
|
+
let dir = startDir;
|
|
1261
|
+
while (dir !== '/') {
|
|
1262
|
+
if (existsSync(join(dir, '.gkm', 'secrets'))) {
|
|
1263
|
+
return dir;
|
|
1264
|
+
}
|
|
1265
|
+
const parent = dirname(dir);
|
|
1266
|
+
if (parent === dir) break;
|
|
1267
|
+
dir = parent;
|
|
1268
|
+
}
|
|
1269
|
+
return startDir;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Create a wrapper script that injects secrets before importing the entry file.
|
|
1274
|
+
* @internal Exported for testing
|
|
1275
|
+
*/
|
|
1276
|
+
export async function createEntryWrapper(
|
|
1277
|
+
wrapperPath: string,
|
|
1278
|
+
entryPath: string,
|
|
1279
|
+
secretsJsonPath?: string,
|
|
1280
|
+
): Promise<void> {
|
|
1281
|
+
const credentialsInjection = secretsJsonPath
|
|
1282
|
+
? `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
1283
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1284
|
+
|
|
1285
|
+
// Inject dev secrets into Credentials (before app import)
|
|
1286
|
+
const secretsPath = '${secretsJsonPath}';
|
|
1287
|
+
if (existsSync(secretsPath)) {
|
|
1288
|
+
Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
`
|
|
1292
|
+
: '';
|
|
1293
|
+
|
|
1294
|
+
const content = `#!/usr/bin/env node
|
|
1295
|
+
/**
|
|
1296
|
+
* Entry wrapper generated by 'gkm dev --entry'
|
|
1297
|
+
*/
|
|
1298
|
+
${credentialsInjection}// Import and run the user's entry file
|
|
1299
|
+
import '${entryPath}';
|
|
1300
|
+
`;
|
|
1301
|
+
|
|
1302
|
+
await writeFile(wrapperPath, content);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Run any TypeScript file with secret injection.
|
|
1307
|
+
* Does not require gkm.config.ts.
|
|
1308
|
+
*/
|
|
1309
|
+
async function entryDevCommand(options: DevOptions): Promise<void> {
|
|
1310
|
+
const { entry, port = 3000, watch = true } = options;
|
|
1311
|
+
|
|
1312
|
+
if (!entry) {
|
|
1313
|
+
throw new Error('--entry requires a file path');
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const entryPath = resolve(process.cwd(), entry);
|
|
1317
|
+
|
|
1318
|
+
if (!existsSync(entryPath)) {
|
|
1319
|
+
throw new Error(`Entry file not found: ${entryPath}`);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
logger.log(`🚀 Starting entry file: ${entry}`);
|
|
1323
|
+
|
|
1324
|
+
// Load .env files
|
|
1325
|
+
const defaultEnv = loadEnvFiles('.env');
|
|
1326
|
+
if (defaultEnv.loaded.length > 0) {
|
|
1327
|
+
logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Determine secrets root (current dir or workspace root)
|
|
1331
|
+
const secretsRoot = findSecretsRoot(process.cwd());
|
|
1332
|
+
|
|
1333
|
+
// Determine app name for per-app secret mapping
|
|
1334
|
+
const appName = getAppNameFromCwd() ?? undefined;
|
|
1335
|
+
|
|
1336
|
+
// Load secrets
|
|
1337
|
+
const appSecrets = await loadSecretsForApp(secretsRoot, appName);
|
|
1338
|
+
if (Object.keys(appSecrets).length > 0) {
|
|
1339
|
+
logger.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Write secrets to temp JSON file
|
|
1343
|
+
let secretsJsonPath: string | undefined;
|
|
1344
|
+
if (Object.keys(appSecrets).length > 0) {
|
|
1345
|
+
const secretsDir = join(secretsRoot, '.gkm');
|
|
1346
|
+
await mkdir(secretsDir, { recursive: true });
|
|
1347
|
+
secretsJsonPath = join(secretsDir, 'dev-secrets.json');
|
|
1348
|
+
await writeFile(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Create wrapper entry that injects secrets before importing user's file
|
|
1352
|
+
const wrapperDir = join(process.cwd(), '.gkm');
|
|
1353
|
+
await mkdir(wrapperDir, { recursive: true });
|
|
1354
|
+
const wrapperPath = join(wrapperDir, 'entry-wrapper.ts');
|
|
1355
|
+
await createEntryWrapper(wrapperPath, entryPath, secretsJsonPath);
|
|
1356
|
+
|
|
1357
|
+
// Start with tsx
|
|
1358
|
+
const runner = new EntryRunner(wrapperPath, entryPath, watch, port);
|
|
1359
|
+
await runner.start();
|
|
1360
|
+
|
|
1361
|
+
// Handle graceful shutdown
|
|
1362
|
+
let isShuttingDown = false;
|
|
1363
|
+
const shutdown = () => {
|
|
1364
|
+
if (isShuttingDown) return;
|
|
1365
|
+
isShuttingDown = true;
|
|
1366
|
+
|
|
1367
|
+
logger.log('\n🛑 Shutting down...');
|
|
1368
|
+
runner.stop();
|
|
1369
|
+
process.exit(0);
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
process.on('SIGINT', shutdown);
|
|
1373
|
+
process.on('SIGTERM', shutdown);
|
|
1374
|
+
|
|
1375
|
+
// Keep the process alive
|
|
1376
|
+
await new Promise(() => {});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Runs and watches a TypeScript entry file using tsx.
|
|
1381
|
+
*/
|
|
1382
|
+
class EntryRunner {
|
|
1383
|
+
private childProcess: ChildProcess | null = null;
|
|
1384
|
+
private watcher: ReturnType<typeof chokidar.watch> | null = null;
|
|
1385
|
+
private isRunning = false;
|
|
1386
|
+
|
|
1387
|
+
constructor(
|
|
1388
|
+
private wrapperPath: string,
|
|
1389
|
+
private entryPath: string,
|
|
1390
|
+
private watch: boolean,
|
|
1391
|
+
private port: number,
|
|
1392
|
+
) {}
|
|
1393
|
+
|
|
1394
|
+
async start(): Promise<void> {
|
|
1395
|
+
await this.runProcess();
|
|
1396
|
+
|
|
1397
|
+
if (this.watch) {
|
|
1398
|
+
// Watch the entry file's directory for changes
|
|
1399
|
+
const watchDir = dirname(this.entryPath);
|
|
1400
|
+
|
|
1401
|
+
this.watcher = chokidar.watch(watchDir, {
|
|
1402
|
+
ignored: /(^|[/\\])\../,
|
|
1403
|
+
persistent: true,
|
|
1404
|
+
ignoreInitial: true,
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
let restartTimeout: NodeJS.Timeout | null = null;
|
|
1408
|
+
|
|
1409
|
+
this.watcher.on('change', (path) => {
|
|
1410
|
+
logger.log(`📝 File changed: ${path}`);
|
|
1411
|
+
|
|
1412
|
+
// Debounce restarts
|
|
1413
|
+
if (restartTimeout) {
|
|
1414
|
+
clearTimeout(restartTimeout);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
restartTimeout = setTimeout(async () => {
|
|
1418
|
+
logger.log('🔄 Restarting...');
|
|
1419
|
+
await this.restart();
|
|
1420
|
+
}, 300);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
logger.log(`👀 Watching for changes in: ${watchDir}`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private async runProcess(): Promise<void> {
|
|
1428
|
+
// Pass PORT as environment variable
|
|
1429
|
+
const env = { ...process.env, PORT: String(this.port) };
|
|
1430
|
+
|
|
1431
|
+
this.childProcess = spawn('npx', ['tsx', this.wrapperPath], {
|
|
1432
|
+
stdio: 'inherit',
|
|
1433
|
+
env,
|
|
1434
|
+
detached: true,
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
this.isRunning = true;
|
|
1438
|
+
|
|
1439
|
+
this.childProcess.on('error', (error) => {
|
|
1440
|
+
logger.error('❌ Process error:', error);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
this.childProcess.on('exit', (code) => {
|
|
1444
|
+
if (code !== null && code !== 0 && code !== 143) {
|
|
1445
|
+
// 143 = SIGTERM
|
|
1446
|
+
logger.error(`❌ Process exited with code ${code}`);
|
|
1447
|
+
}
|
|
1448
|
+
this.isRunning = false;
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
// Give the process a moment to start
|
|
1452
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1453
|
+
|
|
1454
|
+
if (this.isRunning) {
|
|
1455
|
+
logger.log(`\n🎉 Running at http://localhost:${this.port}`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
async restart(): Promise<void> {
|
|
1460
|
+
this.stopProcess();
|
|
1461
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1462
|
+
await this.runProcess();
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
stop(): void {
|
|
1466
|
+
this.watcher?.close();
|
|
1467
|
+
this.stopProcess();
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
private stopProcess(): void {
|
|
1471
|
+
if (this.childProcess && this.isRunning) {
|
|
1472
|
+
const pid = this.childProcess.pid;
|
|
1473
|
+
if (pid) {
|
|
1474
|
+
try {
|
|
1475
|
+
process.kill(-pid, 'SIGTERM');
|
|
1476
|
+
} catch {
|
|
1477
|
+
try {
|
|
1478
|
+
process.kill(pid, 'SIGTERM');
|
|
1479
|
+
} catch {
|
|
1480
|
+
// Process already dead
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
this.childProcess = null;
|
|
1485
|
+
this.isRunning = false;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1181
1490
|
class DevServer {
|
|
1182
1491
|
private serverProcess: ChildProcess | null = null;
|
|
1183
1492
|
private isRunning = false;
|
|
@@ -1192,6 +1501,7 @@ class DevServer {
|
|
|
1192
1501
|
private studio: NormalizedStudioConfig | undefined,
|
|
1193
1502
|
private runtime: Runtime = 'node',
|
|
1194
1503
|
private appRoot: string = process.cwd(),
|
|
1504
|
+
private secretsJsonPath?: string,
|
|
1195
1505
|
) {
|
|
1196
1506
|
this.actualPort = requestedPort;
|
|
1197
1507
|
}
|
|
@@ -1341,7 +1651,7 @@ class DevServer {
|
|
|
1341
1651
|
}
|
|
1342
1652
|
|
|
1343
1653
|
private async createServerEntry(): Promise<void> {
|
|
1344
|
-
const { writeFile } = await import('node:fs/promises');
|
|
1654
|
+
const { writeFile: fsWriteFile } = await import('node:fs/promises');
|
|
1345
1655
|
const { relative, dirname } = await import('node:path');
|
|
1346
1656
|
|
|
1347
1657
|
const serverPath = join(this.appRoot, '.gkm', this.provider, 'server.ts');
|
|
@@ -1351,6 +1661,20 @@ class DevServer {
|
|
|
1351
1661
|
join(dirname(serverPath), 'app.js'),
|
|
1352
1662
|
);
|
|
1353
1663
|
|
|
1664
|
+
// Generate credentials injection code if secrets are available
|
|
1665
|
+
const credentialsInjection = this.secretsJsonPath
|
|
1666
|
+
? `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
1667
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1668
|
+
|
|
1669
|
+
// Inject dev secrets into Credentials (must happen before app import)
|
|
1670
|
+
const secretsPath = '${this.secretsJsonPath}';
|
|
1671
|
+
if (existsSync(secretsPath)) {
|
|
1672
|
+
Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
`
|
|
1676
|
+
: '';
|
|
1677
|
+
|
|
1354
1678
|
const serveCode =
|
|
1355
1679
|
this.runtime === 'bun'
|
|
1356
1680
|
? `Bun.serve({
|
|
@@ -1374,7 +1698,7 @@ class DevServer {
|
|
|
1374
1698
|
* Development server entry point
|
|
1375
1699
|
* This file is auto-generated by 'gkm dev'
|
|
1376
1700
|
*/
|
|
1377
|
-
import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : `./${relativeAppPath}`}';
|
|
1701
|
+
${credentialsInjection}import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : `./${relativeAppPath}`}';
|
|
1378
1702
|
|
|
1379
1703
|
const port = process.argv.includes('--port')
|
|
1380
1704
|
? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
|
|
@@ -1395,6 +1719,6 @@ start({
|
|
|
1395
1719
|
});
|
|
1396
1720
|
`;
|
|
1397
1721
|
|
|
1398
|
-
await
|
|
1722
|
+
await fsWriteFile(serverPath, content);
|
|
1399
1723
|
}
|
|
1400
1724
|
}
|
package/src/index.ts
CHANGED
|
@@ -136,28 +136,42 @@ program
|
|
|
136
136
|
.command('dev')
|
|
137
137
|
.description('Start development server with automatic reload')
|
|
138
138
|
.option('-p, --port <port>', 'Port to run the development server on')
|
|
139
|
+
.option('--entry <file>', 'Entry file to run (bypasses gkm config)')
|
|
140
|
+
.option('--watch', 'Watch for file changes (default: true with --entry)')
|
|
141
|
+
.option('--no-watch', 'Disable file watching')
|
|
139
142
|
.option(
|
|
140
143
|
'--enable-openapi',
|
|
141
144
|
'Enable OpenAPI documentation for development server',
|
|
142
145
|
true,
|
|
143
146
|
)
|
|
144
|
-
.action(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
.action(
|
|
148
|
+
async (options: {
|
|
149
|
+
port?: string;
|
|
150
|
+
entry?: string;
|
|
151
|
+
watch?: boolean;
|
|
152
|
+
enableOpenapi?: boolean;
|
|
153
|
+
}) => {
|
|
154
|
+
try {
|
|
155
|
+
const globalOptions = program.opts();
|
|
156
|
+
if (globalOptions.cwd) {
|
|
157
|
+
process.chdir(globalOptions.cwd);
|
|
158
|
+
}
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
await devCommand({
|
|
161
|
+
port: options.port ? Number.parseInt(options.port, 10) : 3000,
|
|
162
|
+
portExplicit: !!options.port,
|
|
163
|
+
enableOpenApi: options.enableOpenapi ?? true,
|
|
164
|
+
entry: options.entry,
|
|
165
|
+
watch: options.watch,
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(
|
|
169
|
+
error instanceof Error ? error.message : 'Command failed',
|
|
170
|
+
);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
);
|
|
161
175
|
|
|
162
176
|
program
|
|
163
177
|
.command('test')
|
|
@@ -15,9 +15,14 @@ const baseOptions: TemplateOptions = {
|
|
|
15
15
|
template: 'minimal',
|
|
16
16
|
telescope: true,
|
|
17
17
|
database: true,
|
|
18
|
-
|
|
18
|
+
studio: true,
|
|
19
|
+
loggerType: 'pino',
|
|
20
|
+
routesStructure: 'centralized-endpoints',
|
|
19
21
|
monorepo: false,
|
|
20
22
|
apiPath: '',
|
|
23
|
+
packageManager: 'pnpm',
|
|
24
|
+
deployTarget: 'dokploy',
|
|
25
|
+
services: { db: true, cache: true, mail: false },
|
|
21
26
|
};
|
|
22
27
|
|
|
23
28
|
describe('generatePackageJson', () => {
|
|
@@ -161,7 +166,8 @@ describe('generateConfigFiles', () => {
|
|
|
161
166
|
};
|
|
162
167
|
const files = generateConfigFiles(options, minimalTemplate);
|
|
163
168
|
const tsConfig = files.find((f) => f.path === 'tsconfig.json');
|
|
164
|
-
|
|
169
|
+
expect(tsConfig).toBeDefined();
|
|
170
|
+
const config = JSON.parse(tsConfig!.content);
|
|
165
171
|
expect(config.extends).toBe('../../tsconfig.json');
|
|
166
172
|
expect(config.compilerOptions.paths).toBeDefined();
|
|
167
173
|
expect(config.compilerOptions.paths['@test-project/*']).toBeDefined();
|
|
@@ -274,7 +280,8 @@ describe('generateMonorepoFiles', () => {
|
|
|
274
280
|
};
|
|
275
281
|
const files = generateMonorepoFiles(options, minimalTemplate);
|
|
276
282
|
const pkgJson = files.find((f) => f.path === 'package.json');
|
|
277
|
-
|
|
283
|
+
expect(pkgJson).toBeDefined();
|
|
284
|
+
const pkg = JSON.parse(pkgJson!.content);
|
|
278
285
|
expect(pkg.scripts.dev).toBe('turbo dev');
|
|
279
286
|
expect(pkg.scripts.build).toBe('turbo build');
|
|
280
287
|
expect(pkg.scripts.lint).toBe('biome lint .');
|
|
@@ -311,7 +318,8 @@ describe('generateModelsPackage', () => {
|
|
|
311
318
|
const pkgJson = files.find(
|
|
312
319
|
(f) => f.path === 'packages/models/package.json',
|
|
313
320
|
);
|
|
314
|
-
|
|
321
|
+
expect(pkgJson).toBeDefined();
|
|
322
|
+
const pkg = JSON.parse(pkgJson!.content);
|
|
315
323
|
expect(pkg.name).toBe('@test-project/models');
|
|
316
324
|
});
|
|
317
325
|
|
|
@@ -325,7 +333,8 @@ describe('generateModelsPackage', () => {
|
|
|
325
333
|
const pkgJson = files.find(
|
|
326
334
|
(f) => f.path === 'packages/models/package.json',
|
|
327
335
|
);
|
|
328
|
-
|
|
336
|
+
expect(pkgJson).toBeDefined();
|
|
337
|
+
const pkg = JSON.parse(pkgJson!.content);
|
|
329
338
|
expect(pkg.dependencies.zod).toBeDefined();
|
|
330
339
|
});
|
|
331
340
|
|
|
@@ -336,9 +345,7 @@ describe('generateModelsPackage', () => {
|
|
|
336
345
|
apiPath: 'apps/api',
|
|
337
346
|
};
|
|
338
347
|
const files = generateModelsPackage(options);
|
|
339
|
-
const userTs = files.find(
|
|
340
|
-
(f) => f.path === 'packages/models/src/user.ts',
|
|
341
|
-
);
|
|
348
|
+
const userTs = files.find((f) => f.path === 'packages/models/src/user.ts');
|
|
342
349
|
const commonTs = files.find(
|
|
343
350
|
(f) => f.path === 'packages/models/src/common.ts',
|
|
344
351
|
);
|
|
@@ -359,7 +366,8 @@ describe('generateModelsPackage', () => {
|
|
|
359
366
|
const tsConfig = files.find(
|
|
360
367
|
(f) => f.path === 'packages/models/tsconfig.json',
|
|
361
368
|
);
|
|
362
|
-
|
|
369
|
+
expect(tsConfig).toBeDefined();
|
|
370
|
+
const config = JSON.parse(tsConfig!.content);
|
|
363
371
|
expect(config.extends).toBe('../../tsconfig.json');
|
|
364
372
|
});
|
|
365
373
|
});
|
package/src/init/versions.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
35
35
|
'@geekmidas/constructs': '~0.6.0',
|
|
36
36
|
'@geekmidas/db': '~0.3.0',
|
|
37
37
|
'@geekmidas/emailkit': '~0.2.0',
|
|
38
|
-
'@geekmidas/envkit': '~0.
|
|
38
|
+
'@geekmidas/envkit': '~0.5.0',
|
|
39
39
|
'@geekmidas/errors': '~0.1.0',
|
|
40
40
|
'@geekmidas/events': '~0.2.0',
|
|
41
41
|
'@geekmidas/logger': '~0.4.0',
|
|
@@ -478,6 +478,120 @@ describe('WorkspaceConfigSchema', () => {
|
|
|
478
478
|
});
|
|
479
479
|
});
|
|
480
480
|
|
|
481
|
+
describe('auth app configuration', () => {
|
|
482
|
+
it('should accept auth app with provider', () => {
|
|
483
|
+
const config = {
|
|
484
|
+
apps: {
|
|
485
|
+
auth: {
|
|
486
|
+
type: 'auth' as const,
|
|
487
|
+
path: 'apps/auth',
|
|
488
|
+
port: 3002,
|
|
489
|
+
provider: 'better-auth' as const,
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const result = validateWorkspaceConfig(config);
|
|
495
|
+
|
|
496
|
+
expect(result.apps.auth.type).toBe('auth');
|
|
497
|
+
expect(result.apps.auth.provider).toBe('better-auth');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should reject auth app without provider', () => {
|
|
501
|
+
const config = {
|
|
502
|
+
apps: {
|
|
503
|
+
auth: {
|
|
504
|
+
type: 'auth' as const,
|
|
505
|
+
path: 'apps/auth',
|
|
506
|
+
port: 3002,
|
|
507
|
+
// Missing provider
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const result = safeValidateWorkspaceConfig(config);
|
|
513
|
+
|
|
514
|
+
expect(result.success).toBe(false);
|
|
515
|
+
if (result.error) {
|
|
516
|
+
const formatted = formatValidationErrors(result.error);
|
|
517
|
+
expect(formatted).toContain('Auth apps must have provider defined');
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should allow auth app with backend properties', () => {
|
|
522
|
+
const config = {
|
|
523
|
+
apps: {
|
|
524
|
+
auth: {
|
|
525
|
+
type: 'auth' as const,
|
|
526
|
+
path: 'apps/auth',
|
|
527
|
+
port: 3002,
|
|
528
|
+
provider: 'better-auth' as const,
|
|
529
|
+
envParser: './src/config/env',
|
|
530
|
+
logger: './src/logger',
|
|
531
|
+
telescope: true,
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const result = validateWorkspaceConfig(config);
|
|
537
|
+
|
|
538
|
+
expect(result.apps.auth.type).toBe('auth');
|
|
539
|
+
expect(result.apps.auth.envParser).toBe('./src/config/env');
|
|
540
|
+
expect(result.apps.auth.telescope).toBe(true);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should validate fullstack workspace with auth app', () => {
|
|
544
|
+
const config = {
|
|
545
|
+
name: 'fullstack-app',
|
|
546
|
+
apps: {
|
|
547
|
+
api: {
|
|
548
|
+
type: 'backend' as const,
|
|
549
|
+
path: 'apps/api',
|
|
550
|
+
port: 3000,
|
|
551
|
+
routes: './src/endpoints/**/*.ts',
|
|
552
|
+
dependencies: ['auth'],
|
|
553
|
+
},
|
|
554
|
+
auth: {
|
|
555
|
+
type: 'auth' as const,
|
|
556
|
+
path: 'apps/auth',
|
|
557
|
+
port: 3002,
|
|
558
|
+
provider: 'better-auth' as const,
|
|
559
|
+
},
|
|
560
|
+
web: {
|
|
561
|
+
type: 'frontend' as const,
|
|
562
|
+
path: 'apps/web',
|
|
563
|
+
port: 3001,
|
|
564
|
+
framework: 'nextjs' as const,
|
|
565
|
+
dependencies: ['api', 'auth'],
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const result = validateWorkspaceConfig(config);
|
|
571
|
+
|
|
572
|
+
expect(result.apps.api.dependencies).toEqual(['auth']);
|
|
573
|
+
expect(result.apps.auth.type).toBe('auth');
|
|
574
|
+
expect(result.apps.web.dependencies).toEqual(['api', 'auth']);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should reject invalid auth provider', () => {
|
|
578
|
+
const config = {
|
|
579
|
+
apps: {
|
|
580
|
+
auth: {
|
|
581
|
+
type: 'auth' as const,
|
|
582
|
+
path: 'apps/auth',
|
|
583
|
+
port: 3002,
|
|
584
|
+
provider: 'invalid-provider',
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const result = safeValidateWorkspaceConfig(config);
|
|
590
|
+
|
|
591
|
+
expect(result.success).toBe(false);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
481
595
|
describe('deploy target helpers', () => {
|
|
482
596
|
it('isDeployTargetSupported should return true for dokploy', () => {
|
|
483
597
|
expect(isDeployTargetSupported('dokploy')).toBe(true);
|