@geekmidas/cli 0.17.0 → 0.19.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 +2644 -564
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +2639 -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 +223 -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 -0
  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
@@ -0,0 +1,251 @@
1
+ import { describe, expect, expectTypeOf, it } from 'vitest';
2
+ import { defineWorkspace } from '../index.js';
3
+ import type { InferAppNames, InferredWorkspaceConfig } from '../types.js';
4
+
5
+ describe('Type Inference', () => {
6
+ describe('defineWorkspace type inference', () => {
7
+ it('should infer app names as literal types', () => {
8
+ const config = defineWorkspace({
9
+ apps: {
10
+ api: {
11
+ type: 'backend',
12
+ path: 'apps/api',
13
+ port: 3000,
14
+ routes: './src/**/*.ts',
15
+ },
16
+ web: {
17
+ type: 'frontend',
18
+ path: 'apps/web',
19
+ port: 3001,
20
+ framework: 'nextjs',
21
+ },
22
+ },
23
+ });
24
+
25
+ // Type-level test: apps should have 'api' and 'web' as keys
26
+ expectTypeOf(config.apps).toHaveProperty('api');
27
+ expectTypeOf(config.apps).toHaveProperty('web');
28
+
29
+ // Runtime test
30
+ expect(Object.keys(config.apps)).toEqual(['api', 'web']);
31
+ });
32
+
33
+ it('should allow valid dependencies', () => {
34
+ const config = defineWorkspace({
35
+ apps: {
36
+ api: {
37
+ type: 'backend',
38
+ path: 'apps/api',
39
+ port: 3000,
40
+ routes: './src/**/*.ts',
41
+ },
42
+ auth: {
43
+ type: 'backend',
44
+ path: 'apps/auth',
45
+ port: 3001,
46
+ routes: './src/**/*.ts',
47
+ },
48
+ web: {
49
+ type: 'frontend',
50
+ path: 'apps/web',
51
+ port: 3002,
52
+ framework: 'nextjs',
53
+ dependencies: ['api', 'auth'],
54
+ },
55
+ },
56
+ });
57
+
58
+ expect(config.apps.web.dependencies).toEqual(['api', 'auth']);
59
+ });
60
+
61
+ it('should throw error for invalid dependency at runtime', () => {
62
+ expect(() =>
63
+ defineWorkspace({
64
+ apps: {
65
+ api: {
66
+ type: 'backend',
67
+ path: 'apps/api',
68
+ port: 3000,
69
+ routes: './src/**/*.ts',
70
+ },
71
+ web: {
72
+ type: 'frontend',
73
+ path: 'apps/web',
74
+ port: 3001,
75
+ framework: 'nextjs',
76
+ dependencies: ['nonexistent'],
77
+ },
78
+ },
79
+ }),
80
+ ).toThrow(/Invalid dependency.*"nonexistent"/);
81
+ });
82
+
83
+ it('should throw error for self-dependency at runtime', () => {
84
+ expect(() =>
85
+ defineWorkspace({
86
+ apps: {
87
+ api: {
88
+ type: 'backend',
89
+ path: 'apps/api',
90
+ port: 3000,
91
+ routes: './src/**/*.ts',
92
+ dependencies: ['api'],
93
+ },
94
+ },
95
+ }),
96
+ ).toThrow(/cannot depend on itself/);
97
+ });
98
+
99
+ it('should preserve all config properties with inference', () => {
100
+ const config = defineWorkspace({
101
+ name: 'my-workspace',
102
+ apps: {
103
+ api: {
104
+ type: 'backend',
105
+ path: 'apps/api',
106
+ port: 3000,
107
+ routes: './src/**/*.ts',
108
+ envParser: './src/env',
109
+ logger: './src/logger',
110
+ telescope: { enabled: true, port: 9000 },
111
+ },
112
+ },
113
+ services: {
114
+ db: true,
115
+ cache: { version: '7.2' },
116
+ },
117
+ deploy: {
118
+ default: 'dokploy',
119
+ },
120
+ });
121
+
122
+ expect(config.name).toBe('my-workspace');
123
+ expect(config.apps.api.envParser).toBe('./src/env');
124
+ expect(config.apps.api.telescope).toEqual({ enabled: true, port: 9000 });
125
+ expect(config.services?.db).toBe(true);
126
+ expect(config.deploy?.default).toBe('dokploy');
127
+ });
128
+ });
129
+
130
+ describe('InferAppNames type utility', () => {
131
+ it('should extract app names as union type', () => {
132
+ type TestApps = {
133
+ api: { path: string; port: number };
134
+ web: { path: string; port: number };
135
+ worker: { path: string; port: number };
136
+ };
137
+
138
+ type AppNames = InferAppNames<TestApps>;
139
+
140
+ // Type-level assertion: AppNames should be 'api' | 'web' | 'worker'
141
+ expectTypeOf<AppNames>().toEqualTypeOf<'api' | 'web' | 'worker'>();
142
+ });
143
+ });
144
+
145
+ describe('InferredWorkspaceConfig type utility', () => {
146
+ it('should create correct inferred config type', () => {
147
+ type TestApps = {
148
+ api: { path: string; port: number; routes: string };
149
+ web: { path: string; port: number; framework: 'nextjs' };
150
+ };
151
+
152
+ type Inferred = InferredWorkspaceConfig<TestApps>;
153
+
154
+ // The inferred type should have the same structure
155
+ expectTypeOf<Inferred['apps']['api']>().toHaveProperty('path');
156
+ expectTypeOf<Inferred['apps']['web']>().toHaveProperty('framework');
157
+
158
+ // Dependencies should be typed as 'api' | 'web'
159
+ type WebDeps = NonNullable<Inferred['apps']['web']['dependencies']>;
160
+ expectTypeOf<WebDeps>().toEqualTypeOf<('api' | 'web')[]>();
161
+ });
162
+ });
163
+
164
+ describe('Complex workspace scenarios', () => {
165
+ it('should handle workspace with multiple backend and frontend apps', () => {
166
+ const config = defineWorkspace({
167
+ name: 'complex-saas',
168
+ apps: {
169
+ 'api-gateway': {
170
+ type: 'backend',
171
+ path: 'apps/api-gateway',
172
+ port: 3000,
173
+ routes: './src/**/*.ts',
174
+ },
175
+ 'user-service': {
176
+ type: 'backend',
177
+ path: 'apps/user-service',
178
+ port: 3001,
179
+ routes: './src/**/*.ts',
180
+ },
181
+ 'payment-service': {
182
+ type: 'backend',
183
+ path: 'apps/payment-service',
184
+ port: 3002,
185
+ routes: './src/**/*.ts',
186
+ dependencies: ['user-service'],
187
+ },
188
+ 'web-app': {
189
+ type: 'frontend',
190
+ path: 'apps/web',
191
+ port: 3003,
192
+ framework: 'nextjs',
193
+ dependencies: ['api-gateway'],
194
+ },
195
+ 'admin-dashboard': {
196
+ type: 'frontend',
197
+ path: 'apps/admin',
198
+ port: 3004,
199
+ framework: 'nextjs',
200
+ dependencies: ['api-gateway', 'user-service'],
201
+ },
202
+ },
203
+ services: {
204
+ db: { version: '16-alpine' },
205
+ cache: true,
206
+ mail: { smtp: { host: 'smtp.example.com', port: 587 } },
207
+ },
208
+ });
209
+
210
+ expect(Object.keys(config.apps)).toHaveLength(5);
211
+ expect(config.apps['payment-service'].dependencies).toEqual([
212
+ 'user-service',
213
+ ]);
214
+ expect(config.apps['admin-dashboard'].dependencies).toEqual([
215
+ 'api-gateway',
216
+ 'user-service',
217
+ ]);
218
+ });
219
+
220
+ it('should handle empty dependencies array', () => {
221
+ const config = defineWorkspace({
222
+ apps: {
223
+ api: {
224
+ type: 'backend',
225
+ path: 'apps/api',
226
+ port: 3000,
227
+ routes: './src/**/*.ts',
228
+ dependencies: [],
229
+ },
230
+ },
231
+ });
232
+
233
+ expect(config.apps.api.dependencies).toEqual([]);
234
+ });
235
+
236
+ it('should handle undefined dependencies', () => {
237
+ const config = defineWorkspace({
238
+ apps: {
239
+ api: {
240
+ type: 'backend',
241
+ path: 'apps/api',
242
+ port: 3000,
243
+ routes: './src/**/*.ts',
244
+ },
245
+ },
246
+ });
247
+
248
+ expect(config.apps.api.dependencies).toBeUndefined();
249
+ });
250
+ });
251
+ });
@@ -0,0 +1,307 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative } from 'node:path';
4
+ import { EndpointGenerator } from '../generators/EndpointGenerator.js';
5
+ import { OpenApiTsGenerator } from '../generators/OpenApiTsGenerator.js';
6
+ import type { NormalizedWorkspace } from './types.js';
7
+
8
+ const logger = console;
9
+
10
+ /**
11
+ * Result of generating a client for a frontend app.
12
+ */
13
+ export interface ClientGenerationResult {
14
+ frontendApp: string;
15
+ backendApp: string;
16
+ outputPath: string;
17
+ endpointCount: number;
18
+ generated: boolean;
19
+ reason?: string;
20
+ }
21
+
22
+ /**
23
+ * Cache of OpenAPI spec hashes to detect changes.
24
+ */
25
+ const specHashCache = new Map<string, string>();
26
+
27
+ /**
28
+ * Calculate hash of content for change detection.
29
+ */
30
+ function hashContent(content: string): string {
31
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
32
+ }
33
+
34
+ /**
35
+ * Normalize routes to an array of patterns.
36
+ * @internal Exported for use in dev command
37
+ */
38
+ export function normalizeRoutes(
39
+ routes: string | string[] | undefined,
40
+ ): string[] {
41
+ if (!routes) return [];
42
+ return Array.isArray(routes) ? routes : [routes];
43
+ }
44
+
45
+ /**
46
+ * Get the first routes pattern as a string (for simple cases).
47
+ * @internal Exported for use in dev command
48
+ */
49
+ export function getFirstRoute(
50
+ routes: string | string[] | undefined,
51
+ ): string | null {
52
+ const normalized = normalizeRoutes(routes);
53
+ return normalized[0] || null;
54
+ }
55
+
56
+ /**
57
+ * Generate OpenAPI spec for a backend app.
58
+ * Returns the spec content and endpoint count.
59
+ */
60
+ export async function generateBackendOpenApi(
61
+ workspace: NormalizedWorkspace,
62
+ appName: string,
63
+ ): Promise<{ content: string; endpointCount: number } | null> {
64
+ const app = workspace.apps[appName];
65
+ if (!app || app.type !== 'backend' || !app.routes) {
66
+ return null;
67
+ }
68
+
69
+ const appPath = join(workspace.root, app.path);
70
+ const routesPatterns = normalizeRoutes(app.routes);
71
+
72
+ if (routesPatterns.length === 0) {
73
+ return null;
74
+ }
75
+
76
+ // Load endpoints from all routes patterns
77
+ const endpointGenerator = new EndpointGenerator();
78
+ const allLoadedEndpoints = [];
79
+
80
+ for (const pattern of routesPatterns) {
81
+ const fullPattern = join(appPath, pattern);
82
+ const loaded = await endpointGenerator.load(fullPattern);
83
+ allLoadedEndpoints.push(...loaded);
84
+ }
85
+
86
+ const loadedEndpoints = allLoadedEndpoints;
87
+
88
+ if (loadedEndpoints.length === 0) {
89
+ return null;
90
+ }
91
+
92
+ const endpoints = loadedEndpoints.map(({ construct }) => construct);
93
+
94
+ const tsGenerator = new OpenApiTsGenerator();
95
+ const content = await tsGenerator.generate(endpoints, {
96
+ title: `${appName} API`,
97
+ version: '1.0.0',
98
+ description: `Auto-generated API client for ${appName}`,
99
+ });
100
+
101
+ return { content, endpointCount: loadedEndpoints.length };
102
+ }
103
+
104
+ /**
105
+ * Generate client for a frontend app from its backend dependencies.
106
+ * Only regenerates if the OpenAPI spec has changed.
107
+ */
108
+ export async function generateClientForFrontend(
109
+ workspace: NormalizedWorkspace,
110
+ frontendAppName: string,
111
+ options: { force?: boolean } = {},
112
+ ): Promise<ClientGenerationResult[]> {
113
+ const results: ClientGenerationResult[] = [];
114
+ const frontendApp = workspace.apps[frontendAppName];
115
+
116
+ if (!frontendApp || frontendApp.type !== 'frontend') {
117
+ return results;
118
+ }
119
+
120
+ const dependencies = frontendApp.dependencies || [];
121
+ const backendDeps = dependencies.filter((dep) => {
122
+ const depApp = workspace.apps[dep];
123
+ return depApp?.type === 'backend' && depApp.routes;
124
+ });
125
+
126
+ if (backendDeps.length === 0) {
127
+ return results;
128
+ }
129
+
130
+ // Determine output directory
131
+ const clientOutput = frontendApp.client?.output || 'src/api';
132
+ const frontendPath = join(workspace.root, frontendApp.path);
133
+ const outputDir = join(frontendPath, clientOutput);
134
+
135
+ for (const backendAppName of backendDeps) {
136
+ const result: ClientGenerationResult = {
137
+ frontendApp: frontendAppName,
138
+ backendApp: backendAppName,
139
+ outputPath: '',
140
+ endpointCount: 0,
141
+ generated: false,
142
+ };
143
+
144
+ try {
145
+ // Generate OpenAPI spec for backend
146
+ const spec = await generateBackendOpenApi(workspace, backendAppName);
147
+
148
+ if (!spec) {
149
+ result.reason = 'No endpoints found in backend';
150
+ results.push(result);
151
+ continue;
152
+ }
153
+
154
+ result.endpointCount = spec.endpointCount;
155
+
156
+ // Check if spec has changed (unless force)
157
+ const cacheKey = `${backendAppName}:${frontendAppName}`;
158
+ const newHash = hashContent(spec.content);
159
+ const oldHash = specHashCache.get(cacheKey);
160
+
161
+ if (!options.force && oldHash === newHash) {
162
+ result.reason = 'No schema changes detected';
163
+ results.push(result);
164
+ continue;
165
+ }
166
+
167
+ // Generate client file
168
+ await mkdir(outputDir, { recursive: true });
169
+
170
+ // For single dependency, use openapi.ts; for multiple, use {backend}-api.ts
171
+ const fileName =
172
+ backendDeps.length === 1 ? 'openapi.ts' : `${backendAppName}-api.ts`;
173
+ const outputPath = join(outputDir, fileName);
174
+
175
+ // Add header comment with backend reference
176
+ const backendRelPath = relative(
177
+ dirname(outputPath),
178
+ join(workspace.root, workspace.apps[backendAppName]!.path),
179
+ );
180
+
181
+ const clientContent = `/**
182
+ * Auto-generated API client for ${backendAppName}
183
+ * Generated from: ${backendRelPath}
184
+ *
185
+ * DO NOT EDIT - This file is automatically regenerated when backend schemas change.
186
+ */
187
+
188
+ ${spec.content}
189
+ `;
190
+
191
+ await writeFile(outputPath, clientContent);
192
+
193
+ // Update cache
194
+ specHashCache.set(cacheKey, newHash);
195
+
196
+ result.outputPath = outputPath;
197
+ result.generated = true;
198
+ results.push(result);
199
+ } catch (error) {
200
+ result.reason = `Error: ${(error as Error).message}`;
201
+ results.push(result);
202
+ }
203
+ }
204
+
205
+ return results;
206
+ }
207
+
208
+ /**
209
+ * Generate clients for all frontend apps in the workspace.
210
+ */
211
+ export async function generateAllClients(
212
+ workspace: NormalizedWorkspace,
213
+ options: { force?: boolean; silent?: boolean } = {},
214
+ ): Promise<ClientGenerationResult[]> {
215
+ const log = options.silent ? () => {} : logger.log.bind(logger);
216
+ const allResults: ClientGenerationResult[] = [];
217
+
218
+ for (const [appName, app] of Object.entries(workspace.apps)) {
219
+ if (app.type === 'frontend' && app.dependencies.length > 0) {
220
+ const results = await generateClientForFrontend(workspace, appName, {
221
+ force: options.force,
222
+ });
223
+
224
+ for (const result of results) {
225
+ if (result.generated) {
226
+ log(
227
+ `📦 Generated client for ${result.frontendApp} from ${result.backendApp} (${result.endpointCount} endpoints)`,
228
+ );
229
+ }
230
+ allResults.push(result);
231
+ }
232
+ }
233
+ }
234
+
235
+ return allResults;
236
+ }
237
+
238
+ /**
239
+ * Check if a file path matches endpoint patterns that could affect OpenAPI schema.
240
+ * Returns true for changes that should trigger client regeneration.
241
+ */
242
+ export function shouldRegenerateClient(
243
+ filePath: string,
244
+ routesPattern: string,
245
+ ): boolean {
246
+ // Normalize path separators
247
+ const normalizedPath = filePath.replace(/\\/g, '/');
248
+ const normalizedPattern = routesPattern.replace(/\\/g, '/');
249
+
250
+ // Check if the file matches the routes pattern
251
+ // This is a simple check - the file should be within the routes directory
252
+ const patternDir = normalizedPattern.split('*')[0] || '';
253
+
254
+ if (!normalizedPath.includes(patternDir.replace('./', ''))) {
255
+ return false;
256
+ }
257
+
258
+ // Check file extension - only TypeScript endpoint files
259
+ if (!normalizedPath.endsWith('.ts') && !normalizedPath.endsWith('.tsx')) {
260
+ return false;
261
+ }
262
+
263
+ return true;
264
+ }
265
+
266
+ /**
267
+ * Get backend apps that a frontend depends on.
268
+ */
269
+ export function getBackendDependencies(
270
+ workspace: NormalizedWorkspace,
271
+ frontendAppName: string,
272
+ ): string[] {
273
+ const frontendApp = workspace.apps[frontendAppName];
274
+ if (!frontendApp || frontendApp.type !== 'frontend') {
275
+ return [];
276
+ }
277
+
278
+ return frontendApp.dependencies.filter((dep) => {
279
+ const depApp = workspace.apps[dep];
280
+ return depApp?.type === 'backend' && depApp.routes;
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Get frontend apps that depend on a backend app.
286
+ */
287
+ export function getDependentFrontends(
288
+ workspace: NormalizedWorkspace,
289
+ backendAppName: string,
290
+ ): string[] {
291
+ const dependentApps: string[] = [];
292
+
293
+ for (const [appName, app] of Object.entries(workspace.apps)) {
294
+ if (app.type === 'frontend' && app.dependencies.includes(backendAppName)) {
295
+ dependentApps.push(appName);
296
+ }
297
+ }
298
+
299
+ return dependentApps;
300
+ }
301
+
302
+ /**
303
+ * Clear the spec hash cache (useful for testing).
304
+ */
305
+ export function clearSpecHashCache(): void {
306
+ specHashCache.clear();
307
+ }