@opensaas/stack-cli 0.5.0 → 0.6.1

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 (54) hide show
  1. package/README.md +76 -0
  2. package/dist/commands/migrate.d.ts.map +1 -1
  3. package/dist/commands/migrate.js +94 -268
  4. package/dist/commands/migrate.js.map +1 -1
  5. package/package.json +7 -2
  6. package/.turbo/turbo-build.log +0 -4
  7. package/CHANGELOG.md +0 -462
  8. package/CLAUDE.md +0 -298
  9. package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
  10. package/src/commands/dev.test.ts +0 -215
  11. package/src/commands/dev.ts +0 -48
  12. package/src/commands/generate.test.ts +0 -282
  13. package/src/commands/generate.ts +0 -182
  14. package/src/commands/init.ts +0 -34
  15. package/src/commands/mcp.ts +0 -135
  16. package/src/commands/migrate.ts +0 -534
  17. package/src/generator/__snapshots__/context.test.ts.snap +0 -361
  18. package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
  19. package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
  20. package/src/generator/context.test.ts +0 -139
  21. package/src/generator/context.ts +0 -227
  22. package/src/generator/index.ts +0 -7
  23. package/src/generator/lists.test.ts +0 -335
  24. package/src/generator/lists.ts +0 -140
  25. package/src/generator/plugin-types.ts +0 -147
  26. package/src/generator/prisma-config.ts +0 -46
  27. package/src/generator/prisma-extensions.ts +0 -159
  28. package/src/generator/prisma.test.ts +0 -211
  29. package/src/generator/prisma.ts +0 -161
  30. package/src/generator/types.test.ts +0 -268
  31. package/src/generator/types.ts +0 -537
  32. package/src/index.ts +0 -46
  33. package/src/mcp/lib/documentation-provider.ts +0 -710
  34. package/src/mcp/lib/features/catalog.ts +0 -301
  35. package/src/mcp/lib/generators/feature-generator.ts +0 -598
  36. package/src/mcp/lib/types.ts +0 -89
  37. package/src/mcp/lib/wizards/migration-wizard.ts +0 -584
  38. package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
  39. package/src/mcp/server/index.ts +0 -361
  40. package/src/mcp/server/stack-mcp-server.ts +0 -544
  41. package/src/migration/generators/migration-generator.ts +0 -675
  42. package/src/migration/introspectors/index.ts +0 -12
  43. package/src/migration/introspectors/keystone-introspector.ts +0 -296
  44. package/src/migration/introspectors/nextjs-introspector.ts +0 -209
  45. package/src/migration/introspectors/prisma-introspector.ts +0 -233
  46. package/src/migration/types.ts +0 -92
  47. package/tests/introspectors/keystone-introspector.test.ts +0 -255
  48. package/tests/introspectors/nextjs-introspector.test.ts +0 -302
  49. package/tests/introspectors/prisma-introspector.test.ts +0 -268
  50. package/tests/migration-generator.test.ts +0 -592
  51. package/tests/migration-wizard.test.ts +0 -442
  52. package/tsconfig.json +0 -13
  53. package/tsconfig.tsbuildinfo +0 -1
  54. 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()