@opensaas/stack-cli 0.1.7 → 0.4.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 (104) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +348 -0
  3. package/CLAUDE.md +60 -12
  4. package/dist/commands/generate.d.ts.map +1 -1
  5. package/dist/commands/generate.js +13 -13
  6. package/dist/commands/generate.js.map +1 -1
  7. package/dist/commands/mcp.d.ts +6 -0
  8. package/dist/commands/mcp.d.ts.map +1 -0
  9. package/dist/commands/mcp.js +116 -0
  10. package/dist/commands/mcp.js.map +1 -0
  11. package/dist/generator/context.d.ts.map +1 -1
  12. package/dist/generator/context.js +40 -7
  13. package/dist/generator/context.js.map +1 -1
  14. package/dist/generator/index.d.ts +4 -1
  15. package/dist/generator/index.d.ts.map +1 -1
  16. package/dist/generator/index.js +4 -1
  17. package/dist/generator/index.js.map +1 -1
  18. package/dist/generator/lists.d.ts +31 -0
  19. package/dist/generator/lists.d.ts.map +1 -0
  20. package/dist/generator/lists.js +123 -0
  21. package/dist/generator/lists.js.map +1 -0
  22. package/dist/generator/plugin-types.d.ts +10 -0
  23. package/dist/generator/plugin-types.d.ts.map +1 -0
  24. package/dist/generator/plugin-types.js +122 -0
  25. package/dist/generator/plugin-types.js.map +1 -0
  26. package/dist/generator/prisma-config.d.ts +17 -0
  27. package/dist/generator/prisma-config.d.ts.map +1 -0
  28. package/dist/generator/prisma-config.js +40 -0
  29. package/dist/generator/prisma-config.js.map +1 -0
  30. package/dist/generator/prisma-extensions.d.ts +11 -0
  31. package/dist/generator/prisma-extensions.d.ts.map +1 -0
  32. package/dist/generator/prisma-extensions.js +134 -0
  33. package/dist/generator/prisma-extensions.js.map +1 -0
  34. package/dist/generator/prisma.d.ts.map +1 -1
  35. package/dist/generator/prisma.js +5 -2
  36. package/dist/generator/prisma.js.map +1 -1
  37. package/dist/generator/types.d.ts.map +1 -1
  38. package/dist/generator/types.js +201 -17
  39. package/dist/generator/types.js.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/mcp/lib/documentation-provider.d.ts +43 -0
  43. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -0
  44. package/dist/mcp/lib/documentation-provider.js +163 -0
  45. package/dist/mcp/lib/documentation-provider.js.map +1 -0
  46. package/dist/mcp/lib/features/catalog.d.ts +26 -0
  47. package/dist/mcp/lib/features/catalog.d.ts.map +1 -0
  48. package/dist/mcp/lib/features/catalog.js +291 -0
  49. package/dist/mcp/lib/features/catalog.js.map +1 -0
  50. package/dist/mcp/lib/generators/feature-generator.d.ts +35 -0
  51. package/dist/mcp/lib/generators/feature-generator.d.ts.map +1 -0
  52. package/dist/mcp/lib/generators/feature-generator.js +546 -0
  53. package/dist/mcp/lib/generators/feature-generator.js.map +1 -0
  54. package/dist/mcp/lib/types.d.ts +80 -0
  55. package/dist/mcp/lib/types.d.ts.map +1 -0
  56. package/dist/mcp/lib/types.js +5 -0
  57. package/dist/mcp/lib/types.js.map +1 -0
  58. package/dist/mcp/lib/wizards/wizard-engine.d.ts +71 -0
  59. package/dist/mcp/lib/wizards/wizard-engine.d.ts.map +1 -0
  60. package/dist/mcp/lib/wizards/wizard-engine.js +356 -0
  61. package/dist/mcp/lib/wizards/wizard-engine.js.map +1 -0
  62. package/dist/mcp/server/index.d.ts +8 -0
  63. package/dist/mcp/server/index.d.ts.map +1 -0
  64. package/dist/mcp/server/index.js +202 -0
  65. package/dist/mcp/server/index.js.map +1 -0
  66. package/dist/mcp/server/stack-mcp-server.d.ts +92 -0
  67. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -0
  68. package/dist/mcp/server/stack-mcp-server.js +265 -0
  69. package/dist/mcp/server/stack-mcp-server.js.map +1 -0
  70. package/package.json +10 -8
  71. package/src/commands/__snapshots__/generate.test.ts.snap +145 -38
  72. package/src/commands/dev.test.ts +0 -1
  73. package/src/commands/generate.test.ts +18 -8
  74. package/src/commands/generate.ts +20 -19
  75. package/src/commands/mcp.ts +135 -0
  76. package/src/generator/__snapshots__/context.test.ts.snap +63 -18
  77. package/src/generator/__snapshots__/prisma.test.ts.snap +8 -16
  78. package/src/generator/__snapshots__/types.test.ts.snap +1267 -95
  79. package/src/generator/context.test.ts +15 -8
  80. package/src/generator/context.ts +40 -7
  81. package/src/generator/index.ts +4 -1
  82. package/src/generator/lists.test.ts +335 -0
  83. package/src/generator/lists.ts +140 -0
  84. package/src/generator/plugin-types.ts +147 -0
  85. package/src/generator/prisma-config.ts +46 -0
  86. package/src/generator/prisma-extensions.ts +159 -0
  87. package/src/generator/prisma.test.ts +0 -10
  88. package/src/generator/prisma.ts +6 -2
  89. package/src/generator/types.test.ts +0 -12
  90. package/src/generator/types.ts +257 -17
  91. package/src/index.ts +4 -0
  92. package/src/mcp/lib/documentation-provider.ts +203 -0
  93. package/src/mcp/lib/features/catalog.ts +301 -0
  94. package/src/mcp/lib/generators/feature-generator.ts +598 -0
  95. package/src/mcp/lib/types.ts +89 -0
  96. package/src/mcp/lib/wizards/wizard-engine.ts +427 -0
  97. package/src/mcp/server/index.ts +240 -0
  98. package/src/mcp/server/stack-mcp-server.ts +301 -0
  99. package/tsconfig.tsbuildinfo +1 -1
  100. package/dist/generator/type-patcher.d.ts +0 -13
  101. package/dist/generator/type-patcher.d.ts.map +0 -1
  102. package/dist/generator/type-patcher.js +0 -68
  103. package/dist/generator/type-patcher.js.map +0 -1
  104. package/src/generator/type-patcher.ts +0 -93
