@geekmidas/cli 0.25.0 → 0.27.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 (36) hide show
  1. package/dist/{config-CTftATBX.cjs → config-BhryDQEq.cjs} +2 -2
  2. package/dist/{config-CTftATBX.cjs.map → config-BhryDQEq.cjs.map} +1 -1
  3. package/dist/{config-BogU0_oQ.mjs → config-C9bdq0l-.mjs} +2 -2
  4. package/dist/{config-BogU0_oQ.mjs.map → config-C9bdq0l-.mjs.map} +1 -1
  5. package/dist/config.cjs +2 -2
  6. package/dist/config.mjs +2 -2
  7. package/dist/index-CWN-bgrO.d.mts.map +1 -1
  8. package/dist/index-DEWYvYvg.d.cts.map +1 -1
  9. package/dist/index.cjs +128 -27
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.mjs +128 -27
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/{openapi-DNbXfhXE.mjs → openapi-BCEFhkLh.mjs} +2 -2
  14. package/dist/{openapi-DNbXfhXE.mjs.map → openapi-BCEFhkLh.mjs.map} +1 -1
  15. package/dist/{openapi-BrhkPKM7.cjs → openapi-D82bBqG7.cjs} +2 -2
  16. package/dist/{openapi-BrhkPKM7.cjs.map → openapi-D82bBqG7.cjs.map} +1 -1
  17. package/dist/openapi.cjs +3 -3
  18. package/dist/openapi.mjs +3 -3
  19. package/dist/workspace/index.cjs +1 -1
  20. package/dist/workspace/index.mjs +1 -1
  21. package/dist/{workspace-iWgBlX6h.cjs → workspace-CiZBOjf9.cjs} +1 -7
  22. package/dist/{workspace-iWgBlX6h.cjs.map → workspace-CiZBOjf9.cjs.map} +1 -1
  23. package/dist/{workspace-CPLEZDZf.mjs → workspace-DQjmv9lk.mjs} +1 -7
  24. package/dist/{workspace-CPLEZDZf.mjs.map → workspace-DQjmv9lk.mjs.map} +1 -1
  25. package/package.json +4 -4
  26. package/src/init/__tests__/generators.spec.ts +12 -6
  27. package/src/init/__tests__/init.spec.ts +18 -6
  28. package/src/init/generators/models.ts +49 -19
  29. package/src/init/templates/api.ts +57 -3
  30. package/src/init/templates/minimal.ts +8 -0
  31. package/src/init/templates/serverless.ts +9 -0
  32. package/src/init/templates/worker.ts +9 -1
  33. package/src/init/versions.ts +1 -1
  34. package/src/workspace/__tests__/index.spec.ts +5 -6
  35. package/src/workspace/__tests__/schema.spec.ts +5 -5
  36. package/src/workspace/schema.ts +1 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -49,10 +49,10 @@
49
49
  "openapi-typescript": "^7.4.2",
50
50
  "prompts": "~2.4.2",
51
51
  "@geekmidas/constructs": "~0.6.0",
52
- "@geekmidas/envkit": "~0.4.0",
53
- "@geekmidas/logger": "~0.4.0",
54
52
  "@geekmidas/schema": "~0.1.0",
55
- "@geekmidas/errors": "~0.1.0"
53
+ "@geekmidas/errors": "~0.1.0",
54
+ "@geekmidas/envkit": "~0.4.0",
55
+ "@geekmidas/logger": "~0.4.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/lodash.kebabcase": "^4.1.9",
@@ -297,7 +297,8 @@ describe('generateModelsPackage', () => {
297
297
  const paths = files.map((f) => f.path);
298
298
  expect(paths).toContain('packages/models/package.json');
299
299
  expect(paths).toContain('packages/models/tsconfig.json');
300
- expect(paths).toContain('packages/models/src/index.ts');
300
+ expect(paths).toContain('packages/models/src/common.ts');
301
+ expect(paths).toContain('packages/models/src/user.ts');
301
302
  });
302
303
 
