@opensaas/stack-cli 0.5.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 (61) hide show
  1. package/README.md +76 -0
  2. package/dist/commands/migrate.d.ts.map +1 -1
  3. package/dist/commands/migrate.js +91 -265
  4. package/dist/commands/migrate.js.map +1 -1
  5. package/package.json +7 -2
  6. package/plugin/.claude-plugin/plugin.json +15 -0
  7. package/plugin/README.md +112 -0
  8. package/plugin/agents/migration-assistant.md +150 -0
  9. package/plugin/commands/analyze-schema.md +34 -0
  10. package/plugin/commands/generate-config.md +33 -0
  11. package/plugin/commands/validate-migration.md +34 -0
  12. package/plugin/skills/opensaas-migration/SKILL.md +192 -0
  13. package/.turbo/turbo-build.log +0 -4
  14. package/CHANGELOG.md +0 -462
  15. package/CLAUDE.md +0 -298
  16. package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
  17. package/src/commands/dev.test.ts +0 -215
  18. package/src/commands/dev.ts +0 -48
  19. package/src/commands/generate.test.ts +0 -282
  20. package/src/commands/generate.ts +0 -182
  21. package/src/commands/init.ts +0 -34
  22. package/src/commands/mcp.ts +0 -135
  23. package/src/commands/migrate.ts +0 -534
  24. package/src/generator/__snapshots__/context.test.ts.snap +0 -361
  25. package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
  26. package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
  27. package/src/generator/context.test.ts +0 -139
  28. package/src/generator/context.ts +0 -227
  29. package/src/generator/index.ts +0 -7
  30. package/src/generator/lists.test.ts +0 -335
  31. package/src/generator/lists.ts +0 -140
  32. package/src/generator/plugin-types.ts +0 -147
  33. package/src/generator/prisma-config.ts +0 -46
  34. package/src/generator/prisma-extensions.ts +0 -159
  35. package/src/generator/prisma.test.ts +0 -211
  36. package/src/generator/prisma.ts +0 -161
  37. package/src/generator/types.test.ts +0 -268
  38. package/src/generator/types.ts +0 -537
  39. package/src/index.ts +0 -46
  40. package/src/mcp/lib/documentation-provider.ts +0 -710
  41. package/src/mcp/lib/features/catalog.ts +0 -301
  42. package/src/mcp/lib/generators/feature-generator.ts +0 -598
  43. package/src/mcp/lib/types.ts +0 -89
  44. package/src/mcp/lib/wizards/migration-wizard.ts +0 -584
  45. package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
  46. package/src/mcp/server/index.ts +0 -361
  47. package/src/mcp/server/stack-mcp-server.ts +0 -544
  48. package/src/migration/generators/migration-generator.ts +0 -675
  49. package/src/migration/introspectors/index.ts +0 -12
  50. package/src/migration/introspectors/keystone-introspector.ts +0 -296
  51. package/src/migration/introspectors/nextjs-introspector.ts +0 -209
  52. package/src/migration/introspectors/prisma-introspector.ts +0 -233
  53. package/src/migration/types.ts +0 -92
  54. package/tests/introspectors/keystone-introspector.test.ts +0 -255
  55. package/tests/introspectors/nextjs-introspector.test.ts +0 -302
  56. package/tests/introspectors/prisma-introspector.test.ts +0 -268
  57. package/tests/migration-generator.test.ts +0 -592
  58. package/tests/migration-wizard.test.ts +0 -442
  59. package/tsconfig.json +0 -13
  60. package/tsconfig.tsbuildinfo +0 -1
  61. 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,46 +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
- import { createMigrateCommand } from './commands/migrate.js'
9
-
10
- const program = new Command()
11
-
12
- program.name('opensaas').description('OpenSaas Stack CLI').version('0.1.0')
13
-
14
- program
15
- .command('generate')
16
- .description('Generate Prisma schema and TypeScript types from opensaas.config.ts')
17
- .action(async () => {
18
- await generateCommand()
19
- })
20
-
21
- program
22
- .command('init [project-name]')
23
- .description('Create a new OpenSaas project (delegates to create-opensaas-app)')
24
- .option('--with-auth', 'Include authentication (Better-auth)')
25
- .allowUnknownOption() // Allow passing through other options
26
- .action(async (projectName, options) => {
27
- const args = []
28
- if (projectName) args.push(projectName)
29
- if (options.withAuth) args.push('--with-auth')
30
- await initCommand(args)
31
- })
32
-
33
- program
34
- .command('dev')
35
- .description('Watch opensaas.config.ts and regenerate on changes')
36
- .action(async () => {
37
- await devCommand()
38
- })
39
-
40
- // Add MCP command group
41
- program.addCommand(createMCPCommand())
42
-
43
- // Add migrate command
44
- program.addCommand(createMigrateCommand())
45
-
46
- program.parse()