@@ -12,8 +12,9 @@ exports[`Generate Command Integration > Generator Integration > should generate
12
12
 
13
13
  import { getContext as getOpensaasContext } from '@opensaas/stack-core'
14
14
  import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
15
- import { PrismaClient } from './prisma-client'
15
+ import { PrismaClient } from './prisma-client/client'
16
16
  import type { Context } from './types'
17
+ import { prismaExtensions } from './prisma-extensions'
17
18
  import configOrPromise from '../opensaas.config'
18
19
 
19
20
  // Resolve config if it's a Promise (when plugins are present)
@@ -21,15 +22,29 @@ const configPromise = Promise.resolve(configOrPromise)
21
22
  let resolvedConfig: OpenSaasConfig | null = null
22
23
 
23
24
  // Internal Prisma singleton - managed automatically
24
- const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
25
- let prisma: PrismaClient | null = null
25
+ const globalForPrisma = globalThis as unknown as { prisma: ReturnType<typeof createExtendedPrisma> | null }
26
+ let prisma: ReturnType<typeof createExtendedPrisma> | null = null
27
+
28
+ /**
29
+ * Create Prisma client with result extensions
30
+ */
31
+ function createExtendedPrisma(basePrisma: PrismaClient) {
32
+ // Check if there are any extensions to apply
33
+ if (Object.keys(prismaExtensions).length === 0) {
34
+ return basePrisma
35
+ }
36
+ // Apply result extensions
37
+ return basePrisma.$extends(prismaExtensions)
38
+ }
26
39
 
27
40
  async function getPrisma() {
28
41
  if (!prisma) {
29
42
  if (!resolvedConfig) {
30
43
  resolvedConfig = await configPromise
31
44
  }
32
- prisma = globalForPrisma.prisma ?? new PrismaClient()
45
+ const basePrisma = resolvedConfig.db.prismaClientConstructor!(PrismaClient)
46
+ const extendedPrisma = createExtendedPrisma(basePrisma)
47
+ prisma = globalForPrisma.prisma ?? extendedPrisma
33
48
  if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
34
49
  }
35
50
  return prisma
@@ -84,7 +99,7 @@ const storage = {
84
99
  export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
85
100
  const config = await getConfig()
86
101
  const prismaClient = await getPrisma()
87
- return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
102
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as unknown as Context<TSession>
88
103
  }
89
104
 
90
105
  /**
@@ -94,7 +109,7 @@ export async function getContext<TSession extends OpensaasSession = OpensaasSess
94
109
  export const rawOpensaasContext = (async () => {
95
110
  const config = await getConfig()
96
111
  const prismaClient = await getPrisma()
97
- return getOpensaasContext(config, prismaClient, null, storage)
112
+ return getOpensaasContext(config, prismaClient, null, storage) as unknown as Context
98
113
  })()
99
114
 
100
115
  /**
@@ -107,13 +122,12 @@ export const config = getConfig()
107
122
 
108
123
  exports[`Generate Command Integration > Generator Integration > should generate all files for a basic config > prisma-schema 1`] = `
109
124
  "generator client {
110
- provider = "prisma-client-js"
125
+ provider = "prisma-client"
111
126
  output = "../.opensaas/prisma-client"
112
127
  }
