@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.
Files changed (118) hide show
  1. package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
  2. package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
  3. package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
  4. package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
  5. package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
  6. package/dist/config-BaYqrF3n.mjs.map +1 -0
  7. package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
  8. package/dist/config-CxrLu8ia.cjs.map +1 -0
  9. package/dist/config.cjs +4 -1
  10. package/dist/config.d.cts +27 -2
  11. package/dist/config.d.cts.map +1 -1
  12. package/dist/config.d.mts +27 -2
  13. package/dist/config.d.mts.map +1 -1
  14. package/dist/config.mjs +3 -2
  15. package/dist/dokploy-api-B0w17y4_.mjs +3 -0
  16. package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
  17. package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
  18. package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
  19. package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
  20. package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
  21. package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
  22. package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
  23. package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
  24. package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  25. package/dist/index-CWN-bgrO.d.mts +495 -0
  26. package/dist/index-CWN-bgrO.d.mts.map +1 -0
  27. package/dist/index-DEWYvYvg.d.cts +495 -0
  28. package/dist/index-DEWYvYvg.d.cts.map +1 -0
  29. package/dist/index.cjs +2640 -564
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +2635 -564
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
  34. package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
  35. package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
  36. package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
  38. package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
  39. package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
  40. package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
  41. package/dist/openapi-react-query.cjs +1 -1
  42. package/dist/openapi-react-query.mjs +1 -1
  43. package/dist/openapi.cjs +3 -2
  44. package/dist/openapi.d.cts +1 -1
  45. package/dist/openapi.d.mts +1 -1
  46. package/dist/openapi.mjs +3 -2
  47. package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
  48. package/dist/storage-BPRgh3DU.cjs.map +1 -0
  49. package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
  50. package/dist/storage-Dhst7BhI.mjs +272 -0
  51. package/dist/storage-Dhst7BhI.mjs.map +1 -0
  52. package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
  53. package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
  54. package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
  55. package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
  56. package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
  57. package/dist/workspace/index.cjs +19 -0
  58. package/dist/workspace/index.d.cts +3 -0
  59. package/dist/workspace/index.d.mts +3 -0
  60. package/dist/workspace/index.mjs +3 -0
  61. package/dist/workspace-CPLEZDZf.mjs +3788 -0
  62. package/dist/workspace-CPLEZDZf.mjs.map +1 -0
  63. package/dist/workspace-iWgBlX6h.cjs +3885 -0
  64. package/dist/workspace-iWgBlX6h.cjs.map +1 -0
  65. package/package.json +9 -4
  66. package/src/build/__tests__/workspace-build.spec.ts +215 -0
  67. package/src/build/index.ts +189 -1
  68. package/src/config.ts +71 -14
  69. package/src/deploy/__tests__/docker.spec.ts +1 -1
  70. package/src/deploy/__tests__/index.spec.ts +305 -1
  71. package/src/deploy/index.ts +426 -4
  72. package/src/deploy/types.ts +32 -0
  73. package/src/dev/__tests__/index.spec.ts +572 -1
  74. package/src/dev/index.ts +582 -2
  75. package/src/docker/__tests__/compose.spec.ts +425 -0
  76. package/src/docker/__tests__/templates.spec.ts +145 -0
  77. package/src/docker/compose.ts +248 -0
  78. package/src/docker/index.ts +159 -3
  79. package/src/docker/templates.ts +219 -4
  80. package/src/index.ts +24 -0
  81. package/src/init/__tests__/generators.spec.ts +17 -24
  82. package/src/init/__tests__/init.spec.ts +157 -5
  83. package/src/init/generators/auth.ts +220 -0
  84. package/src/init/generators/config.ts +61 -4
  85. package/src/init/generators/docker.ts +115 -8
  86. package/src/init/generators/env.ts +7 -127
  87. package/src/init/generators/index.ts +1 -0
  88. package/src/init/generators/models.ts +3 -1
  89. package/src/init/generators/monorepo.ts +154 -10
  90. package/src/init/generators/package.ts +5 -3
  91. package/src/init/generators/web.ts +213 -0
  92. package/src/init/index.ts +290 -58
  93. package/src/init/templates/api.ts +38 -29
  94. package/src/init/templates/index.ts +132 -4
  95. package/src/init/templates/minimal.ts +33 -35
  96. package/src/init/templates/serverless.ts +16 -19
  97. package/src/init/templates/worker.ts +50 -25
  98. package/src/init/versions.ts +47 -0
  99. package/src/secrets/keystore.ts +144 -0
  100. package/src/secrets/storage.ts +109 -6
  101. package/src/test/index.ts +97 -0
  102. package/src/workspace/__tests__/client-generator.spec.ts +357 -0
  103. package/src/workspace/__tests__/index.spec.ts +543 -0
  104. package/src/workspace/__tests__/schema.spec.ts +519 -0
  105. package/src/workspace/__tests__/type-inference.spec.ts +251 -0
  106. package/src/workspace/client-generator.ts +307 -0
  107. package/src/workspace/index.ts +372 -0
  108. package/src/workspace/schema.ts +368 -0
  109. package/src/workspace/types.ts +336 -0
  110. package/tsconfig.tsbuildinfo +1 -1
  111. package/tsdown.config.ts +1 -0
  112. package/dist/config-AmInkU7k.cjs.map +0 -1
  113. package/dist/config-DYULeEv8.mjs.map +0 -1
  114. package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
  115. package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
  116. package/dist/storage-BaOP55oq.mjs +0 -147
  117. package/dist/storage-BaOP55oq.mjs.map +0 -1
  118. package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -15,6 +15,11 @@
