@opensaas/stack-cli 0.4.0 → 0.6.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 (90) hide show
  1. package/README.md +76 -0
  2. package/dist/commands/migrate.d.ts +9 -0
  3. package/dist/commands/migrate.d.ts.map +1 -0
  4. package/dist/commands/migrate.js +299 -0
  5. package/dist/commands/migrate.js.map +1 -0
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/mcp/lib/documentation-provider.d.ts +23 -0
  9. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -1
  10. package/dist/mcp/lib/documentation-provider.js +471 -0
  11. package/dist/mcp/lib/documentation-provider.js.map +1 -1
  12. package/dist/mcp/lib/wizards/migration-wizard.d.ts +80 -0
  13. package/dist/mcp/lib/wizards/migration-wizard.d.ts.map +1 -0
  14. package/dist/mcp/lib/wizards/migration-wizard.js +499 -0
  15. package/dist/mcp/lib/wizards/migration-wizard.js.map +1 -0
  16. package/dist/mcp/server/index.d.ts.map +1 -1
  17. package/dist/mcp/server/index.js +103 -0
  18. package/dist/mcp/server/index.js.map +1 -1
  19. package/dist/mcp/server/stack-mcp-server.d.ts +85 -0
  20. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -1
  21. package/dist/mcp/server/stack-mcp-server.js +219 -0
  22. package/dist/mcp/server/stack-mcp-server.js.map +1 -1
  23. package/dist/migration/generators/migration-generator.d.ts +60 -0
  24. package/dist/migration/generators/migration-generator.d.ts.map +1 -0
  25. package/dist/migration/generators/migration-generator.js +510 -0
  26. package/dist/migration/generators/migration-generator.js.map +1 -0
  27. package/dist/migration/introspectors/index.d.ts +12 -0
  28. package/dist/migration/introspectors/index.d.ts.map +1 -0
  29. package/dist/migration/introspectors/index.js +10 -0
  30. package/dist/migration/introspectors/index.js.map +1 -0
  31. package/dist/migration/introspectors/keystone-introspector.d.ts +59 -0
  32. package/dist/migration/introspectors/keystone-introspector.d.ts.map +1 -0
  33. package/dist/migration/introspectors/keystone-introspector.js +229 -0
  34. package/dist/migration/introspectors/keystone-introspector.js.map +1 -0
  35. package/dist/migration/introspectors/nextjs-introspector.d.ts +59 -0
  36. package/dist/migration/introspectors/nextjs-introspector.d.ts.map +1 -0
  37. package/dist/migration/introspectors/nextjs-introspector.js +159 -0
  38. package/dist/migration/introspectors/nextjs-introspector.js.map +1 -0
  39. package/dist/migration/introspectors/prisma-introspector.d.ts +45 -0
  40. package/dist/migration/introspectors/prisma-introspector.d.ts.map +1 -0
  41. package/dist/migration/introspectors/prisma-introspector.js +190 -0
  42. package/dist/migration/introspectors/prisma-introspector.js.map +1 -0
  43. package/dist/migration/types.d.ts +86 -0
  44. package/dist/migration/types.d.ts.map +1 -0
  45. package/dist/migration/types.js +5 -0
  46. package/dist/migration/types.js.map +1 -0
  47. package/package.json +10 -2
  48. package/plugin/.claude-plugin/plugin.json +15 -0
  49. package/plugin/README.md +112 -0
  50. package/plugin/agents/migration-assistant.md +150 -0
  51. package/plugin/commands/analyze-schema.md +34 -0
  52. package/plugin/commands/generate-config.md +33 -0
  53. package/plugin/commands/validate-migration.md +34 -0
  54. package/plugin/skills/opensaas-migration/SKILL.md +192 -0
  55. package/.turbo/turbo-build.log +0 -4
  56. package/CHANGELOG.md +0 -410
  57. package/CLAUDE.md +0 -298
  58. package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
  59. package/src/commands/dev.test.ts +0 -215
  60. package/src/commands/dev.ts +0 -48
  61. package/src/commands/generate.test.ts +0 -282
  62. package/src/commands/generate.ts +0 -182
  63. package/src/commands/init.ts +0 -34
  64. package/src/commands/mcp.ts +0 -135
  65. package/src/generator/__snapshots__/context.test.ts.snap +0 -361
  66. package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
  67. package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
  68. package/src/generator/context.test.ts +0 -139
  69. package/src/generator/context.ts +0 -227
  70. package/src/generator/index.ts +0 -7
  71. package/src/generator/lists.test.ts +0 -335
  72. package/src/generator/lists.ts +0 -140
  73. package/src/generator/plugin-types.ts +0 -147
  74. package/src/generator/prisma-config.ts +0 -46
  75. package/src/generator/prisma-extensions.ts +0 -159
  76. package/src/generator/prisma.test.ts +0 -211
  77. package/src/generator/prisma.ts +0 -161
  78. package/src/generator/types.test.ts +0 -268
  79. package/src/generator/types.ts +0 -537
  80. package/src/index.ts +0 -42
  81. package/src/mcp/lib/documentation-provider.ts +0 -203
  82. package/src/mcp/lib/features/catalog.ts +0 -301
  83. package/src/mcp/lib/generators/feature-generator.ts +0 -598
  84. package/src/mcp/lib/types.ts +0 -89
  85. package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
  86. package/src/mcp/server/index.ts +0 -240
  87. package/src/mcp/server/stack-mcp-server.ts +0 -301
  88. package/tsconfig.json +0 -13
  89. package/tsconfig.tsbuildinfo +0 -1
  90. package/vitest.config.ts +0 -26