113
128
 
114
129
  datasource db {
115
130
  provider = "sqlite"
116
- url = env("DATABASE_URL")
117
131
  }
118
132
 
119
133
  model User {
@@ -132,16 +146,35 @@ exports[`Generate Command Integration > Generator Integration > should generate
132
146
  * DO NOT EDIT - This file is automatically generated
133
147
  */
134
148
 
135
- import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB } from '@opensaas/stack-core'
136
- import type { PrismaClient } from './prisma-client'
149
+ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB, AccessContext } from '@opensaas/stack-core'
150
+ import type { PrismaClient, Prisma } from './prisma-client/client'
151
+ import type { PluginServices } from './plugin-types'
152
+
153
+ /**
154
+ * Virtual fields for User - computed fields not in database
155
+ * These are added to query results via resolveOutput hooks
156
+ */
157
+ export type UserVirtualFields = {
158
+ // No virtual fields defined
159
+ }
160
+
161
+ /**
162
+ * Transformed fields for User - fields with resultExtension transformations
163
+ * These override Prisma's base types with transformed types via result extensions
164
+ */
165
+ export type UserTransformedFields = {
166
+ // No transformed fields defined
167
+ }
137
168
 
138
- export type User = {
169
+ export type UserOutput = {
139
170
  id: string
140
171
  name: string
141
172
  email: string
142
173
  createdAt: Date
143
174
  updatedAt: Date
144
- }
175
+ } & UserVirtualFields
176
+
177
+ export type User = UserOutput
145
178
 
146
179
  export type UserCreateInput = {
147
180
  name: string
@@ -162,26 +195,94 @@ export type UserWhereInput = {
162
195
  email?: { equals?: string, not?: string }
163
196
  }
164
197
 
165
- export type Context<TSession extends OpensaasSession = OpensaasSession> = {
166
- db: AccessControlledDB<PrismaClient>
198
+ /**
199
+ * Hook types for User list
200
+ * Properly typed to use Prisma's generated input types
201
+ */
202
+ export type UserHooks = {
203
+ resolveInput?: (args:
204
+ | {
205
+ operation: 'create'
206
+ resolvedData: Prisma.UserCreateInput
207
+ item: undefined
208
+ context: import('@opensaas/stack-core').AccessContext
209
+ }
210
+ | {
211
+ operation: 'update'
212
+ resolvedData: Prisma.UserUpdateInput
213
+ item: User
214
+ context: import('@opensaas/stack-core').AccessContext
215
+ }
216
+ ) => Promise<Prisma.UserCreateInput | Prisma.UserUpdateInput>
217
+ validateInput?: (args: {
218
+ operation: 'create' | 'update'
219
+ resolvedData: Prisma.UserCreateInput | Prisma.UserUpdateInput
220
+ item?: User
221
+ context: import('@opensaas/stack-core').AccessContext
222
+ addValidationError: (msg: string) => void
223
+ }) => Promise<void>
224
+ beforeOperation?: (args: {
225
+ operation: 'create' | 'update' | 'delete'
226
+ resolvedData?: Prisma.UserCreateInput | Prisma.UserUpdateInput
227
+ item?: User
228
+ context: import('@opensaas/stack-core').AccessContext
229
+ }) => Promise<void>
230
+ afterOperation?: (args: {
231
+ operation: 'create' | 'update' | 'delete'
232
+ resolvedData?: Prisma.UserCreateInput | Prisma.UserUpdateInput
233
+ item?: User
234
+ context: import('@opensaas/stack-core').AccessContext
235
+ }) => Promise<void>
236
+ }
237
+
238
+ /**
239
+ * Custom DB type that uses Prisma's conditional types with virtual and transformed field support
240
+ * Types change based on select/include - relationships only present when explicitly included
241
+ * Virtual fields and transformed fields are added to the base model type
242
+ */
243
+ export type CustomDB = Omit<AccessControlledDB<PrismaClient>,
244
+ 'user'
245
+ > & {
246
+ user: {
247
+ findUnique: <T extends Prisma.UserFindUniqueArgs>(
248
+ args: Prisma.SelectSubset<T, Prisma.UserFindUniqueArgs>
249
+ ) => Promise<(Omit<Prisma.UserGetPayload<T>, keyof UserTransformedFields> & UserTransformedFields & UserVirtualFields) | null>
250
+ findMany: <T extends Prisma.UserFindManyArgs>(
251
+ args?: Prisma.SelectSubset<T, Prisma.UserFindManyArgs>
252
+ ) => Promise<Array<Omit<Prisma.UserGetPayload<T>, keyof UserTransformedFields> & UserTransformedFields & UserVirtualFields>>
253
+ create: <T extends Prisma.UserCreateArgs>(
254
+ args: Prisma.SelectSubset<T, Prisma.UserCreateArgs>
255
+ ) => Promise<Omit<Prisma.UserGetPayload<T>, keyof UserTransformedFields> & UserTransformedFields & UserVirtualFields>
256
+ update: <T extends Prisma.UserUpdateArgs>(
257
+ args: Prisma.SelectSubset<T, Prisma.UserUpdateArgs>
258
+ ) => Promise<(Omit<Prisma.UserGetPayload<T>, keyof UserTransformedFields> & UserTransformedFields & UserVirtualFields) | null>
259
+ delete: <T extends Prisma.UserDeleteArgs>(
260
+ args: Prisma.SelectSubset<T, Prisma.UserDeleteArgs>
261
+ ) => Promise<(Omit<Prisma.UserGetPayload<T>, keyof UserTransformedFields> & UserTransformedFields & UserVirtualFields) | null>
262
+ count: (args?: Prisma.UserCountArgs) => Promise<number>
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Context type compatible with AccessContext but with CustomDB for virtual field typing
268
+ * Extends AccessContext and overrides db property to include virtual fields in output types
269
+ */
270
+ export type Context<TSession extends OpensaasSession = OpensaasSession> = Omit<AccessContext<PrismaClient>, 'db' | 'session'> & {
271
+ db: CustomDB
167
272
  session: TSession
168
- prisma: PrismaClient
169
- storage: StorageUtils
170
273
  serverAction: (props: ServerActionProps) => Promise<unknown>
171
274
  sudo: () => Context<TSession>
172
- _isSudo: boolean
173
275
  }"
174
276
  `;
175
277
 
176
278
  exports[`Generate Command Integration > Generator Integration > should generate consistent output across multiple runs > consistent-output 1`] = `
177
279
  "generator client {
178
- provider = "prisma-client-js"
280
+ provider = "prisma-client"
179
281
  output = "../.opensaas/prisma-client"
180
282
  }
181
283
 
182
284
  datasource db {
183
285
  provider = "sqlite"
184
- url = env("DATABASE_URL")
185
286
  }
186
287
 
187
288
  model User {
@@ -195,52 +296,48 @@ model User {
195
296
 
196
297
  exports[`Generate Command Integration > Generator Integration > should handle different database providers > mysql-provider 1`] = `
197
298
  "generator client {
198
- provider = "prisma-client-js"
299
+ provider = "prisma-client"
199
300
  output = "../.opensaas/prisma-client"
200
301
  }
201
302
 
202
303
  datasource db {
203
304
  provider = "mysql"
204
- url = env("DATABASE_URL")
205
305
  }
206
306
  "
207
307
  `;
208
308
 
209
309
  exports[`Generate Command Integration > Generator Integration > should handle different database providers > postgresql-provider 1`] = `
210
310
  "generator client {
211
- provider = "prisma-client-js"
311
+ provider = "prisma-client"
212
312
  output = "../.opensaas/prisma-client"
213
313
  }