15
15
  "import": "./dist/config.mjs",
16
16
  "require": "./dist/config.cjs"
17
17
  },
18
+ "./workspace": {
19
+ "types": "./dist/workspace/index.d.ts",
20
+ "import": "./dist/workspace/index.mjs",
21
+ "require": "./dist/workspace/index.cjs"
22
+ },
18
23
  "./openapi": {
19
24
  "types": "./dist/openapi.d.ts",
20
25
  "import": "./dist/openapi.mjs",
@@ -53,11 +58,11 @@
53
58
  "@geekmidas/testkit": "0.6.0"
54
59
  },
55
60
  "peerDependencies": {
56
- "@geekmidas/constructs": "~0.6.0",
57
61
  "@geekmidas/envkit": "~0.4.0",
62
+ "@geekmidas/logger": "~0.4.0",
63
+ "@geekmidas/constructs": "~0.6.0",
58
64
  "@geekmidas/schema": "~0.1.0",
59
- "@geekmidas/telescope": "~0.4.0",
60
- "@geekmidas/logger": "~0.4.0"
65
+ "@geekmidas/telescope": "~0.4.0"
61
66
  },
62
67
  "peerDependenciesMeta": {
63
68
  "@geekmidas/telescope": {
@@ -0,0 +1,215 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import type { NormalizedWorkspace } from '../../workspace/types.js';
6
+ import {
7
+ type AppBuildResult,
8
+ detectPackageManager,
9
+ getTurboCommand,
10
+ } from '../index.js';
11
+
12
+ describe('Workspace Build Command', () => {
13
+ describe('detectPackageManager', () => {
14
+ let testDir: string;
15
+ let originalCwd: string;
16
+
17
+ beforeEach(() => {
18
+ testDir = join(
19
+ tmpdir(),
20
+ `gkm-build-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
21
+ );
22
+ mkdirSync(testDir, { recursive: true });
23
+ originalCwd = process.cwd();
24
+ process.chdir(testDir);
25
+ });
26
+
27
+ afterEach(() => {
28
+ process.chdir(originalCwd);
29
+ if (testDir) {
30
+ rmSync(testDir, { recursive: true, force: true });
31
+ }
32
+ });
33
+
34
+ it('should detect pnpm from lock file', () => {
35
+ writeFileSync(join(testDir, 'pnpm-lock.yaml'), '');
36
+ expect(detectPackageManager()).toBe('pnpm');
37
+ });
38
+
39
+ it('should detect yarn from lock file', () => {
40
+ writeFileSync(join(testDir, 'yarn.lock'), '');
41
+ expect(detectPackageManager()).toBe('yarn');
42
+ });
43
+
44
+ it('should default to npm when no lock file exists', () => {
45
+ expect(detectPackageManager()).toBe('npm');
46
+ });
47
+
48
+ it('should prefer pnpm over yarn when both exist', () => {
49
+ writeFileSync(join(testDir, 'pnpm-lock.yaml'), '');
50
+ writeFileSync(join(testDir, 'yarn.lock'), '');
51
+ expect(detectPackageManager()).toBe('pnpm');
52
+ });
53
+ });
54
+
55
+ describe('getTurboCommand', () => {
56
+ it('should generate pnpm turbo command', () => {
57
+ expect(getTurboCommand('pnpm')).toBe('pnpm exec turbo run build');
58
+ });
59
+
60
+ it('should generate yarn turbo command', () => {
61
+ expect(getTurboCommand('yarn')).toBe('yarn turbo run build');
62
+ });
63
+
64
+ it('should generate npm turbo command', () => {
65
+ expect(getTurboCommand('npm')).toBe('npx turbo run build');
66
+ });
67
+
68
+ it('should add filter argument when provided', () => {
69
+ expect(getTurboCommand('pnpm', 'api')).toBe(
70
+ 'pnpm exec turbo run build --filter=api',
71
+ );
72
+ });
73
+
74
+ it('should add filter for yarn', () => {
75
+ expect(getTurboCommand('yarn', 'web')).toBe(
76
+ 'yarn turbo run build --filter=web',
77
+ );
78
+ });
79
+
80
+ it('should add filter for npm', () => {
81
+ expect(getTurboCommand('npm', '@myapp/api')).toBe(
82
+ 'npx turbo run build --filter=@myapp/api',
83
+ );
84
+ });
85
+ });
86
+
87
+ describe('AppBuildResult type', () => {
88
+ it('should have correct structure for successful build', () => {
89
+ const result: AppBuildResult = {
90
+ appName: 'api',
91
+ type: 'backend',
92
+ success: true,
93
+ outputPath: '/path/to/.gkm',
94
+ };
95
+
96
+ expect(result.appName).toBe('api');
97
+ expect(result.type).toBe('backend');
98
+ expect(result.success).toBe(true);
99
+ expect(result.outputPath).toBe('/path/to/.gkm');
100
+ expect(result.error).toBeUndefined();
101
+ });
102
+
103
+ it('should have correct structure for failed build', () => {
104
+ const result: AppBuildResult = {
105
+ appName: 'web',
106
+ type: 'frontend',
107
+ success: false,
108
+ error: 'Build failed',
109
+ };
110
+
111
+ expect(result.appName).toBe('web');
112
+ expect(result.type).toBe('frontend');
113
+ expect(result.success).toBe(false);
114
+ expect(result.error).toBe('Build failed');
115
+ expect(result.outputPath).toBeUndefined();
116
+ });
117
+ });
118
+
119
+ describe('workspace build integration', () => {
120
+ it('should correctly categorize apps by type', () => {
121
+ const workspace: NormalizedWorkspace = {
122
+ name: 'test-workspace',
123
+ root: '/test',
124
+ apps: {
125
+ api: {
126
+ type: 'backend',
127
+ path: 'apps/api',
128
+ port: 3000,
129
+ dependencies: [],
130
+ routes: './src/**/*.ts',
131
+ },
132
+ auth: {
133
+ type: 'backend',
134
+ path: 'apps/auth',
135
+ port: 3001,
136
+ dependencies: [],
137
+ routes: './src/**/*.ts',
138
+ },
139
+ web: {
140
+ type: 'frontend',
141
+ path: 'apps/web',
142
+ port: 3002,
143
+ dependencies: ['api', 'auth'],
144
+ framework: 'nextjs',
145
+ },
146
+ admin: {
147
+ type: 'frontend',
148
+ path: 'apps/admin',
149
+ port: 3003,
150
+ dependencies: ['api'],
151
+ framework: 'nextjs',
152
+ },
153
+ },
154
+ services: {},
155
+ deploy: { default: 'dokploy' },
156
+ shared: { packages: [] },
157
+ secrets: {},
158
+ };
159
+
160
+ const apps = Object.entries(workspace.apps);
161
+ const backendApps = apps.filter(([, app]) => app.type === 'backend');
162
+ const frontendApps = apps.filter(([, app]) => app.type === 'frontend');
163
+
164
+ expect(backendApps).toHaveLength(2);
165
+ expect(frontendApps).toHaveLength(2);
166
+ expect(backendApps.map(([name]) => name)).toContain('api');
167
+ expect(backendApps.map(([name]) => name)).toContain('auth');
168
+ expect(frontendApps.map(([name]) => name)).toContain('web');
169
+ expect(frontendApps.map(([name]) => name)).toContain('admin');
170
+ });
171
+
172
+ it('should generate correct output paths for different app types', () => {
173
+ const workspace: NormalizedWorkspace = {
174
+ name: 'test',
175
+ root: '/workspace',
176
+ apps: {
177
+ api: {
178
+ type: 'backend',
179
+ path: 'apps/api',
180
+ port: 3000,
181
+ dependencies: [],
182
+ },
183
+ web: {
184
+ type: 'frontend',
185
+ path: 'apps/web',
186
+ port: 3001,
187
+ dependencies: [],
188
+ },
189
+ },
190
+ services: {},
191
+ deploy: { default: 'dokploy' },
192
+ shared: { packages: [] },
193
+ secrets: {},
194
+ };
195
+
196
+ // Test that backend apps output to .gkm
197
+ const backendApp = workspace.apps.api!;
198
+ const backendOutputPath = join(
199
+ workspace.root,
200
+ backendApp.path,
201
+ backendApp.type === 'backend' ? '.gkm' : '.next',
202
+ );
203
+ expect(backendOutputPath).toBe('/workspace/apps/api/.gkm');
204
+
205
+ // Test that frontend apps output to .next
206
+ const frontendApp = workspace.apps.web!;
207
+ const frontendOutputPath = join(
208
+ workspace.root,
209
+ frontendApp.path,
210
+ frontendApp.type === 'frontend' ? '.next' : '.gkm',
211
+ );
212
+ expect(frontendOutputPath).toBe('/workspace/apps/web/.next');
213
+ });
214
+ });
215
+ });
@@ -1,10 +1,12 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
1
3
  import { mkdir } from 'node:fs/promises';
2
4
  import { join, relative } from 'node:path';
3
5
  import type { Cron } from '@geekmidas/constructs/crons';
4
6
  import type { Endpoint } from '@geekmidas/constructs/endpoints';
5
7
  import type { Function } from '@geekmidas/constructs/functions';
6
8
  import type { Subscriber } from '@geekmidas/constructs/subscribers';
7
- import { loadConfig, parseModuleConfig } from '../config';
9
+ import { loadConfig, loadWorkspaceConfig, parseModuleConfig } from '../config';
8
10
  import {
9
11
  getProductionConfigFromGkm,
10
12
  normalizeHooksConfig,
@@ -25,6 +27,11 @@ import type {
25
27
  LegacyProvider,
26
28
  RouteInfo,
27
29
  } from '../types';
30
+ import {
31
+ getAppBuildOrder,
32
+ type NormalizedAppConfig,
33
+ type NormalizedWorkspace,
34
+ } from '../workspace/index.js';
28
35
  import {
29
36
  generateAwsManifest,
30
37
  generateServerManifest,
@@ -38,6 +45,16 @@ const logger = console;
38
45
  export async function buildCommand(
39
46
  options: BuildOptions,
40
47
  ): Promise<BuildResult> {
48
+ // Load config with workspace detection
49
+ const loadedConfig = await loadWorkspaceConfig();
50
+
51
+ // Route to workspace build mode for multi-app workspaces
52
+ if (loadedConfig.type === 'workspace') {
53
+ logger.log('📦 Detected workspace configuration');
54
+ return workspaceBuildCommand(loadedConfig.workspace, options);
55
+ }
56
+
57
+ // Single-app build - use existing logic
41
58
  const config = await loadConfig();
42
59
 
43
60
  // Resolve providers from new config format
@@ -299,3 +316,174 @@ async function buildForProvider(
299
316
 
300
317
  return {};
301
318
  }
319
+
320
+ /**
321
+ * Result of building a single app in a workspace.
322
+ */
323
+ export interface AppBuildResult {
324
+ appName: string;
325
+ type: 'backend' | 'frontend';
326
+ success: boolean;
327
+ outputPath?: string;
328
+ error?: string;
329
+ }
330
+
331
+ /**
332
+ * Result of workspace build command.
333
+ */
334
+ export interface WorkspaceBuildResult extends BuildResult {
335
+ apps: AppBuildResult[];
336
+ }
337
+
338
+ /**
339
+ * Detect available package manager.
340
+ * @internal Exported for testing
341
+ */
342
+ export function detectPackageManager(): 'pnpm' | 'npm' | 'yarn' {
343
+ if (existsSync('pnpm-lock.yaml')) return 'pnpm';
344
+ if (existsSync('yarn.lock')) return 'yarn';
345
+ return 'npm';
346
+ }
347
+
348
+ /**
349
+ * Get the turbo command for running builds.
350
+ * @internal Exported for testing
351
+ */
352
+ export function getTurboCommand(
353
+ pm: 'pnpm' | 'npm' | 'yarn',
354
+ filter?: string,
355
+ ): string {
356
+ const filterArg = filter ? ` --filter=${filter}` : '';
357
+ switch (pm) {
358
+ case 'pnpm':
359
+ return `pnpm exec turbo run build${filterArg}`;
360
+ case 'yarn':
361
+ return `yarn turbo run build${filterArg}`;
362
+ case 'npm':
363
+ return `npx turbo run build${filterArg}`;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Build all apps in a workspace using Turbo for dependency-ordered parallel builds.
369
+ * @internal Exported for testing
370
+ */
371
+ export async function workspaceBuildCommand(
372
+ workspace: NormalizedWorkspace,
373
+ options: BuildOptions,
374
+ ): Promise<WorkspaceBuildResult> {
375
+ const results: AppBuildResult[] = [];
376
+ const apps = Object.entries(workspace.apps);
377
+ const backendApps = apps.filter(([, app]) => app.type === 'backend');
378
+ const frontendApps = apps.filter(([, app]) => app.type === 'frontend');
379
+
380
+ logger.log(`\n🏗️ Building workspace: ${workspace.name}`);
381
+ logger.log(
382
+ ` Backend apps: ${backendApps.map(([name]) => name).join(', ') || 'none'}`,
383
+ );
384
+ logger.log(
385
+ ` Frontend apps: ${frontendApps.map(([name]) => name).join(', ') || 'none'}`,
386
+ );
387
+
388
+ if (options.production) {
389
+ logger.log(` 🏭 Production mode enabled`);
390
+ }
391
+
392
+ // Get build order (topologically sorted by dependencies)
393
+ const buildOrder = getAppBuildOrder(workspace);
394
+ logger.log(` Build order: ${buildOrder.join(' → ')}`);
395
+
396
+ // Use Turbo for parallel builds with dependency awareness
397
+ const pm = detectPackageManager();
398
+ logger.log(`\n📦 Using ${pm} with Turbo for parallel builds...\n`);
399
+
400
+ try {
401
+ // Run turbo build which handles dependency ordering and parallelization
402
+ const turboCommand = getTurboCommand(pm);
403
+ logger.log(`Running: ${turboCommand}`);
404
+
405
+ await new Promise<void>((resolve, reject) => {
406
+ const child = spawn(turboCommand, {
407
+ shell: true,
408
+ cwd: workspace.root,
409
+ stdio: 'inherit',
410
+ env: {
411
+ ...process.env,
412
+ // Pass production flag to builds
413
+ NODE_ENV: options.production ? 'production' : 'development',
414
+ },
415
+ });
416
+
417
+ child.on('close', (code) => {
418
+ if (code === 0) {
419
+ resolve();
420
+ } else {
421
+ reject(new Error(`Turbo build failed with exit code ${code}`));
422
+ }
423
+ });
424
+
425
+ child.on('error', (err) => {
426
+ reject(err);
427
+ });
428
+ });
429
+
430
+ // Mark all apps as successful
431
+ for (const [appName, app] of apps) {
432
+ const outputPath = getAppOutputPath(workspace, appName, app);
433
+ results.push({
434
+ appName,
435
+ type: app.type,
436
+ success: true,
437
+ outputPath,
438
+ });
439
+ }
440
+
441
+ logger.log(`\n✅ Workspace build complete!`);
442
+
443
+ // Summary
444
+ logger.log(`\n📋 Build Summary:`);
445
+ for (const result of results) {
446
+ const icon = result.type === 'backend' ? '⚙️' : '🌐';
447
+ logger.log(
448
+ ` ${icon} ${result.appName}: ${result.outputPath || 'built'}`,
449
+ );
450
+ }
451
+ } catch (error) {
452
+ const errorMessage =
453
+ error instanceof Error ? error.message : 'Build failed';
454
+ logger.log(`\n❌ Build failed: ${errorMessage}`);
455
+
456
+ // Mark all apps as failed
457
+ for (const [appName, app] of apps) {
458
+ results.push({
459
+ appName,
460
+ type: app.type,
461
+ success: false,
462
+ error: errorMessage,
463
+ });
464
+ }
465
+
466
+ throw error;
467
+ }
468
+
469
+ return { apps: results };
470
+ }
471
+
472
+ /**
473
+ * Get the output path for a built app.
474
+ */
475
+ function getAppOutputPath(
476
+ workspace: NormalizedWorkspace,
477
+ _appName: string,
478
+ app: NormalizedAppConfig,
479
+ ): string {
480
+ const appPath = join(workspace.root, app.path);
481
+
482
+ if (app.type === 'frontend') {
483
+ // Next.js standalone output
484
+ return join(appPath, '.next');
485
+ } else {
486
+ // Backend .gkm output
487
+ return join(appPath, '.gkm');
488
+ }
489
+ }
package/src/config.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import type { GkmConfig } from './types.ts';
3
+ import type { GkmConfig } from './types.js';
4
+ import {
5
+ isWorkspaceConfig,
6
+ type LoadedConfig,
7
+ processConfig,
8
+ type WorkspaceConfig,
9
+ } from './workspace/index.js';
4
10
 
5
- export type { GkmConfig } from './types.ts';
11
+ export type { GkmConfig } from './types.js';
12
+ export type { LoadedConfig, WorkspaceConfig } from './workspace/index.js';
13
+ export { defineWorkspace } from './workspace/index.js';
6
14
  /**
7
15
  * Define GKM configuration with full TypeScript support.
8
16
  * This is an identity function that provides type safety and autocomplete.
@@ -62,32 +70,81 @@ export function parseModuleConfig(
62
70
  return { path, importPattern };
63
71
  }
64
72
 
65
- export async function loadConfig(
66
- cwd: string = process.cwd(),
67
- ): Promise<GkmConfig> {
73
+ /**
74
+ * Find and return the path to the config file.
75
+ */
76
+ function findConfigPath(cwd: string): string {
68
77
  const files = ['gkm.config.json', 'gkm.config.ts', 'gkm.config.js'];
69
- let configPath = '';
70
78
 
71
79
  for (const file of files) {
72
80
  const path = join(cwd, file);
73
81
  if (existsSync(path)) {
74
- configPath = path;
75
- break;
82
+ return path;
76
83
  }
77
84
  }
78
85
 
79
- if (!configPath) {
80
- throw new Error(
81
- 'Configuration file not found. Please create gkm.config.json, gkm.config.ts, or gkm.config.js in the project root.',
82
- );
83
- }
86
+ throw new Error(
87
+ 'Configuration file not found. Please create gkm.config.json, gkm.config.ts, or gkm.config.js in the project root.',
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Load raw configuration from file.
93
+ */
94
+ async function loadRawConfig(
95
+ cwd: string,
96
+ ): Promise<GkmConfig | WorkspaceConfig> {
97
+ const configPath = findConfigPath(cwd);
84
98
 
85
99
  try {
86
100
  const config = await import(configPath);
87
101
  return config.default;
88
102
  } catch (error) {
103
+ throw new Error(`Failed to load config: ${(error as Error).message}`);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Load configuration file (single-app format).
109
+ * For backwards compatibility with existing code.
110
+ *
111
+ * @deprecated Use loadWorkspaceConfig for new code
112
+ */
113
+ export async function loadConfig(
114
+ cwd: string = process.cwd(),
115
+ ): Promise<GkmConfig> {
116
+ const config = await loadRawConfig(cwd);
117
+
118
+ // If it's a workspace config, throw an error
119
+ if (isWorkspaceConfig(config)) {
89
120
  throw new Error(
90
- `Failed to load gkm.config.json: ${(error as Error).message}`,
121
+ 'Workspace configuration detected. Use loadWorkspaceConfig() instead.',
91
122
  );
92
123
  }
124
+
125
+ return config;
126
+ }
127
+
128
+ /**
129
+ * Load configuration file and process it as a workspace.
130
+ * Works with both single-app and workspace configurations.
131
+ *
132
+ * Single-app configs are automatically wrapped as a workspace with one app.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const { type, workspace } = await loadWorkspaceConfig();
137
+ *
138
+ * if (type === 'workspace') {
139
+ * console.log('Multi-app workspace:', workspace.apps);
140
+ * } else {
141
+ * console.log('Single app wrapped as workspace');
142
+ * }
143
+ * ```
144
+ */
145
+ export async function loadWorkspaceConfig(
146
+ cwd: string = process.cwd(),
147
+ ): Promise<LoadedConfig> {
148
+ const config = await loadRawConfig(cwd);
149
+ return processConfig(config, cwd);
93
150
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import type { GkmConfig } from '../../types';
3
3
  import {
4
4
  getAppNameFromCwd,