@geekmidas/cli 0.18.0 → 0.20.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/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2640 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2635 -564
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +9 -4
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/src/dev/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
NormalizedStudioConfig,
|
|
15
15
|
NormalizedTelescopeConfig,
|
|
16
16
|
} from '../build/types';
|
|
17
|
-
import {
|
|
17
|
+
import { loadWorkspaceConfig, parseModuleConfig } from '../config';
|
|
18
18
|
import {
|
|
19
19
|
CronGenerator,
|
|
20
20
|
EndpointGenerator,
|
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
OPENAPI_OUTPUT_PATH,
|
|
27
27
|
resolveOpenApiConfig,
|
|
28
28
|
} from '../openapi';
|
|
29
|
+
import {
|
|
30
|
+
readStageSecrets,
|
|
31
|
+
secretsExist,
|
|
32
|
+
toEmbeddableSecrets,
|
|
33
|
+
} from '../secrets/storage.js';
|
|
29
34
|
import type {
|
|
30
35
|
GkmConfig,
|
|
31
36
|
LegacyProvider,
|
|
@@ -35,6 +40,17 @@ import type {
|
|
|
35
40
|
StudioConfig,
|
|
36
41
|
TelescopeConfig,
|
|
37
42
|
} from '../types';
|
|
43
|
+
import {
|
|
44
|
+
generateAllClients,
|
|
45
|
+
generateClientForFrontend,
|
|
46
|
+
getDependentFrontends,
|
|
47
|
+
normalizeRoutes,
|
|
48
|
+
} from '../workspace/client-generator.js';
|
|
49
|
+
import {
|
|
50
|
+
getAppBuildOrder,
|
|
51
|
+
getDependencyEnvVars,
|
|
52
|
+
type NormalizedWorkspace,
|
|
53
|
+
} from '../workspace/index.js';
|
|
38
54
|
|
|
39
55
|
const logger = console;
|
|
40
56
|
|
|
@@ -278,6 +294,10 @@ export interface DevOptions {
|
|
|
278
294
|
port?: number;
|
|
279
295
|
portExplicit?: boolean;
|
|
280
296
|
enableOpenApi?: boolean;
|
|
297
|
+
/** Specific app to run in workspace mode (default: all apps) */
|
|
298
|
+
app?: string;
|
|
299
|
+
/** Filter apps by pattern (passed to turbo --filter) */
|
|
300
|
+
filter?: string;
|
|
281
301
|
}
|
|
282
302
|
|
|
283
303
|
export async function devCommand(options: DevOptions): Promise<void> {
|
|
@@ -288,7 +308,17 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
288
308
|
logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
|
|
289
309
|
}
|
|
290
310
|
|
|
291
|
-
|
|
311
|
+
// Try to load workspace config first
|
|
312
|
+
const loadedConfig = await loadWorkspaceConfig();
|
|
313
|
+
|
|
314
|
+
// Route to workspace dev mode for multi-app workspaces
|
|
315
|
+
if (loadedConfig.type === 'workspace') {
|
|
316
|
+
logger.log('📦 Detected workspace configuration');
|
|
317
|
+
return workspaceDevCommand(loadedConfig.workspace, options);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Single-app mode - use existing logic
|
|
321
|
+
const config = loadedConfig.raw as GkmConfig;
|
|
292
322
|
|
|
293
323
|
// Load any additional env files specified in config
|
|
294
324
|
if (config.env) {
|
|
@@ -510,6 +540,556 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
510
540
|
process.on('SIGTERM', shutdown);
|
|
511
541
|
}
|
|
512
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Generate all dependency environment variables for all apps.
|
|
545
|
+
* Returns a flat object with all {APP_NAME}_URL variables.
|
|
546
|
+
* @internal Exported for testing
|
|
547
|
+
*/
|
|
548
|
+
export function generateAllDependencyEnvVars(
|
|
549
|
+
workspace: NormalizedWorkspace,
|
|
550
|
+
urlPrefix = 'http://localhost',
|
|
551
|
+
): Record<string, string> {
|
|
552
|
+
const env: Record<string, string> = {};
|
|
553
|
+
|
|
554
|
+
for (const appName of Object.keys(workspace.apps)) {
|
|
555
|
+
const appEnv = getDependencyEnvVars(workspace, appName, urlPrefix);
|
|
556
|
+
Object.assign(env, appEnv);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return env;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Check for port conflicts across all apps.
|
|
564
|
+
* Returns list of conflicts if any ports are duplicated.
|
|
565
|
+
* @internal Exported for testing
|
|
566
|
+
*/
|
|
567
|
+
export function checkPortConflicts(
|
|
568
|
+
workspace: NormalizedWorkspace,
|
|
569
|
+
): { app1: string; app2: string; port: number }[] {
|
|
570
|
+
const conflicts: { app1: string; app2: string; port: number }[] = [];
|
|
571
|
+
const portToApp = new Map<number, string>();
|
|
572
|
+
|
|
573
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
574
|
+
const existingApp = portToApp.get(app.port);
|
|
575
|
+
if (existingApp) {
|
|
576
|
+
conflicts.push({ app1: existingApp, app2: appName, port: app.port });
|
|
577
|
+
} else {
|
|
578
|
+
portToApp.set(app.port, appName);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return conflicts;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Next.js config file patterns to check.
|
|
587
|
+
*/
|
|
588
|
+
const NEXTJS_CONFIG_FILES = [
|
|
589
|
+
'next.config.js',
|
|
590
|
+
'next.config.ts',
|
|
591
|
+
'next.config.mjs',
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Validation result for a frontend app.
|
|
596
|
+
*/
|
|
597
|
+
export interface FrontendValidationResult {
|
|
598
|
+
appName: string;
|
|
599
|
+
valid: boolean;
|
|
600
|
+
errors: string[];
|
|
601
|
+
warnings: string[];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Validate a frontend (Next.js) app configuration.
|
|
606
|
+
* Checks for Next.js config file and dependency.
|
|
607
|
+
* @internal Exported for testing
|
|
608
|
+
*/
|
|
609
|
+
export async function validateFrontendApp(
|
|
610
|
+
appName: string,
|
|
611
|
+
appPath: string,
|
|
612
|
+
workspaceRoot: string,
|
|
613
|
+
): Promise<FrontendValidationResult> {
|
|
614
|
+
const errors: string[] = [];
|
|
615
|
+
const warnings: string[] = [];
|
|
616
|
+
const fullPath = join(workspaceRoot, appPath);
|
|
617
|
+
|
|
618
|
+
// Check for Next.js config file
|
|
619
|
+
const hasConfigFile = NEXTJS_CONFIG_FILES.some((file) =>
|
|
620
|
+
existsSync(join(fullPath, file)),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
if (!hasConfigFile) {
|
|
624
|
+
errors.push(
|
|
625
|
+
`Next.js config file not found. Expected one of: ${NEXTJS_CONFIG_FILES.join(', ')}`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Check for package.json
|
|
630
|
+
const packageJsonPath = join(fullPath, 'package.json');
|
|
631
|
+
if (existsSync(packageJsonPath)) {
|
|
632
|
+
try {
|
|
633
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
634
|
+
const pkg = require(packageJsonPath);
|
|
635
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
636
|
+
|
|
637
|
+
if (!deps.next) {
|
|
638
|
+
errors.push(
|
|
639
|
+
'Next.js not found in dependencies. Run: pnpm add next react react-dom',
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Check for dev script
|
|
644
|
+
if (!pkg.scripts?.dev) {
|
|
645
|
+
warnings.push(
|
|
646
|
+
'No "dev" script found in package.json. Turbo expects a "dev" script to run.',
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
errors.push(`Failed to read package.json at ${packageJsonPath}`);
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
errors.push(
|
|
654
|
+
`package.json not found at ${appPath}. Run: pnpm init in the app directory.`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
appName,
|
|
660
|
+
valid: errors.length === 0,
|
|
661
|
+
errors,
|
|
662
|
+
warnings,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Validate all frontend apps in the workspace.
|
|
668
|
+
* Returns validation results for each frontend app.
|
|
669
|
+
* @internal Exported for testing
|
|
670
|
+
*/
|
|
671
|
+
export async function validateFrontendApps(
|
|
672
|
+
workspace: NormalizedWorkspace,
|
|
673
|
+
): Promise<FrontendValidationResult[]> {
|
|
674
|
+
const results: FrontendValidationResult[] = [];
|
|
675
|
+
|
|
676
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
677
|
+
if (app.type === 'frontend') {
|
|
678
|
+
const result = await validateFrontendApp(
|
|
679
|
+
appName,
|
|
680
|
+
app.path,
|
|
681
|
+
workspace.root,
|
|
682
|
+
);
|
|
683
|
+
results.push(result);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return results;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Load secrets for development stage.
|
|
692
|
+
* Returns env vars to inject, or empty object if secrets not configured/found.
|
|
693
|
+
* @internal Exported for testing
|
|
694
|
+
*/
|
|
695
|
+
export async function loadDevSecrets(
|
|
696
|
+
workspace: NormalizedWorkspace,
|
|
697
|
+
): Promise<Record<string, string>> {
|
|
698
|
+
// Check if secrets are enabled in workspace config
|
|
699
|
+
if (!workspace.secrets.enabled) {
|
|
700
|
+
return {};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Try 'dev' stage first, then 'development'
|
|
704
|
+
const stages = ['dev', 'development'];
|
|
705
|
+
|
|
706
|
+
for (const stage of stages) {
|
|
707
|
+
if (secretsExist(stage, workspace.root)) {
|
|
708
|
+
const secrets = await readStageSecrets(stage, workspace.root);
|
|
709
|
+
if (secrets) {
|
|
710
|
+
logger.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
711
|
+
return toEmbeddableSecrets(secrets);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
logger.warn(
|
|
717
|
+
'⚠️ Secrets enabled but no dev/development secrets found. Run "gkm secrets:init --stage dev"',
|
|
718
|
+
);
|
|
719
|
+
return {};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Start docker-compose services for the workspace.
|
|
724
|
+
* @internal Exported for testing
|
|
725
|
+
*/
|
|
726
|
+
export async function startWorkspaceServices(
|
|
727
|
+
workspace: NormalizedWorkspace,
|
|
728
|
+
): Promise<void> {
|
|
729
|
+
const services = workspace.services;
|
|
730
|
+
if (!services.db && !services.cache && !services.mail) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const servicesToStart: string[] = [];
|
|
735
|
+
|
|
736
|
+
if (services.db) {
|
|
737
|
+
servicesToStart.push('postgres');
|
|
738
|
+
}
|
|
739
|
+
if (services.cache) {
|
|
740
|
+
servicesToStart.push('redis');
|
|
741
|
+
}
|
|
742
|
+
if (services.mail) {
|
|
743
|
+
servicesToStart.push('mailpit');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (servicesToStart.length === 0) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
logger.log(`🐳 Starting services: ${servicesToStart.join(', ')}`);
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
// Check if docker-compose.yml exists
|
|
754
|
+
const composeFile = join(workspace.root, 'docker-compose.yml');
|
|
755
|
+
if (!existsSync(composeFile)) {
|
|
756
|
+
logger.warn(
|
|
757
|
+
'⚠️ No docker-compose.yml found. Services will not be started.',
|
|
758
|
+
);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Start services with docker-compose
|
|
763
|
+
execSync(`docker compose up -d ${servicesToStart.join(' ')}`, {
|
|
764
|
+
cwd: workspace.root,
|
|
765
|
+
stdio: 'inherit',
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
logger.log('✅ Services started');
|
|
769
|
+
} catch (error) {
|
|
770
|
+
logger.error('❌ Failed to start services:', (error as Error).message);
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Workspace dev command - orchestrates multi-app development using Turbo.
|
|
777
|
+
*
|
|
778
|
+
* Flow:
|
|
779
|
+
* 1. Check for port conflicts
|
|
780
|
+
* 2. Start docker-compose services (db, cache, mail)
|
|
781
|
+
* 3. Generate dependency URLs ({APP_NAME}_URL)
|
|
782
|
+
* 4. Spawn turbo run dev with injected env vars
|
|
783
|
+
*/
|
|
784
|
+
async function workspaceDevCommand(
|
|
785
|
+
workspace: NormalizedWorkspace,
|
|
786
|
+
options: DevOptions,
|
|
787
|
+
): Promise<void> {
|
|
788
|
+
const appCount = Object.keys(workspace.apps).length;
|
|
789
|
+
const backendApps = Object.entries(workspace.apps).filter(
|
|
790
|
+
([_, app]) => app.type === 'backend',
|
|
791
|
+
);
|
|
792
|
+
const frontendApps = Object.entries(workspace.apps).filter(
|
|
793
|
+
([_, app]) => app.type === 'frontend',
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
logger.log(`\n🚀 Starting workspace: ${workspace.name}`);
|
|
797
|
+
logger.log(
|
|
798
|
+
` ${backendApps.length} backend app(s), ${frontendApps.length} frontend app(s)`,
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// Check for port conflicts
|
|
802
|
+
const conflicts = checkPortConflicts(workspace);
|
|
803
|
+
if (conflicts.length > 0) {
|
|
804
|
+
for (const conflict of conflicts) {
|
|
805
|
+
logger.error(
|
|
806
|
+
`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
throw new Error(
|
|
810
|
+
'Port conflicts detected. Please assign unique ports to each app.',
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Validate frontend apps (Next.js setup)
|
|
815
|
+
if (frontendApps.length > 0) {
|
|
816
|
+
logger.log('\n🔍 Validating frontend apps...');
|
|
817
|
+
const validationResults = await validateFrontendApps(workspace);
|
|
818
|
+
|
|
819
|
+
let hasErrors = false;
|
|
820
|
+
for (const result of validationResults) {
|
|
821
|
+
if (!result.valid) {
|
|
822
|
+
hasErrors = true;
|
|
823
|
+
logger.error(
|
|
824
|
+
`\n❌ Frontend app "${result.appName}" validation failed:`,
|
|
825
|
+
);
|
|
826
|
+
for (const error of result.errors) {
|
|
827
|
+
logger.error(` • ${error}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
for (const warning of result.warnings) {
|
|
831
|
+
logger.warn(` ⚠️ ${result.appName}: ${warning}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (hasErrors) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
'Frontend app validation failed. Fix the issues above and try again.',
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
logger.log('✅ Frontend apps validated');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Generate initial clients for frontends with backend dependencies
|
|
844
|
+
if (frontendApps.length > 0) {
|
|
845
|
+
const clientResults = await generateAllClients(workspace, { force: true });
|
|
846
|
+
const generatedCount = clientResults.filter((r) => r.generated).length;
|
|
847
|
+
if (generatedCount > 0) {
|
|
848
|
+
logger.log(`\n📦 Generated ${generatedCount} API client(s)`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Start docker-compose services
|
|
853
|
+
await startWorkspaceServices(workspace);
|
|
854
|
+
|
|
855
|
+
// Load secrets if enabled
|
|
856
|
+
const secretsEnv = await loadDevSecrets(workspace);
|
|
857
|
+
if (Object.keys(secretsEnv).length > 0) {
|
|
858
|
+
logger.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Generate dependency URLs
|
|
862
|
+
const dependencyEnv = generateAllDependencyEnvVars(workspace);
|
|
863
|
+
if (Object.keys(dependencyEnv).length > 0) {
|
|
864
|
+
logger.log('📡 Dependency URLs:');
|
|
865
|
+
for (const [key, value] of Object.entries(dependencyEnv)) {
|
|
866
|
+
logger.log(` ${key}=${value}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Build turbo filter
|
|
871
|
+
let turboFilter: string[] = [];
|
|
872
|
+
if (options.app) {
|
|
873
|
+
// Run specific app
|
|
874
|
+
if (!workspace.apps[options.app]) {
|
|
875
|
+
const appNames = Object.keys(workspace.apps).join(', ');
|
|
876
|
+
throw new Error(
|
|
877
|
+
`App "${options.app}" not found. Available apps: ${appNames}`,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
turboFilter = ['--filter', options.app];
|
|
881
|
+
logger.log(`\n🎯 Running single app: ${options.app}`);
|
|
882
|
+
} else if (options.filter) {
|
|
883
|
+
// Use custom filter
|
|
884
|
+
turboFilter = ['--filter', options.filter];
|
|
885
|
+
logger.log(`\n🔍 Using filter: ${options.filter}`);
|
|
886
|
+
} else {
|
|
887
|
+
// Run all apps
|
|
888
|
+
logger.log(`\n🎯 Running all ${appCount} apps`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// List apps and their ports
|
|
892
|
+
const buildOrder = getAppBuildOrder(workspace);
|
|
893
|
+
logger.log('\n📋 Apps (in dependency order):');
|
|
894
|
+
for (const appName of buildOrder) {
|
|
895
|
+
const app = workspace.apps[appName];
|
|
896
|
+
if (!app) continue;
|
|
897
|
+
const deps =
|
|
898
|
+
app.dependencies.length > 0
|
|
899
|
+
? ` (depends on: ${app.dependencies.join(', ')})`
|
|
900
|
+
: '';
|
|
901
|
+
logger.log(
|
|
902
|
+
` ${app.type === 'backend' ? '🔧' : '🌐'} ${appName} → http://localhost:${app.port}${deps}`,
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Prepare environment variables
|
|
907
|
+
// Order matters: secrets first, then dependencies (dependencies can override)
|
|
908
|
+
const turboEnv: Record<string, string> = {
|
|
909
|
+
...process.env,
|
|
910
|
+
...secretsEnv,
|
|
911
|
+
...dependencyEnv,
|
|
912
|
+
NODE_ENV: 'development',
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// Spawn turbo run dev
|
|
916
|
+
logger.log('\n🏃 Starting turbo run dev...\n');
|
|
917
|
+
|
|
918
|
+
const turboProcess = spawn('pnpm', ['turbo', 'run', 'dev', ...turboFilter], {
|
|
919
|
+
cwd: workspace.root,
|
|
920
|
+
stdio: 'inherit',
|
|
921
|
+
env: turboEnv,
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Set up file watcher for backend endpoint changes (smart client regeneration)
|
|
925
|
+
let endpointWatcher: ReturnType<typeof chokidar.watch> | null = null;
|
|
926
|
+
|
|
927
|
+
if (frontendApps.length > 0 && backendApps.length > 0) {
|
|
928
|
+
// Collect all backend route patterns to watch
|
|
929
|
+
const watchPatterns: string[] = [];
|
|
930
|
+
const backendRouteMap = new Map<string, string[]>(); // routes pattern -> backend app names
|
|
931
|
+
|
|
932
|
+
for (const [appName, app] of backendApps) {
|
|
933
|
+
const routePatterns = normalizeRoutes(app.routes);
|
|
934
|
+
for (const routePattern of routePatterns) {
|
|
935
|
+
const fullPattern = join(workspace.root, app.path, routePattern);
|
|
936
|
+
watchPatterns.push(fullPattern);
|
|
937
|
+
|
|
938
|
+
// Map pattern to app name for change detection
|
|
939
|
+
const patternKey = join(app.path, routePattern);
|
|
940
|
+
const existing = backendRouteMap.get(patternKey) || [];
|
|
941
|
+
backendRouteMap.set(patternKey, [...existing, appName]);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (watchPatterns.length > 0) {
|
|
946
|
+
// Resolve glob patterns to files
|
|
947
|
+
const resolvedFiles = await fg(watchPatterns, {
|
|
948
|
+
cwd: workspace.root,
|
|
949
|
+
absolute: true,
|
|
950
|
+
onlyFiles: true,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
if (resolvedFiles.length > 0) {
|
|
954
|
+
logger.log(
|
|
955
|
+
`\n👀 Watching ${resolvedFiles.length} endpoint file(s) for schema changes`,
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
endpointWatcher = chokidar.watch(resolvedFiles, {
|
|
959
|
+
ignored: /(^|[/\\])\../,
|
|
960
|
+
persistent: true,
|
|
961
|
+
ignoreInitial: true,
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
let regenerateTimeout: NodeJS.Timeout | null = null;
|
|
965
|
+
|
|
966
|
+
endpointWatcher.on('change', async (changedPath) => {
|
|
967
|
+
// Debounce regeneration
|
|
968
|
+
if (regenerateTimeout) {
|
|
969
|
+
clearTimeout(regenerateTimeout);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
regenerateTimeout = setTimeout(async () => {
|
|
973
|
+
// Find which backend app this file belongs to
|
|
974
|
+
const changedBackends: string[] = [];
|
|
975
|
+
|
|
976
|
+
for (const [appName, app] of backendApps) {
|
|
977
|
+
const routePatterns = normalizeRoutes(app.routes);
|
|
978
|
+
for (const routePattern of routePatterns) {
|
|
979
|
+
const routesDir = join(
|
|
980
|
+
workspace.root,
|
|
981
|
+
app.path,
|
|
982
|
+
routePattern.split('*')[0] || '',
|
|
983
|
+
);
|
|
984
|
+
if (changedPath.startsWith(routesDir.replace(/\/$/, ''))) {
|
|
985
|
+
changedBackends.push(appName);
|
|
986
|
+
break; // Found a match, no need to check other patterns
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (changedBackends.length === 0) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Find frontends that depend on changed backends
|
|
996
|
+
const affectedFrontends = new Set<string>();
|
|
997
|
+
for (const backend of changedBackends) {
|
|
998
|
+
const dependents = getDependentFrontends(workspace, backend);
|
|
999
|
+
for (const frontend of dependents) {
|
|
1000
|
+
affectedFrontends.add(frontend);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (affectedFrontends.size === 0) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Regenerate clients for affected frontends
|
|
1009
|
+
logger.log(
|
|
1010
|
+
`\n🔄 Detected schema change in ${changedBackends.join(', ')}`,
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
for (const frontend of affectedFrontends) {
|
|
1014
|
+
try {
|
|
1015
|
+
const results = await generateClientForFrontend(
|
|
1016
|
+
workspace,
|
|
1017
|
+
frontend,
|
|
1018
|
+
);
|
|
1019
|
+
for (const result of results) {
|
|
1020
|
+
if (result.generated) {
|
|
1021
|
+
logger.log(
|
|
1022
|
+
` 📦 Regenerated client for ${result.frontendApp} (${result.endpointCount} endpoints)`,
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
logger.error(
|
|
1028
|
+
` ❌ Failed to regenerate client for ${frontend}: ${(error as Error).message}`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}, 500); // 500ms debounce
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Handle graceful shutdown
|
|
1039
|
+
let isShuttingDown = false;
|
|
1040
|
+
const shutdown = () => {
|
|
1041
|
+
if (isShuttingDown) return;
|
|
1042
|
+
isShuttingDown = true;
|
|
1043
|
+
|
|
1044
|
+
logger.log('\n🛑 Shutting down workspace...');
|
|
1045
|
+
|
|
1046
|
+
// Close endpoint watcher
|
|
1047
|
+
if (endpointWatcher) {
|
|
1048
|
+
endpointWatcher.close().catch(() => {});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Kill turbo process
|
|
1052
|
+
if (turboProcess.pid) {
|
|
1053
|
+
try {
|
|
1054
|
+
// Try to kill the process group
|
|
1055
|
+
process.kill(-turboProcess.pid, 'SIGTERM');
|
|
1056
|
+
} catch {
|
|
1057
|
+
// Fall back to killing just the process
|
|
1058
|
+
turboProcess.kill('SIGTERM');
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Give processes time to clean up
|
|
1063
|
+
setTimeout(() => {
|
|
1064
|
+
process.exit(0);
|
|
1065
|
+
}, 2000);
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
process.on('SIGINT', shutdown);
|
|
1069
|
+
process.on('SIGTERM', shutdown);
|
|
1070
|
+
|
|
1071
|
+
// Wait for turbo to exit
|
|
1072
|
+
return new Promise((resolve, reject) => {
|
|
1073
|
+
turboProcess.on('error', (error) => {
|
|
1074
|
+
logger.error('❌ Turbo error:', error);
|
|
1075
|
+
reject(error);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
turboProcess.on('exit', (code) => {
|
|
1079
|
+
// Close watcher on exit
|
|
1080
|
+
if (endpointWatcher) {
|
|
1081
|
+
endpointWatcher.close().catch(() => {});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (code !== null && code !== 0) {
|
|
1085
|
+
reject(new Error(`Turbo exited with code ${code}`));
|
|
1086
|
+
} else {
|
|
1087
|
+
resolve();
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
513
1093
|
async function buildServer(
|
|
514
1094
|
config: any,
|
|
515
1095
|
context: BuildContext,
|