214
314
 
215
315
  datasource db {
216
316
  provider = "postgresql"
217
- url = env("DATABASE_URL")
218
317
  }
219
318
  "
220
319
  `;
221
320
 
222
321
  exports[`Generate Command Integration > Generator Integration > should handle different database providers > sqlite-provider 1`] = `
223
322
  "generator client {
224
- provider = "prisma-client-js"
323
+ provider = "prisma-client"
225
324
  output = "../.opensaas/prisma-client"
226
325
  }
227
326
 
228
327
  datasource db {
229
328
  provider = "sqlite"
230
- url = env("DATABASE_URL")
231
329
  }
232
330
  "
233
331
  `;
234
332
 
235
333
  exports[`Generate Command Integration > Generator Integration > should handle empty lists config > empty-lists-schema 1`] = `
236
334
  "generator client {
237
- provider = "prisma-client-js"
335
+ provider = "prisma-client"
238
336
  output = "../.opensaas/prisma-client"
239
337
  }
240
338
 
241
339
  datasource db {
242
340
  provider = "sqlite"
243
- url = env("DATABASE_URL")
244
341
  }
245
342
  "
246
343
  `;
@@ -251,29 +348,40 @@ exports[`Generate Command Integration > Generator Integration > should handle em
251
348
  * DO NOT EDIT - This file is automatically generated
