@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +348 -0
- package/CLAUDE.md +60 -12
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +13 -13
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +116 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/generator/context.d.ts.map +1 -1
- package/dist/generator/context.js +40 -7
- package/dist/generator/context.js.map +1 -1
- package/dist/generator/index.d.ts +4 -1
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +4 -1
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/lists.d.ts +31 -0
- package/dist/generator/lists.d.ts.map +1 -0
- package/dist/generator/lists.js +123 -0
- package/dist/generator/lists.js.map +1 -0
- package/dist/generator/plugin-types.d.ts +10 -0
- package/dist/generator/plugin-types.d.ts.map +1 -0
- package/dist/generator/plugin-types.js +122 -0
- package/dist/generator/plugin-types.js.map +1 -0
- package/dist/generator/prisma-config.d.ts +17 -0
- package/dist/generator/prisma-config.d.ts.map +1 -0
- package/dist/generator/prisma-config.js +40 -0
- package/dist/generator/prisma-config.js.map +1 -0
- package/dist/generator/prisma-extensions.d.ts +11 -0
- package/dist/generator/prisma-extensions.d.ts.map +1 -0
- package/dist/generator/prisma-extensions.js +134 -0
- package/dist/generator/prisma-extensions.js.map +1 -0
- package/dist/generator/prisma.d.ts.map +1 -1
- package/dist/generator/prisma.js +5 -2
- package/dist/generator/prisma.js.map +1 -1
- package/dist/generator/types.d.ts.map +1 -1
- package/dist/generator/types.js +201 -17
- package/dist/generator/types.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/lib/documentation-provider.d.ts +43 -0
- package/dist/mcp/lib/documentation-provider.d.ts.map +1 -0
- package/dist/mcp/lib/documentation-provider.js +163 -0
- package/dist/mcp/lib/documentation-provider.js.map +1 -0
- package/dist/mcp/lib/features/catalog.d.ts +26 -0
- package/dist/mcp/lib/features/catalog.d.ts.map +1 -0
- package/dist/mcp/lib/features/catalog.js +291 -0
- package/dist/mcp/lib/features/catalog.js.map +1 -0
- package/dist/mcp/lib/generators/feature-generator.d.ts +35 -0
- package/dist/mcp/lib/generators/feature-generator.d.ts.map +1 -0
- package/dist/mcp/lib/generators/feature-generator.js +546 -0
- package/dist/mcp/lib/generators/feature-generator.js.map +1 -0
- package/dist/mcp/lib/types.d.ts +80 -0
- package/dist/mcp/lib/types.d.ts.map +1 -0
- package/dist/mcp/lib/types.js +5 -0
- package/dist/mcp/lib/types.js.map +1 -0
- package/dist/mcp/lib/wizards/wizard-engine.d.ts +71 -0
- package/dist/mcp/lib/wizards/wizard-engine.d.ts.map +1 -0
- package/dist/mcp/lib/wizards/wizard-engine.js +356 -0
- package/dist/mcp/lib/wizards/wizard-engine.js.map +1 -0
- package/dist/mcp/server/index.d.ts +8 -0
- package/dist/mcp/server/index.d.ts.map +1 -0
- package/dist/mcp/server/index.js +202 -0
- package/dist/mcp/server/index.js.map +1 -0
- package/dist/mcp/server/stack-mcp-server.d.ts +92 -0
- package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -0
- package/dist/mcp/server/stack-mcp-server.js +265 -0
- package/dist/mcp/server/stack-mcp-server.js.map +1 -0
- package/package.json +10 -8
- package/src/commands/__snapshots__/generate.test.ts.snap +145 -38
- package/src/commands/dev.test.ts +0 -1
- package/src/commands/generate.test.ts +18 -8
- package/src/commands/generate.ts +20 -19
- package/src/commands/mcp.ts +135 -0
- package/src/generator/__snapshots__/context.test.ts.snap +63 -18
- package/src/generator/__snapshots__/prisma.test.ts.snap +8 -16
- package/src/generator/__snapshots__/types.test.ts.snap +1267 -95
- package/src/generator/context.test.ts +15 -8
- package/src/generator/context.ts +40 -7
- package/src/generator/index.ts +4 -1
- package/src/generator/lists.test.ts +335 -0
- package/src/generator/lists.ts +140 -0
- package/src/generator/plugin-types.ts +147 -0
- package/src/generator/prisma-config.ts +46 -0
- package/src/generator/prisma-extensions.ts +159 -0
- package/src/generator/prisma.test.ts +0 -10
- package/src/generator/prisma.ts +6 -2
- package/src/generator/types.test.ts +0 -12
- package/src/generator/types.ts +257 -17
- package/src/index.ts +4 -0
- package/src/mcp/lib/documentation-provider.ts +203 -0
- package/src/mcp/lib/features/catalog.ts +301 -0
- package/src/mcp/lib/generators/feature-generator.ts +598 -0
- package/src/mcp/lib/types.ts +89 -0
- package/src/mcp/lib/wizards/wizard-engine.ts +427 -0
- package/src/mcp/server/index.ts +240 -0
- package/src/mcp/server/stack-mcp-server.ts +301 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/generator/type-patcher.d.ts +0 -13
- package/dist/generator/type-patcher.d.ts.map +0 -1
- package/dist/generator/type-patcher.js +0 -68
- package/dist/generator/type-patcher.js.map +0 -1
- 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:
|
|
25
|
-
let prisma:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
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
|
|
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 {
|
package/src/commands/dev.test.ts
CHANGED
|
@@ -48,7 +48,8 @@ describe('Generate Command Integration', () => {
|
|
|
48
48
|
const config: OpenSaasConfig = {
|
|
49
49
|
db: {
|
|
50
50
|
provider: 'sqlite',
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
256
|
+
prismaClientConstructor: (() => null) as any,
|
|
247
257
|
},
|
|
248
258
|
lists: {
|
|
249
259
|
User: {
|
package/src/commands/generate.ts
CHANGED
|
@@ -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
|
-
|
|
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 } =
|
|
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 } =
|
|
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
|
+
}
|