303
304
  it('should use correct package name', () => {
@@ -335,12 +336,17 @@ describe('generateModelsPackage', () => {
335
336
  apiPath: 'apps/api',
336
337
  };
337
338
  const files = generateModelsPackage(options);
338
- const indexTs = files.find(
339
- (f) => f.path === 'packages/models/src/index.ts',
339
+ const userTs = files.find(
340
+ (f) => f.path === 'packages/models/src/user.ts',
340
341
  );
341
- expect(indexTs?.content).toContain('userSchema');
342
- expect(indexTs?.content).toContain('paginationSchema');
343
- expect(indexTs?.content).toContain("import { z } from 'zod'");
342
+ const commonTs = files.find(
343
+ (f) => f.path === 'packages/models/src/common.ts',
344
+ );
345
+ expect(userTs?.content).toContain('UserSchema');
346
+ expect(userTs?.content).toContain('UserResponseSchema');
347
+ expect(userTs?.content).toContain("import { z } from 'zod'");
348
+ expect(commonTs?.content).toContain('PaginationSchema');
349
+ expect(commonTs?.content).toContain('IdSchema');
344
350
  });
345
351
 
346
352
  it('should extend root tsconfig', () => {
@@ -176,7 +176,10 @@ describe('initCommand', () => {
176
176
  expect(
177
177
  existsSync(join(projectDir, 'packages/models/tsconfig.json')),
178
178
  ).toBe(true);
179
- expect(existsSync(join(projectDir, 'packages/models/src/index.ts'))).toBe(
179
+ expect(existsSync(join(projectDir, 'packages/models/src/common.ts'))).toBe(
180
+ true,
181
+ );
182
+ expect(existsSync(join(projectDir, 'packages/models/src/user.ts'))).toBe(
180
183
  true,
181
184
  );
182
185
  });
@@ -265,14 +268,23 @@ describe('initCommand', () => {
265
268
  expect(pkg.name).toBe('@my-monorepo/models');
266
269
  expect(pkg.dependencies.zod).toBeDefined();
267
270
 
268
- const indexPath = join(
271
+ const userPath = join(
272
+ tempDir,
273
+ 'my-monorepo',
274
+ 'packages/models/src/user.ts',
275
+ );
276
+ const userContent = await readFile(userPath, 'utf-8');
277
+ expect(userContent).toContain('UserSchema');
278
+ expect(userContent).toContain('UserResponseSchema');
279
+
280
+ const commonPath = join(
269
281
  tempDir,
270
282
  'my-monorepo',
271
- 'packages/models/src/index.ts',
283
+ 'packages/models/src/common.ts',
272
284
  );
273
- const indexContent = await readFile(indexPath, 'utf-8');
274
- expect(indexContent).toContain('userSchema');
275
- expect(indexContent).toContain('paginationSchema');
285
+ const commonContent = await readFile(commonPath, 'utf-8');
286
+ expect(commonContent).toContain('PaginationSchema');
287
+ expect(commonContent).toContain('IdSchema');
276
288
  });
277
289
 
278
290
  it('should support custom API path', async () => {
@@ -55,26 +55,26 @@ export function generateModelsPackage(
55
55
  exclude: ['node_modules', 'dist'],
56
56
  };
57
57
 
58
- // Main index.ts with example schemas
59
- const indexTs = `import { z } from 'zod';
58
+ // common.ts - shared utility schemas
59
+ const commonTs = `import { z } from 'zod';
60
60
 
61
61
  // ============================================
62
62
  // Common Schemas
63
63
  // ============================================
64
64
 
65
- export const idSchema = z.string().uuid();
65
+ export const IdSchema = z.string().uuid();
66
66
 
67
- export const timestampsSchema = z.object({
67
+ export const TimestampsSchema = z.object({
68
68
  createdAt: z.coerce.date(),
69
69
  updatedAt: z.coerce.date(),
70
70
  });
71
71
 
72
- export const paginationSchema = z.object({
72
+ export const PaginationSchema = z.object({
73
73
  page: z.coerce.number().int().positive().default(1),
74
74
  limit: z.coerce.number().int().positive().max(100).default(20),
75
75
  });
76
76
 
77
- export const paginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
77
+ export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
78
78
  z.object({
79
79
  items: z.array(itemSchema),
80
80
  total: z.number(),
@@ -83,35 +83,61 @@ export const paginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =
83
83
  totalPages: z.number(),
84
84
  });
85
85
 
86
+ // ============================================
87
+ // Type Exports
88
+ // ============================================
89
+
90
+ export type Id = z.infer<typeof IdSchema>;
91
+ export type Timestamps = z.infer<typeof TimestampsSchema>;
92
+ export type Pagination = z.infer<typeof PaginationSchema>;
93
+ `;
94
+
95
+ // user.ts - user-related schemas
96
+ const userTs = `import { z } from 'zod';
97
+ import { IdSchema, TimestampsSchema } from './common.js';
98
+
86
99
  // ============================================
87
100
  // User Schemas
88
101
  // ============================================
89
102
 
90
- export const userSchema = z.object({
91
- id: idSchema,
103
+ export const UserSchema = z.object({
104
+ id: IdSchema,
92
105
  email: z.string().email(),
93
106
  name: z.string().min(1).max(100),
94
- ...timestampsSchema.shape,
107
+ ...TimestampsSchema.shape,
95
108
  });
96
109
 
97
- export const createUserSchema = userSchema.omit({
110
+ export const CreateUserSchema = UserSchema.omit({
98
111
  id: true,
99
112
  createdAt: true,
100
113
  updatedAt: true,
101
114
  });
102
115
 
103
- export const updateUserSchema = createUserSchema.partial();
116
+ export const UpdateUserSchema = CreateUserSchema.partial();
117
+
118
+ // ============================================
119
+ // Response Schemas
120
+ // ============================================
121
+
122
+ export const UserResponseSchema = UserSchema.pick({
123
+ id: true,
124
+ name: true,
125
+ email: true,
126
+ });
127
+
128
+ export const ListUsersResponseSchema = z.object({
129
+ users: z.array(UserSchema.pick({ id: true, name: true })),
130
+ });
104
131
 
105
132
  // ============================================
106
133
  // Type Exports
107
134
  // ============================================
108
135
 
109
- export type Id = z.infer<typeof idSchema>;
110
- export type Timestamps = z.infer<typeof timestampsSchema>;
111
- export type Pagination = z.infer<typeof paginationSchema>;
112
- export type User = z.infer<typeof userSchema>;
113
- export type CreateUser = z.infer<typeof createUserSchema>;
114
- export type UpdateUser = z.infer<typeof updateUserSchema>;
136
+ export type User = z.infer<typeof UserSchema>;
137
+ export type CreateUser = z.infer<typeof CreateUserSchema>;
138
+ export type UpdateUser = z.infer<typeof UpdateUserSchema>;
139
+ export type UserResponse = z.infer<typeof UserResponseSchema>;
140
+ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
115
141
  `;
116
142
 
117
143
  return [
@@ -124,8 +150,12 @@ export type UpdateUser = z.infer<typeof updateUserSchema>;
124
150
  content: `${JSON.stringify(tsConfig, null, 2)}\n`,
125
151
  },
126
152
  {
127
- path: 'packages/models/src/index.ts',
128
- content: indexTs,
153
+ path: 'packages/models/src/common.ts',
154
+ content: commonTs,
155
+ },
156
+ {
157
+ path: 'packages/models/src/user.ts',
158
+ content: userTs,
129
159
  },
130
160
  ];
131
161
  }
@@ -10,10 +10,13 @@ export const apiTemplate: TemplateConfig = {
10
10
  description: 'Full API with auth, database, services',
11
11
 
12
12
  dependencies: {
13
+ '@geekmidas/audit': GEEKMIDAS_VERSIONS['@geekmidas/audit'],
13
14
  '@geekmidas/constructs': GEEKMIDAS_VERSIONS['@geekmidas/constructs'],
14
15
  '@geekmidas/envkit': GEEKMIDAS_VERSIONS['@geekmidas/envkit'],
15
16
  '@geekmidas/events': GEEKMIDAS_VERSIONS['@geekmidas/events'],
16
17
  '@geekmidas/logger': GEEKMIDAS_VERSIONS['@geekmidas/logger'],
18
+ '@geekmidas/rate-limit': GEEKMIDAS_VERSIONS['@geekmidas/rate-limit'],
19
+ '@geekmidas/schema': GEEKMIDAS_VERSIONS['@geekmidas/schema'],
17
20
  '@geekmidas/services': GEEKMIDAS_VERSIONS['@geekmidas/services'],
18
21
  '@geekmidas/errors': GEEKMIDAS_VERSIONS['@geekmidas/errors'],
19
22
  '@geekmidas/auth': GEEKMIDAS_VERSIONS['@geekmidas/auth'],
@@ -44,13 +47,16 @@ export const apiTemplate: TemplateConfig = {
44
47
  },
45
48
 
46
49
  files: (options: TemplateOptions): GeneratedFile[] => {
47
- const { loggerType, routesStructure } = options;
50
+ const { loggerType, routesStructure, monorepo, name } = options;
48
51
 
49
52
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
50
53
 
51
54
  export const logger = createLogger();
52
55
  `;
53
56
 
57
+ // Models package import path for monorepo
58
+ const modelsImport = monorepo ? `@${name}/models` : null;
59
+
54
60
  // Get route path based on structure
55
61
  const getRoutePath = (file: string) => {
56
62
  switch (routesStructure) {
@@ -98,9 +104,14 @@ export const config = envParser
98
104
  {
99
105
  path: getRoutePath('health.ts'),
100
106
  content: `import { e } from '@geekmidas/constructs/endpoints';
107
+ import { z } from 'zod';
101
108
 
102
109
  export const healthEndpoint = e
103
110
  .get('/health')
111
+ .output(z.object({
112
+ status: z.string(),
113
+ timestamp: z.string(),
114
+ }))
104
115
  .handle(async () => ({
105
116
  status: 'ok',
106
117
  timestamp: new Date().toISOString(),
@@ -111,10 +122,33 @@ export const healthEndpoint = e
111
122
  // users endpoints
112
123
  {
113
124
  path: getRoutePath('users/list.ts'),
114
- content: `import { e } from '@geekmidas/constructs/endpoints';
125
+ content: modelsImport
126
+ ? `import { e } from '@geekmidas/constructs/endpoints';
127
+ import { ListUsersResponseSchema } from '${modelsImport}/user';
115
128
 
116
129
  export const listUsersEndpoint = e
117
130
  .get('/users')
131
+ .output(ListUsersResponseSchema)
132
+ .handle(async () => ({
133
+ users: [
134
+ { id: '1', name: 'Alice' },
135
+ { id: '2', name: 'Bob' },
136
+ ],
137
+ }));
138
+ `
139
+ : `import { e } from '@geekmidas/constructs/endpoints';
140
+ import { z } from 'zod';
141
+
142
+ const UserSchema = z.object({
143
+ id: z.string(),
144
+ name: z.string(),
145
+ });
146
+
147
+ export const listUsersEndpoint = e
148
+ .get('/users')
149
+ .output(z.object({
150
+ users: z.array(UserSchema),
151
+ }))
118
152
  .handle(async () => ({
119
153
  users: [
120
154
  { id: '1', name: 'Alice' },
@@ -125,12 +159,32 @@ export const listUsersEndpoint = e
125
159
  },
126
160
  {
127
161
  path: getRoutePath('users/get.ts'),
128
- content: `import { e } from '@geekmidas/constructs/endpoints';
162
+ content: modelsImport
163
+ ? `import { e } from '@geekmidas/constructs/endpoints';
164
+ import { z } from 'zod';
165
+ import { UserResponseSchema } from '${modelsImport}/user';
166
+
167
+ export const getUserEndpoint = e
168
+ .get('/users/:id')
169
+ .params(z.object({ id: z.string() }))
170
+ .output(UserResponseSchema)
171
+ .handle(async ({ params }) => ({
172
+ id: params.id,
173
+ name: 'Alice',
174
+ email: 'alice@example.com',
175
+ }));
176
+ `
177
+ : `import { e } from '@geekmidas/constructs/endpoints';
129
178
  import { z } from 'zod';
130
179
 
131
180
  export const getUserEndpoint = e
132
181
  .get('/users/:id')
133
182
  .params(z.object({ id: z.string() }))
183
+ .output(z.object({
184
+ id: z.string(),
185
+ name: z.string(),
186
+ email: z.string().email(),
187
+ }))
134
188
  .handle(async ({ params }) => ({
135
189
  id: params.id,
136
190
  name: 'Alice',
@@ -10,9 +10,12 @@ export const minimalTemplate: TemplateConfig = {
10
10
  description: 'Basic health endpoint',
11
11
 
12
12
  dependencies: {
13
+ '@geekmidas/audit': GEEKMIDAS_VERSIONS['@geekmidas/audit'],
13
14
  '@geekmidas/constructs': GEEKMIDAS_VERSIONS['@geekmidas/constructs'],
14
15
  '@geekmidas/envkit': GEEKMIDAS_VERSIONS['@geekmidas/envkit'],
15
16
  '@geekmidas/logger': GEEKMIDAS_VERSIONS['@geekmidas/logger'],
17
+ '@geekmidas/rate-limit': GEEKMIDAS_VERSIONS['@geekmidas/rate-limit'],
18
+ '@geekmidas/schema': GEEKMIDAS_VERSIONS['@geekmidas/schema'],
16
19
  '@hono/node-server': '~1.14.1',
17
20
  hono: '~4.8.2',
18
21
  pino: '~9.6.0',
@@ -89,9 +92,14 @@ export const config = envParser
89
92
  {
90
93
  path: getRoutePath('health.ts'),
91
94
  content: `import { e } from '@geekmidas/constructs/endpoints';
95
+ import { z } from 'zod';
92
96
 
93
97
  export const healthEndpoint = e
94
98
  .get('/health')
99
+ .output(z.object({
100
+ status: z.string(),
101
+ timestamp: z.string(),
102
+ }))
95
103
  .handle(async () => ({
96
104
  status: 'ok',
97
105
  timestamp: new Date().toISOString(),
@@ -10,10 +10,13 @@ export const serverlessTemplate: TemplateConfig = {
10
10
  description: 'AWS Lambda handlers',
11
11
 
12
12
  dependencies: {
13
+ '@geekmidas/audit': GEEKMIDAS_VERSIONS['@geekmidas/audit'],
13
14
  '@geekmidas/constructs': GEEKMIDAS_VERSIONS['@geekmidas/constructs'],
14
15
  '@geekmidas/envkit': GEEKMIDAS_VERSIONS['@geekmidas/envkit'],
15
16
  '@geekmidas/logger': GEEKMIDAS_VERSIONS['@geekmidas/logger'],
16
17
  '@geekmidas/cloud': GEEKMIDAS_VERSIONS['@geekmidas/cloud'],
18
+ '@geekmidas/rate-limit': GEEKMIDAS_VERSIONS['@geekmidas/rate-limit'],
19
+ '@geekmidas/schema': GEEKMIDAS_VERSIONS['@geekmidas/schema'],
17
20
  '@hono/node-server': '~1.14.1',
18
21
  hono: '~4.8.2',
19
22
  pino: '~9.6.0',
@@ -91,9 +94,15 @@ export const config = envParser
91
94
  {
92
95
  path: getRoutePath('health.ts'),
93
96
  content: `import { e } from '@geekmidas/constructs/endpoints';
97
+ import { z } from 'zod';
94
98
 
95
99
  export const healthEndpoint = e
96
100
  .get('/health')
101
+ .output(z.object({
102
+ status: z.string(),
103
+ timestamp: z.string(),
104
+ region: z.string(),
105
+ }))
97
106
  .handle(async () => ({
98
107
  status: 'ok',
99
108
  timestamp: new Date().toISOString(),
@@ -10,10 +10,13 @@ export const workerTemplate: TemplateConfig = {
10
10
  description: 'Background job processing',
11
11
 
12
12
  dependencies: {
13
+ '@geekmidas/audit': GEEKMIDAS_VERSIONS['@geekmidas/audit'],
13
14
  '@geekmidas/constructs': GEEKMIDAS_VERSIONS['@geekmidas/constructs'],
14
15
  '@geekmidas/envkit': GEEKMIDAS_VERSIONS['@geekmidas/envkit'],
15
- '@geekmidas/logger': GEEKMIDAS_VERSIONS['@geekmidas/logger'],
16
16
  '@geekmidas/events': GEEKMIDAS_VERSIONS['@geekmidas/events'],
17
+ '@geekmidas/logger': GEEKMIDAS_VERSIONS['@geekmidas/logger'],
18
+ '@geekmidas/rate-limit': GEEKMIDAS_VERSIONS['@geekmidas/rate-limit'],
19
+ '@geekmidas/schema': GEEKMIDAS_VERSIONS['@geekmidas/schema'],
17
20
  '@hono/node-server': '~1.14.1',
18
21
  hono: '~4.8.2',
19
22
  pino: '~9.6.0',
@@ -90,9 +93,14 @@ export const config = envParser
90
93
  {
91
94
  path: getRoutePath('health.ts'),
92
95
  content: `import { e } from '@geekmidas/constructs/endpoints';
96
+ import { z } from 'zod';
93
97
 
94
98
  export const healthEndpoint = e
95
99
  .get('/health')
100
+ .output(z.object({
101
+ status: z.string(),
102
+ timestamp: z.string(),
103
+ }))
96
104
  .handle(async () => ({
97
105
  status: 'ok',
98
106
  timestamp: new Date().toISOString(),
@@ -44,7 +44,7 @@ export const GEEKMIDAS_VERSIONS = {
44
44
  '@geekmidas/services': '~0.2.0',
45
45
  '@geekmidas/storage': '~0.1.0',
46
46
  '@geekmidas/studio': '~0.4.0',
47
- '@geekmidas/telescope': '~0.4.0',
47
+ '@geekmidas/telescope': '~0.5.0',
48
48
  '@geekmidas/testkit': '~0.6.0',
49
49
  };
50
50
 
@@ -41,20 +41,19 @@ describe('defineWorkspace', () => {
41
41
  );
42
42
  });
43
43
 
44
- it('should throw with descriptive error for missing routes', () => {
44
+ it('should allow backend apps without routes (e.g., auth servers)', () => {
45
45
  const config = {
46
46
  apps: {
47
- api: {
47
+ auth: {
48
48
  type: 'backend',
49
- path: 'apps/api',
49
+ path: 'apps/auth',
50
50
  port: 3000,
51
51
  },
52
52
  },
53
53
  } as WorkspaceConfig;
54
54
 
55
- expect(() => defineWorkspace(config)).toThrow(
56
- 'Backend apps must have routes defined',
57
- );
55
+ // Should not throw - routes are optional for backend apps
56
+ expect(() => defineWorkspace(config)).not.toThrow();
58
57
  });
59
58
  });
60
59
 
@@ -142,21 +142,21 @@ describe('WorkspaceConfigSchema', () => {
142
142
  expect(result.error).toBeDefined();
143
143
  });
144
144
 
145
- it('should reject backend app without routes', () => {
145
+ it('should allow backend app without routes (e.g., auth servers)', () => {
146
146
  const config = {
147
147
  apps: {
148
- api: {
148
+ auth: {
149
149
  type: 'backend' as const,
150
- path: 'apps/api',
150
+ path: 'apps/auth',
151
151
  port: 3000,
152
- // Missing routes
152
+ // Routes are optional for backend apps
153
153
  },
154
154
  },
155
155
  };
156
156
 
157
157
  const result = safeValidateWorkspaceConfig(config);
158
158
 
159
- expect(result.success).toBe(false);
159
+ expect(result.success).toBe(true);
160
160
  });
161
161
 
162
162
  it('should reject frontend app without framework', () => {
@@ -203,19 +203,7 @@ const AppConfigSchema = z
203
203
  framework: z.enum(['nextjs']).optional(),
204
204
  client: ClientConfigSchema.optional(),
205
205
  })
206
- .refine(
207
- (data) => {
208
- // Backend apps must have routes
209
- if (data.type === 'backend' && !data.routes) {
210
- return false;
211
- }
212
- return true;
213
- },
214
- {
215
- message: 'Backend apps must have routes defined',
216
- path: ['routes'],
217
- },
218
- )
206
+ // Note: routes is optional for backend apps - some backends like auth servers don't use routes
219
207
  .refine(
220
208
  (data) => {
221
209
  // Frontend apps must have framework