252
349
  */
253
350
 
254
- import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB } from '@opensaas/stack-core'
255
- import type { PrismaClient } from './prisma-client'
351
+ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB, AccessContext } from '@opensaas/stack-core'
352
+ import type { PrismaClient, Prisma } from './prisma-client/client'
353
+ import type { PluginServices } from './plugin-types'
354
+
355
+ /**
356
+ * Custom DB type that uses Prisma's conditional types with virtual and transformed field support
357
+ * Types change based on select/include - relationships only present when explicitly included
358
+ * Virtual fields and transformed fields are added to the base model type
359
+ */
360
+ export type CustomDB = Omit<AccessControlledDB<PrismaClient>,
361
+
362
+ > & {
363
+ }
256
364
 
257
- export type Context<TSession extends OpensaasSession = OpensaasSession> = {
258
- db: AccessControlledDB<PrismaClient>
365
+ /**
366
+ * Context type compatible with AccessContext but with CustomDB for virtual field typing
367
+ * Extends AccessContext and overrides db property to include virtual fields in output types
368
+ */
369
+ export type Context<TSession extends OpensaasSession = OpensaasSession> = Omit<AccessContext<PrismaClient>, 'db' | 'session'> & {
370
+ db: CustomDB
259
371
  session: TSession
260
- prisma: PrismaClient
261
- storage: StorageUtils
262
372
  serverAction: (props: ServerActionProps) => Promise<unknown>
263
373
  sudo: () => Context<TSession>
264
- _isSudo: boolean
265
374
  }"
266
375
  `;
267
376
 
268
377
  exports[`Generate Command Integration > Generator Integration > should overwrite existing files > overwrite-after 1`] = `
269
378
  "generator client {
270
- provider = "prisma-client-js"
379
+ provider = "prisma-client"
271
380
  output = "../.opensaas/prisma-client"
272
381
  }
273
382
 
274
383
  datasource db {
275
384
  provider = "sqlite"
276
- url = env("DATABASE_URL")
277
385
  }
278
386
 