@@ -1,537 +0,0 @@
1
- import type { OpenSaasConfig, FieldConfig, RelationshipField } from '@opensaas/stack-core'
2
- import * as fs from 'fs'
3
- import * as path from 'path'
4
-
5
- /**
6
- * Map OpenSaas field types to TypeScript types
7
- */
8
- function mapFieldTypeToTypeScript(field: FieldConfig): string | null {
9
- // Relationships are handled separately
10
- if (field.type === 'relationship') {
11
- return null
12
- }
13
-
14
- // Use field's own TypeScript type generator if available
15
- if (field.getTypeScriptType) {
16
- const result = field.getTypeScriptType()
17
- return result.type
18
- }
19
-
20
- // Fallback for fields without generator methods
21
- throw new Error(`Field type "${field.type}" does not implement getTypeScriptType method`)
22
- }
23
-
24
- /**
25
- * Check if a field is optional in the type
26
- */
27
- function isFieldOptional(field: FieldConfig): boolean {
28
- // Relationships are always nullable
29
- if (field.type === 'relationship') {
30
- return true
31
- }
32
-
33
- // Use field's own TypeScript type generator if available
34
- if (field.getTypeScriptType) {
35
- const result = field.getTypeScriptType()
36
- return result.optional
37
- }
38
-
39
- // Fallback: assume optional
40
- return true
41
- }
42
-
43
- /**
44
- * Generate virtual fields type - only contains virtual fields
45
- * This is intersected with Prisma's GetPayload to add virtual fields to query results
46
- */
47
- function generateVirtualFieldsType(listName: string, fields: Record<string, FieldConfig>): string {
48
- const lines: string[] = []
49
- const virtualFields = Object.entries(fields).filter(([_, config]) => config.type === 'virtual')
50
-
51
- lines.push(`/**`)
52
- lines.push(` * Virtual fields for ${listName} - computed fields not in database`)
53
- lines.push(` * These are added to query results via resolveOutput hooks`)
54
- lines.push(` */`)
55
- lines.push(`export type ${listName}VirtualFields = {`)
56
-
57
- for (const [fieldName, fieldConfig] of virtualFields) {
58
- const tsType = mapFieldTypeToTypeScript(fieldConfig)
59
- if (!tsType) continue
60
-
61
- const optional = isFieldOptional(fieldConfig)
62
- const nullability = optional ? ' | null' : ''
63
- lines.push(` ${fieldName}: ${tsType}${nullability}`)
64
- }
65
-
66
- // If no virtual fields, make it an empty object
67
- if (virtualFields.length === 0) {
68
- lines.push(' // No virtual fields defined')
69
- }
70
-
71
- lines.push('}')
72
-
73
- return lines.join('\n')
74
- }
75
-
76
- /**
77
- * Generate transformed fields type - fields with resultExtension transformations
78
- * This replaces Prisma's base types with transformed types (e.g., string -> HashedPassword)
79
- */
80
- function generateTransformedFieldsType(
81
- listName: string,
82
- fields: Record<string, FieldConfig>,
83
- ): string {
84
- const lines: string[] = []
85
- const transformedFields = Object.entries(fields).filter(([_, config]) => config.resultExtension)
86
-
87
- lines.push(`/**`)
88
- lines.push(` * Transformed fields for ${listName} - fields with resultExtension transformations`)
89
- lines.push(` * These override Prisma's base types with transformed types via result extensions`)
90
- lines.push(` */`)
91
- lines.push(`export type ${listName}TransformedFields = {`)
92
-
93
- for (const [fieldName, fieldConfig] of transformedFields) {
94
- if (fieldConfig.resultExtension) {
95
- const optional = isFieldOptional(fieldConfig)
96
- const nullability = optional ? ' | undefined' : ''
97
- lines.push(` ${fieldName}: ${fieldConfig.resultExtension.outputType}${nullability}`)
98
- }
99
- }
100
-
101
- // If no transformed fields, make it an empty object
102
- if (transformedFields.length === 0) {
103
- lines.push(' // No transformed fields defined')
104
- }
105
-
106
- lines.push('}')
107
-
108
- return lines.join('\n')
109
- }
110
-
111
- /**
112
- * Generate TypeScript Output type for a model (includes virtual fields)
113
- * This is kept for backwards compatibility but CustomDB uses Prisma's GetPayload + VirtualFields
114
- */
115
- function generateModelOutputType(listName: string, fields: Record<string, FieldConfig>): string {
116
- const lines: string[] = []
117
-
118
- lines.push(`export type ${listName}Output = {`)
119
- lines.push(' id: string')
120
-
121
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
122
- // Skip virtual fields - they're in VirtualFields type
123
- if (fieldConfig.type === 'virtual') continue
124
-
125
- if (fieldConfig.type === 'relationship') {
126
- const relField = fieldConfig as RelationshipField
127
- const [targetList] = relField.ref.split('.')
128
-
129
- if (relField.many) {
130
- lines.push(` ${fieldName}?: ${targetList}Output[]`) // Optional since only present with include
131
- } else {
132
- lines.push(` ${fieldName}Id: string | null`)
133
- lines.push(` ${fieldName}?: ${targetList}Output | null`) // Optional since only present with include
134
- }
135
- } else {
136
- const tsType = mapFieldTypeToTypeScript(fieldConfig)
137
- if (!tsType) continue // Skip if no type returned
138
-
139
- const optional = isFieldOptional(fieldConfig)
140
- const nullability = optional ? ' | null' : ''
141
- lines.push(` ${fieldName}: ${tsType}${nullability}`)
142
- }
143
- }
144
-
145
- lines.push(' createdAt: Date')
146
- lines.push(' updatedAt: Date')
147
- lines.push('} & ' + listName + 'VirtualFields') // Include virtual fields
148
-
149
- return lines.join('\n')
150
- }
151
-
152
- /**
153
- * Generate convenience type alias (List = ListOutput)
154
- */
155
- function generateModelTypeAlias(listName: string): string {
156
- return `export type ${listName} = ${listName}Output`
157
- }
158
-
159
- /**
160
- * Generate CreateInput type
161
- */
162
- function generateCreateInputType(listName: string, fields: Record<string, FieldConfig>): string {
163
- const lines: string[] = []
164
-
165
- lines.push(`export type ${listName}CreateInput = {`)
166
-
167
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
168
- // Skip virtual fields - they don't accept input in create operations
169
- // Virtual fields with resolveInput hooks handle side effects but don't store data
170
- if (fieldConfig.virtual) {
171
- continue
172
- }
173
-
174
- if (fieldConfig.type === 'relationship') {
175
- const relField = fieldConfig as RelationshipField
176
-
177
- if (relField.many) {
178
- lines.push(` ${fieldName}?: { connect: Array<{ id: string }> }`)
179
- } else {
180
- lines.push(` ${fieldName}?: { connect: { id: string } }`)
181
- }
182
- } else {
183
- const tsType = mapFieldTypeToTypeScript(fieldConfig)
184
- if (!tsType) continue // Skip if no type returned
185
-
186
- const required = !isFieldOptional(fieldConfig) && !fieldConfig.defaultValue
187
- const optional = required ? '' : '?'
188
- lines.push(` ${fieldName}${optional}: ${tsType}`)
189
- }
190
- }
191
-
192
- lines.push('}')
193
-
194
- return lines.join('\n')
195
- }
196
-
197
- /**
198
- * Generate UpdateInput type
199
- */
200
- function generateUpdateInputType(listName: string, fields: Record<string, FieldConfig>): string {
201
- const lines: string[] = []
202
-
203
- lines.push(`export type ${listName}UpdateInput = {`)
204
-
205
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
206
- // Virtual fields with resolveInput hooks can accept input for side effects
207
- // but we still skip them in the input type since they don't store data
208
- if (fieldConfig.virtual) {
209
- continue
210
- }
211
-
212
- if (fieldConfig.type === 'relationship') {
213
- const relField = fieldConfig as RelationshipField
214
-
215
- if (relField.many) {
216
- lines.push(
217
- ` ${fieldName}?: { connect: Array<{ id: string }>, disconnect: Array<{ id: string }> }`,
218
- )
219
- } else {
220
- lines.push(` ${fieldName}?: { connect: { id: string } } | { disconnect: true }`)
221
- }
222
- } else {
223
- const tsType = mapFieldTypeToTypeScript(fieldConfig)
224
- if (!tsType) continue // Skip if no type returned
225
-
226
- lines.push(` ${fieldName}?: ${tsType}`)
227
- }
228
- }
229
-
230
- lines.push('}')
231
-
232
- return lines.join('\n')
233
- }
234
-
235
- /**
236
- * Generate WhereInput type (simplified)
237
- */
238
- function generateWhereInputType(listName: string, fields: Record<string, FieldConfig>): string {
239
- const lines: string[] = []
240
-
241
- lines.push(`export type ${listName}WhereInput = {`)
242
- lines.push(' id?: string')
243
- lines.push(' AND?: Array<' + listName + 'WhereInput>')
244
- lines.push(' OR?: Array<' + listName + 'WhereInput>')
245
- lines.push(' NOT?: ' + listName + 'WhereInput')
246
-
247
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
248
- if (fieldConfig.type === 'relationship') {
249
- continue // Skip for now
250
- } else {
251
- const tsType = mapFieldTypeToTypeScript(fieldConfig)
252
- if (!tsType) continue // Skip if no type returned
253
-
254
- lines.push(` ${fieldName}?: { equals?: ${tsType}, not?: ${tsType} }`)
255
- }
256
- }
257
-
258
- lines.push('}')
259
-
260
- return lines.join('\n')
261
- }
262
-
263
- /**
264
- * Generate hook types that reference Prisma input types
265
- */
266
- function generateHookTypes(listName: string): string {
267
- const lines: string[] = []
268
-
269
- lines.push(`/**`)
270
- lines.push(` * Hook types for ${listName} list`)
271
- lines.push(` * Properly typed to use Prisma's generated input types`)
272
- lines.push(` */`)
273
- lines.push(`export type ${listName}Hooks = {`)
274
- lines.push(` resolveInput?: (args:`)
275
- lines.push(` | {`)
276
- lines.push(` operation: 'create'`)
277
- lines.push(` resolvedData: Prisma.${listName}CreateInput`)
278
- lines.push(` item: undefined`)
279
- lines.push(` context: import('@opensaas/stack-core').AccessContext`)
280
- lines.push(` }`)
281
- lines.push(` | {`)
282
- lines.push(` operation: 'update'`)
283
- lines.push(` resolvedData: Prisma.${listName}UpdateInput`)
284
- lines.push(` item: ${listName}`)
285
- lines.push(` context: import('@opensaas/stack-core').AccessContext`)
286
- lines.push(` }`)
287
- lines.push(` ) => Promise<Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput>`)
288
- lines.push(` validateInput?: (args: {`)
289
- lines.push(` operation: 'create' | 'update'`)
290
- lines.push(` resolvedData: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
291
- lines.push(` item?: ${listName}`)
292
- lines.push(` context: import('@opensaas/stack-core').AccessContext`)
293
- lines.push(` addValidationError: (msg: string) => void`)
294
- lines.push(` }) => Promise<void>`)
295
- lines.push(` beforeOperation?: (args: {`)
296
- lines.push(` operation: 'create' | 'update' | 'delete'`)
297
- lines.push(` resolvedData?: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
298
- lines.push(` item?: ${listName}`)
299
- lines.push(` context: import('@opensaas/stack-core').AccessContext`)
300
- lines.push(` }) => Promise<void>`)
301
- lines.push(` afterOperation?: (args: {`)
302
- lines.push(` operation: 'create' | 'update' | 'delete'`)
303
- lines.push(` resolvedData?: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
304
- lines.push(` item?: ${listName}`)
305
- lines.push(` context: import('@opensaas/stack-core').AccessContext`)
306
- lines.push(` }) => Promise<void>`)
307
- lines.push(`}`)
308
-
309
- return lines.join('\n')
310
- }
311
-
312
- /**
313
- * Generate custom DB interface that uses Prisma's conditional types with virtual and transformed fields
314
- * This leverages Prisma's GetPayload utility to get correct types based on select/include
315
- */
316
- function generateCustomDBType(config: OpenSaasConfig): string {
317
- const lines: string[] = []
318
-
319
- // Generate list of db keys to omit from AccessControlledDB
320
- const dbKeys = Object.keys(config.lists).map((listName) => {
321
- const dbKey = listName.charAt(0).toLowerCase() + listName.slice(1)
322
- return `'${dbKey}'`
323
- })
324
-
325
- lines.push('/**')
326
- lines.push(
327
- " * Custom DB type that uses Prisma's conditional types with virtual and transformed field support",
328
- )
329
- lines.push(
330
- ' * Types change based on select/include - relationships only present when explicitly included',
331
- )
332
- lines.push(' * Virtual fields and transformed fields are added to the base model type')
333
- lines.push(' */')
334
- lines.push('export type CustomDB = Omit<AccessControlledDB<PrismaClient>, ')
335
- lines.push(` ${dbKeys.join(' | ')}`)
336
- lines.push('> & {')
337
-
338
- // For each list, create strongly-typed methods using Prisma's conditional types
339
- for (const listName of Object.keys(config.lists)) {
340
- const dbKey = listName.charAt(0).toLowerCase() + listName.slice(1) // camelCase
341
-
342
- lines.push(` ${dbKey}: {`)
343
-
344
- // findUnique - generic to preserve Prisma's conditional return type
345
- lines.push(` findUnique: <T extends Prisma.${listName}FindUniqueArgs>(`)
346
- lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}FindUniqueArgs>`)
347
- lines.push(
348
- ` ) => Promise<(Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields) | null>`,
349
- )
350
-
351
- // findMany - generic to preserve Prisma's conditional return type
352
- lines.push(` findMany: <T extends Prisma.${listName}FindManyArgs>(`)
353
- lines.push(` args?: Prisma.SelectSubset<T, Prisma.${listName}FindManyArgs>`)
354
- lines.push(
355
- ` ) => Promise<Array<Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields>>`,
356
- )
357
-
358
- // create - generic to preserve Prisma's conditional return type
359
- lines.push(` create: <T extends Prisma.${listName}CreateArgs>(`)
360
- lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}CreateArgs>`)
361
- lines.push(
362
- ` ) => Promise<Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields>`,
363
- )
364
-
365
- // update - generic to preserve Prisma's conditional return type
366
- lines.push(` update: <T extends Prisma.${listName}UpdateArgs>(`)
367
- lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}UpdateArgs>`)
368
- lines.push(
369
- ` ) => Promise<(Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields) | null>`,
370
- )
371
-
372
- // delete - generic to preserve Prisma's conditional return type
373
- lines.push(` delete: <T extends Prisma.${listName}DeleteArgs>(`)
374
- lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}DeleteArgs>`)
375
- lines.push(
376
- ` ) => Promise<(Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields) | null>`,
377
- )
378
-
379
- // count - no changes to return type
380
- lines.push(` count: (args?: Prisma.${listName}CountArgs) => Promise<number>`)
381
-
382
- lines.push(` }`)
383
- }
384
-
385
- lines.push('}')
386
-
387
- return lines.join('\n')
388
- }
389
-
390
- /**
391
- * Generate Context type that is compatible with AccessContext
392
- */
393
- function generateContextType(_config: OpenSaasConfig): string {
394
- const lines: string[] = []
395
-
396
- lines.push('/**')
397
- lines.push(
398
- ' * Context type compatible with AccessContext but with CustomDB for virtual field typing',
399
- )
400
- lines.push(
401
- ' * Extends AccessContext and overrides db property to include virtual fields in output types',
402
- )
403
- lines.push(' */')
404
- lines.push(
405
- "export type Context<TSession extends OpensaasSession = OpensaasSession> = Omit<AccessContext<PrismaClient>, 'db' | 'session'> & {",
406
- )
407
- lines.push(' db: CustomDB')
408
- lines.push(' session: TSession')
409
- lines.push(' serverAction: (props: ServerActionProps) => Promise<unknown>')
410
- lines.push(' sudo: () => Context<TSession>')
411
- lines.push('}')
412
-
413
- return lines.join('\n')
414
- }
415
-
416
- /**
417
- * Collect TypeScript imports from field configurations
418
- */
419
- function collectFieldImports(config: OpenSaasConfig): Array<{
420
- names: string[]
421
- from: string
422
- typeOnly: boolean
423
- }> {
424
- const importsMap = new Map<string, { names: Set<string>; typeOnly: boolean }>()
425
-
426
- // Iterate through all lists and fields
427
- for (const listConfig of Object.values(config.lists)) {
428
- for (const fieldConfig of Object.values(listConfig.fields)) {
429
- // Check if field provides imports
430
- if (fieldConfig.getTypeScriptImports) {
431
- const imports = fieldConfig.getTypeScriptImports()
432
- for (const imp of imports) {
433
- const existing = importsMap.get(imp.from)
434
- if (existing) {
435
- // Merge names into existing import
436
- imp.names.forEach((name) => existing.names.add(name))
437
- // If either import is not type-only, make the merged import not type-only
438
- if (imp.typeOnly === false) {
439
- existing.typeOnly = false
440
- }
441
- } else {
442
- // Add new import
443
- importsMap.set(imp.from, {
444
- names: new Set(imp.names),
445
- typeOnly: imp.typeOnly ?? true,
446
- })
447
- }
448
- }
449
- }
450
- }
451
- }
452
-
453
- // Convert map to array
454
- return Array.from(importsMap.entries()).map(([from, { names, typeOnly }]) => ({
455
- names: Array.from(names).sort(),
456
- from,
457
- typeOnly,
458
- }))
459
- }
460
-
461
- /**
462
- * Generate all TypeScript types from config
463
- */
464
- export function generateTypes(config: OpenSaasConfig): string {
465
- const lines: string[] = []
466
-
467
- // Add header comment
468
- lines.push('/**')
469
- lines.push(' * Generated types from OpenSaas configuration')
470
- lines.push(' * DO NOT EDIT - This file is automatically generated')
471
- lines.push(' */')
472
- lines.push('')
473
-
474
- // Add necessary imports
475
- // Use alias for Session to avoid conflicts if user has a list named "Session"
476
- lines.push(
477
- "import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB, AccessContext } from '@opensaas/stack-core'",
478
- )
479
- lines.push("import type { PrismaClient, Prisma } from './prisma-client/client'")
480
- lines.push("import type { PluginServices } from './plugin-types'")
481
-
482
- // Add field-specific imports
483
- const fieldImports = collectFieldImports(config)
484
- for (const imp of fieldImports) {
485
- const typePrefix = imp.typeOnly ? 'type ' : ''
486
- const names = imp.names.join(', ')
487
- lines.push(`import ${typePrefix}{ ${names} } from '${imp.from}'`)
488
- }
489
-
490
- lines.push('')
491
-
492
- // Generate types for each list
493
- for (const [listName, listConfig] of Object.entries(config.lists)) {
494
- // Generate VirtualFields type first (needed by Output type and CustomDB)
495
- lines.push(generateVirtualFieldsType(listName, listConfig.fields))
496
- lines.push('')
497
- // Generate TransformedFields type (needed by CustomDB)
498
- lines.push(generateTransformedFieldsType(listName, listConfig.fields))
499
- lines.push('')
500
- lines.push(generateModelOutputType(listName, listConfig.fields))
501
- lines.push('')
502
- lines.push(generateModelTypeAlias(listName))
503
- lines.push('')
504
- lines.push(generateCreateInputType(listName, listConfig.fields))
505
- lines.push('')
506
- lines.push(generateUpdateInputType(listName, listConfig.fields))
507
- lines.push('')
508
- lines.push(generateWhereInputType(listName, listConfig.fields))
509
- lines.push('')
510
- lines.push(generateHookTypes(listName))
511
- lines.push('')
512
- }
513
-
514
- // Generate CustomDB interface
515
- lines.push(generateCustomDBType(config))
516
- lines.push('')
517
-
518
- // Generate Context type
519
- lines.push(generateContextType(config))
520
-
521
- return lines.join('\n')
522
- }
523
-
524
- /**
525
- * Write TypeScript types to file
526
- */
527
- export function writeTypes(config: OpenSaasConfig, outputPath: string): void {
528
- const types = generateTypes(config)
529
-
530
- // Ensure directory exists
531
- const dir = path.dirname(outputPath)
532
- if (!fs.existsSync(dir)) {
533
- fs.mkdirSync(dir, { recursive: true })
534
- }
535
-
536
- fs.writeFileSync(outputPath, types, 'utf-8')
537
- }
package/src/index.ts DELETED
@@ -1,42 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander'
4
- import { generateCommand } from './commands/generate.js'
5
- import { initCommand } from './commands/init.js'
6
- import { devCommand } from './commands/dev.js'
7
- import { createMCPCommand } from './commands/mcp.js'
8
-
9
- const program = new Command()
10
-
11
- program.name('opensaas').description('OpenSaas Stack CLI').version('0.1.0')
12
-
13
- program
14
- .command('generate')
15
- .description('Generate Prisma schema and TypeScript types from opensaas.config.ts')
16
- .action(async () => {
17
- await generateCommand()
18
- })
19
-
20
- program
21
- .command('init [project-name]')
22
- .description('Create a new OpenSaas project (delegates to create-opensaas-app)')
23
- .option('--with-auth', 'Include authentication (Better-auth)')
24
- .allowUnknownOption() // Allow passing through other options
25
- .action(async (projectName, options) => {
26
- const args = []
27
- if (projectName) args.push(projectName)
28
- if (options.withAuth) args.push('--with-auth')
29
- await initCommand(args)
30
- })
31
-
32
- program
33
- .command('dev')
34
- .description('Watch opensaas.config.ts and regenerate on changes')
35
- .action(async () => {
36
- await devCommand()
37
- })
38
-
39
- // Add MCP command group
40
- program.addCommand(createMCPCommand())
41
-
42
- program.parse()