279
387
  model Post {
@@ -287,13 +395,12 @@ model Post {
287
395
 
288
396
  exports[`Generate Command Integration > Generator Integration > should overwrite existing files > overwrite-before 1`] = `
289
397
  "generator client {
290
- provider = "prisma-client-js"
398
+ provider = "prisma-client"
291
399
  output = "../.opensaas/prisma-client"
292
400
  }
293
401
 
294
402
  datasource db {
295
403
  provider = "sqlite"
296
- url = env("DATABASE_URL")
297
404
  }
298
405
 
299
406
  model User {
@@ -73,7 +73,6 @@ describe('Dev Command', () => {
73
73
  `
74
74
  import { config } from '@opensaas/stack-core'
75
75
  export default config({
76
- db: { provider: 'sqlite', url: 'file:./dev.db' },
77
76
  lists: {}
78
77
  })
79
78
  `,
@@ -48,7 +48,8 @@ describe('Generate Command Integration', () => {
48
48
  const config: OpenSaasConfig = {
49
49
  db: {
50
50
  provider: 'sqlite',
51
- url: 'file:./dev.db',
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ prismaClientConstructor: (() => null) as any,
52
53
  },
53
54
  lists: {
54
55
  User: {
@@ -89,7 +90,8 @@ describe('Generate Command Integration', () => {
89
90
  const config: OpenSaasConfig = {
90
91
  db: {
91
92
  provider: 'sqlite',
92
- url: 'file:./dev.db',
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ prismaClientConstructor: (() => null) as any,
93
95
  },
94
96
  lists: {},
95
97
  }
@@ -106,7 +108,8 @@ describe('Generate Command Integration', () => {
106
108
  const config1: OpenSaasConfig = {
107
109
  db: {
108
110
  provider: 'sqlite',
109
- url: 'file:./dev.db',
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ prismaClientConstructor: (() => null) as any,
110
113
  },
111
114
  lists: {
112
115
  User: {
@@ -120,7 +123,8 @@ describe('Generate Command Integration', () => {
120
123
  const config2: OpenSaasConfig = {
121
124
  db: {
122
125
  provider: 'sqlite',
123
- url: 'file:./dev.db',
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ prismaClientConstructor: (() => null) as any,
124
128
  },
125
129
  lists: {
126
130
  Post: {
@@ -148,7 +152,8 @@ describe('Generate Command Integration', () => {
148
152
  const config: OpenSaasConfig = {
149
153
  db: {
150
154
  provider: 'sqlite',
151
- url: 'file:./dev.db',
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
+ prismaClientConstructor: (() => null) as any,
152
157
  },
153
158
  opensaasPath: '.custom',
154
159
  lists: {},
@@ -169,7 +174,8 @@ describe('Generate Command Integration', () => {
169
174
  const config: OpenSaasConfig = {
170
175
  db: {
171
176
  provider: 'sqlite',
172
- url: 'file:./dev.db',
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ prismaClientConstructor: (() => null) as any,
173
179
  },
174
180
  lists: {
175
181
  User: {
@@ -198,7 +204,8 @@ describe('Generate Command Integration', () => {
198
204
  const config: OpenSaasConfig = {
199
205
  db: {
200
206
  provider: 'sqlite',
201
- url: 'file:./dev.db',
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
+ prismaClientConstructor: (() => null) as any,
202
209
  },
203
210
  lists: {},
204
211
  }
@@ -227,6 +234,8 @@ describe('Generate Command Integration', () => {
227
234
  db: {
228
235
  provider,
229
236
  url: provider === 'sqlite' ? 'file:./dev.db' : 'postgresql://localhost:5432/db',
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ prismaClientConstructor: (() => null) as any,
230
239
  },
231
240
  lists: {},
232
241
  }
@@ -243,7 +252,8 @@ describe('Generate Command Integration', () => {
243
252
  const config: OpenSaasConfig = {
244
253
  db: {
245
254
  provider: 'sqlite',
246
- url: 'file:./dev.db',
255
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
256
+ prismaClientConstructor: (() => null) as any,
247
257
  },
248
258
  lists: {
249
259
  User: {
@@ -6,9 +6,12 @@ import ora from 'ora'
6
6
  import { createJiti } from 'jiti'
7
7
  import {
8
8
  writePrismaSchema,
9
+ writePrismaConfig,
9
10
  writeTypes,
11
+ writeLists,
10
12
  writeContext,
11
- patchPrismaTypes,
13
+ writePluginTypes,
14
+ writePrismaExtensions,
12
15
  } from '../generator/index.js'
13
16
  import { OpenSaasConfig } from '@opensaas/stack-core'
14
17
 
@@ -55,9 +58,8 @@ export async function generateCommand() {
55
58
 
56
59
  try {
57
60
  // Import plugin engine (avoid circular dependency)
58
- const { executeBeforeGenerateHooks } = await import(
59
- '@opensaas/stack-core/config/plugin-engine'
60
- )
61
+ const { executeBeforeGenerateHooks } =
62
+ await import('@opensaas/stack-core/config/plugin-engine')
61
63
  config = await executeBeforeGenerateHooks(config)
62
64
  pluginSpinner.succeed(chalk.green('Plugin beforeGenerate hooks complete'))
63
65
  } catch (err) {
@@ -70,17 +72,29 @@ export async function generateCommand() {
70
72
  const generatorSpinner = ora('Generating schema and types...').start()
71
73
  try {
72
74
  const prismaSchemaPath = path.join(cwd, 'prisma', 'schema.prisma')
75
+ const prismaConfigPath = path.join(cwd, 'prisma.config.ts')
73
76
  const typesPath = path.join(cwd, '.opensaas', 'types.ts')
77
+ const listsPath = path.join(cwd, '.opensaas', 'lists.ts')
74
78
  const contextPath = path.join(cwd, '.opensaas', 'context.ts')
79
+ const pluginTypesPath = path.join(cwd, '.opensaas', 'plugin-types.ts')
80
+ const prismaExtensionsPath = path.join(cwd, '.opensaas', 'prisma-extensions.ts')
75
81
 
76
82
  writePrismaSchema(config, prismaSchemaPath)
83
+ writePrismaConfig(config, prismaConfigPath)
77
84
  writeTypes(config, typesPath)
85
+ writeLists(config, listsPath)
78
86
  writeContext(config, contextPath)
87
+ writePluginTypes(config, pluginTypesPath)
88
+ writePrismaExtensions(config, prismaExtensionsPath)
79
89
 
80
90
  generatorSpinner.succeed(chalk.green('Schema generation complete'))
81
91
  console.log(chalk.green('✅ Prisma schema generated'))
92
+ console.log(chalk.green('✅ Prisma config generated'))
82
93
  console.log(chalk.green('✅ TypeScript types generated'))
94
+ console.log(chalk.green('✅ Lists namespace generated'))
83
95
  console.log(chalk.green('✅ Context factory generated'))
96
+ console.log(chalk.green('✅ Plugin types generated'))
97
+ console.log(chalk.green('✅ Prisma extensions generated'))
84
98
 
85
99
  // Execute afterGenerate hooks if plugins are present
86
100
  if (config.plugins && config.plugins.length > 0) {
@@ -95,9 +109,8 @@ export async function generateCommand() {
95
109
  }
96
110
 
97
111
  // Execute afterGenerate hooks
98
- const { executeAfterGenerateHooks } = await import(
99
- '@opensaas/stack-core/config/plugin-engine'
100
- )
112
+ const { executeAfterGenerateHooks } =
113
+ await import('@opensaas/stack-core/config/plugin-engine')
101
114
  const modifiedFiles = await executeAfterGenerateHooks(config, generatedFiles)
102
115
 
103
116
  // Write back modified files
@@ -153,18 +166,6 @@ export async function generateCommand() {
153
166
  process.exit(1)
154
167
  }
155
168
 
156
- // Patch Prisma types with field transformations
157
- const patchSpinner = ora('Patching Prisma types...').start()
158
- try {
159
- patchPrismaTypes(config, cwd)
160
- patchSpinner.succeed(chalk.green('Type patching complete'))
161
- } catch (err) {
162
- patchSpinner.fail(chalk.red('Failed to patch types'))
163
- const message = err instanceof Error ? err.message : String(err)
164
- console.error(chalk.red('\n❌ Error:'), message)
165
- process.exit(1)
166
- }
167
-
168
169
  console.log(chalk.bold('\n✨ Generation complete!\n'))
169
170
  console.log(chalk.gray('Next steps:'))
170
171
  console.log(chalk.gray(' 1. Run: npx prisma db push'))
@@ -0,0 +1,135 @@
1
+ /**
2
+ * MCP command group for AI-assisted development
3
+ */
4
+
5
+ import { Command } from 'commander'
6
+ import { spawn } from 'child_process'
7
+ import { fileURLToPath } from 'url'
8
+ import { dirname, join } from 'path'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+
13
+ function getServerPath(): string {
14
+ // In development: cli/dist/mcp/server/index.js
15
+ // In production: same structure
16
+ return join(__dirname, '..', 'mcp', 'server', 'index.js')
17
+ }
18
+
19
+ function installMCPServer(): Promise<void> {
20
+ return new Promise((resolve, reject) => {
21
+ console.log('📦 Installing OpenSaaS Stack MCP server...\n')
22
+
23
+ const serverPath = getServerPath()
24
+ const claudeCommand = ['claude', 'mcp', 'add', 'opensaas-stack', '--', 'node', serverPath]
25
+
26
+ console.log(`Running: ${claudeCommand.join(' ')}\n`)
27
+
28
+ const child = spawn(claudeCommand[0], claudeCommand.slice(1), {
29
+ stdio: 'inherit',
30
+ })
31
+
32
+ child.on('close', (code) => {
33
+ if (code === 0) {
34
+ console.log('\n✅ OpenSaaS Stack MCP server installed successfully!')
35
+ console.log('\n📖 Available tools:')
36
+ console.log(' - opensaas_implement_feature')
37
+ console.log(' - opensaas_feature_docs')
38
+ console.log(' - opensaas_list_features')
39
+ console.log(' - opensaas_suggest_features')
40
+ console.log(' - opensaas_validate_feature')
41
+ console.log('\n🚀 Restart Claude Code to use the MCP tools.')
42
+ resolve()
43
+ } else {
44
+ console.error(
45
+ `\n❌ Installation failed with code ${code}. Please ensure Claude Code is installed.`,
46
+ )
47
+ reject(new Error(`Installation failed with code ${code}`))
48
+ }
49
+ })
50
+
51
+ child.on('error', (error) => {
52
+ console.error('\n❌ Error during installation:', error.message)
53
+ console.error('\nMake sure Claude Code is installed and the "claude" command is available.')
54
+ reject(error)
55
+ })
56
+ })
57
+ }
58
+
59
+ function uninstallMCPServer(): Promise<void> {
60
+ return new Promise((resolve, reject) => {
61
+ console.log('🗑️ Uninstalling OpenSaaS Stack MCP server...\n')
62
+
63
+ const child = spawn('claude', ['mcp', 'remove', 'opensaas-stack'], {
64
+ stdio: 'inherit',
65
+ })
66
+
67
+ child.on('close', (code) => {
68
+ if (code === 0) {
69
+ console.log('\n✅ OpenSaaS Stack MCP server uninstalled successfully.')
70
+ resolve()
71
+ } else {
72
+ console.error(`\n❌ Uninstall failed with code ${code}`)
73
+ reject(new Error(`Uninstall failed with code ${code}`))
74
+ }
75
+ })
76
+
77
+ child.on('error', (error) => {
78
+ console.error('\n❌ Error during uninstall:', error.message)
79
+ reject(error)
80
+ })
81
+ })
82
+ }
83
+
84
+ function startMCPServer(): void {
85
+ console.log('🚀 Starting OpenSaaS Stack MCP server...\n')
86
+
87
+ const serverPath = getServerPath()
88
+
89
+ const child = spawn('node', [serverPath], {
90
+ stdio: 'inherit',
91
+ })
92
+
93
+ child.on('error', (error) => {
94
+ console.error('❌ Error starting server:', error.message)
95
+ process.exit(1)
96
+ })
97
+ }
98
+
99
+ export function createMCPCommand(): Command {
100
+ const mcp = new Command('mcp')
101
+ mcp.description('MCP server for AI-assisted development with Claude Code')
102
+
103
+ mcp
104
+ .command('install')
105
+ .description('Install MCP server in Claude Code')
106
+ .action(async () => {
107
+ try {
108
+ await installMCPServer()
109
+ process.exit(0)
110
+ } catch {
111
+ process.exit(1)
112
+ }
113
+ })
114
+
115
+ mcp
116
+ .command('uninstall')
117
+ .description('Remove MCP server from Claude Code')
118
+ .action(async () => {
119
+ try {
120
+ await uninstallMCPServer()
121
+ process.exit(0)
122
+ } catch {
123
+ process.exit(1)
124
+ }
125
+ })
126
+
127
+ mcp
128
+ .command('start')
129
+ .description('Start MCP server directly (for debugging)')
130
+ .action(() => {
131
+ startMCPServer()
132
+ })
133
+
134
+ return mcp
135
+ }