@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,675 +0,0 @@
1
- /**
2
- * Migration Config Generator
3
- *
4
- * Generates opensaas.config.ts from migration session data.
5
- */
6
-
7
- import type {
8
- MigrationSession,
9
- MigrationOutput,
10
- IntrospectedSchema,
11
- IntrospectedModel,
12
- IntrospectedField,
13
- } from '../types.js'
14
- import { PrismaIntrospector } from '../introspectors/prisma-introspector.js'
15
- import { KeystoneIntrospector } from '../introspectors/keystone-introspector.js'
16
-
17
- export class MigrationGenerator {
18
- private prismaIntrospector: PrismaIntrospector
19
- private keystoneIntrospector: KeystoneIntrospector
20
-
21
- constructor() {
22
- this.prismaIntrospector = new PrismaIntrospector()
23
- this.keystoneIntrospector = new KeystoneIntrospector()
24
- }
25
-
26
- /**
27
- * Generate migration output from session
28
- */
29
- async generate(session: MigrationSession): Promise<MigrationOutput> {
30
- const { projectType, analysis, answers } = session
31
-
32
- // Get full schema if available
33
- let schema: IntrospectedSchema | undefined
34
- try {
35
- if (projectType === 'prisma') {
36
- schema = await this.prismaIntrospector.introspect(analysis.cwd)
37
- } else if (projectType === 'keystone') {
38
- schema = await this.keystoneIntrospector.introspect(analysis.cwd)
39
- }
40
- } catch {
41
- // Continue without schema - will generate example config
42
- }
43
-
44
- // Collect used field types for imports
45
- const usedFieldTypes = new Set<string>(['text']) // Always need text
46
- const warnings: string[] = []
47
-
48
- // Generate lists
49
- const lists = this.generateLists(schema, answers, usedFieldTypes, warnings)
50
-
51
- // Generate access control helpers
52
- const accessHelpers = this.generateAccessHelpers(answers)
53
-
54
- // Generate database config
55
- const dbConfig = this.generateDatabaseConfig(
56
- (answers.db_provider as string) || analysis.provider || 'sqlite',
57
- )
58
-
59
- // Determine if using auth
60
- const useAuth = answers.enable_auth === true
61
-
62
- // Generate imports
63
- const imports = this.generateImports(usedFieldTypes, useAuth, dbConfig.provider)
64
-
65
- // Generate the full config
66
- const configContent = this.assembleConfig({
67
- imports,
68
- accessHelpers,
69
- dbConfig,
70
- lists,
71
- useAuth,
72
- authMethods: (answers.auth_methods as string[]) || ['email-password'],
73
- adminBasePath: (answers.admin_base_path as string) || '/admin',
74
- })
75
-
76
- // Generate dependencies list
77
- const dependencies = this.generateDependencies(dbConfig.provider, useAuth)
78
-
79
- // Generate additional files
80
- const files = this.generateAdditionalFiles(answers, dbConfig.provider)
81
-
82
- // Generate next steps
83
- const steps = this.generateSteps(useAuth, dbConfig.provider)
84
-
85
- // Add warnings from introspection
86
- if (schema) {
87
- const introspectorWarnings =
88
- projectType === 'prisma'
89
- ? this.prismaIntrospector.getWarnings(schema)
90
- : this.keystoneIntrospector.getWarnings(schema)
91
- warnings.push(...introspectorWarnings)
92
- }
93
-
94
- return {
95
- configContent,
96
- dependencies,
97
- files,
98
- steps,
99
- warnings,
100
- }
101
- }
102
-
103
- /**
104
- * Generate list definitions from schema
105
- */
106
- private generateLists(
107
- schema: IntrospectedSchema | undefined,
108
- answers: Record<string, unknown>,
109
- usedFieldTypes: Set<string>,
110
- warnings: string[],
111
- ): string {
112
- if (!schema || schema.models.length === 0) {
113
- // No schema, generate example lists
114
- usedFieldTypes.add('timestamp')
115
- return ` // Add your lists here
116
- // Example:
117
- // Post: list({
118
- // fields: {
119
- // title: text({ validation: { isRequired: true } }),
120
- // content: text(),
121
- // createdAt: timestamp({ defaultValue: { kind: 'now' } }),
122
- // },
123
- // }),`
124
- }
125
-
126
- // Filter out auth models if using auth plugin
127
- const skipAuthModels = answers.skip_auth_models === true
128
- const authModelNames = ['User', 'Account', 'Session', 'Verification']
129
-
130
- const modelsToGenerate = schema.models.filter((model) => {
131
- if (skipAuthModels && authModelNames.includes(model.name)) {
132
- return false
133
- }
134
- return true
135
- })
136
-
137
- // Get models that should have owner access
138
- const ownerModels = new Set((answers.models_with_owner as string[]) || [])
139
-
140
- // Generate each list
141
- const listDefinitions = modelsToGenerate.map((model) => {
142
- return this.generateList(
143
- model,
144
- schema,
145
- ownerModels.has(model.name),
146
- answers,
147
- usedFieldTypes,
148
- warnings,
149
- )
150
- })
151
-
152
- return listDefinitions.join('\n')
153
- }
154
-
155
- /**
156
- * Generate a single list definition
157
- */
158
- private generateList(
159
- model: IntrospectedModel,
160
- schema: IntrospectedSchema,
161
- hasOwnerAccess: boolean,
162
- answers: Record<string, unknown>,
163
- usedFieldTypes: Set<string>,
164
- warnings: string[],
165
- ): string {
166
- const fields: string[] = []
167
-
168
- // Skip system fields (id, createdAt, updatedAt) - OpenSaaS adds these automatically
169
- const systemFields = ['id', 'createdAt', 'updatedAt']
170
-
171
- for (const field of model.fields) {
172
- if (systemFields.includes(field.name)) continue
173
- if (field.isId) continue // Skip ID fields
174
-
175
- const fieldDef = this.generateField(field, schema, usedFieldTypes, warnings)
176
- if (fieldDef) {
177
- fields.push(` ${field.name}: ${fieldDef},`)
178
- }
179
- }
180
-
181
- // Generate access control
182
- const access = this.generateListAccess(hasOwnerAccess, model, answers)
183
-
184
- const fieldsBlock = fields.length > 0 ? `\n${fields.join('\n')}\n ` : ''
185
-
186
- return ` ${model.name}: list({
187
- fields: {${fieldsBlock}},${access}
188
- }),`
189
- }
190
-
191
- /**
192
- * Generate a field definition
193
- */
194
- private generateField(
195
- field: IntrospectedField,
196
- schema: IntrospectedSchema,
197
- usedFieldTypes: Set<string>,
198
- warnings: string[],
199
- ): string | null {
200
- // Handle relationships
201
- if (field.relation) {
202
- usedFieldTypes.add('relationship')
203
-
204
- // Find the related model and back-reference field
205
- const relatedModel = schema.models.find((m) => m.name === field.relation!.model)
206
- const backRef = relatedModel?.fields.find(
207
- (f) => f.relation && f.relation.model === field.type,
208
- )
209
-
210
- const ref = backRef ? `${field.relation.model}.${backRef.name}` : field.relation.model
211
-
212
- const many = field.isList ? ', many: true' : ''
213
- return `relationship({ ref: '${ref}'${many} })`
214
- }
215
-
216
- // Handle enums as select fields
217
- const enumDef = schema.enums.find((e) => e.name === field.type)
218
- if (enumDef) {
219
- usedFieldTypes.add('select')
220
- const enumOptions = enumDef.values.map((v) => `{ label: '${v}', value: '${v}' }`).join(', ')
221
-
222
- let selectOptions = `options: [${enumOptions}]`
223
- if (field.defaultValue) {
224
- const defaultVal = field.defaultValue.replace(/^["']|["']$/g, '')
225
- selectOptions += `, defaultValue: '${defaultVal}'`
226
- }
227
- return `select({ ${selectOptions} })`
228
- }
229
-
230
- // Map Prisma/Keystone types to OpenSaaS
231
- const mapping = this.prismaIntrospector.mapPrismaTypeToOpenSaas(field.type)
232
- usedFieldTypes.add(mapping.import)
233
-
234
- // Build options
235
- const options: string[] = []
236
-
237
- if (field.isRequired && !field.defaultValue) {
238
- options.push('validation: { isRequired: true }')
239
- }
240
-
241
- if (field.isUnique) {
242
- options.push("isIndexed: 'unique'")
243
- }
244
-
245
- // Handle default values
246
- if (field.defaultValue) {
247
- if (field.type === 'DateTime' && field.defaultValue === 'now()') {
248
- options.push("defaultValue: { kind: 'now' }")
249
- } else if (field.type === 'Boolean') {
250
- options.push(`defaultValue: ${field.defaultValue}`)
251
- }
252
- // Other defaults are harder to map automatically
253
- }
254
-
255
- // Generate unsupported type warning
256
- if (['BigInt', 'Decimal', 'Bytes'].includes(field.type)) {
257
- warnings.push(
258
- `Field "${field.name}" uses unsupported type "${field.type}" - mapped to text()`,
259
- )
260
- }
261
-
262
- const optionsStr = options.length > 0 ? `{ ${options.join(', ')} }` : ''
263
- return `${mapping.type}(${optionsStr})`
264
- }
265
-
266
- /**
267
- * Generate list access control
268
- */
269
- private generateListAccess(
270
- hasOwnerAccess: boolean,
271
- model: IntrospectedModel,
272
- answers: Record<string, unknown>,
273
- ): string {
274
- const defaultAccess = (answers.default_access as string) || 'public-read-auth-write'
275
-
276
- if (hasOwnerAccess) {
277
- // Find the user relationship field
278
- const userField = model.fields.find(
279
- (f) =>
280
- f.relation?.model === 'User' ||
281
- f.name.toLowerCase().includes('author') ||
282
- f.name.toLowerCase().includes('owner') ||
283
- f.name.toLowerCase().includes('user'),
284
- )
285
-
286
- const ownerField = userField?.name || 'author'
287
- const ownerIdField = ownerField.endsWith('Id') ? ownerField : `${ownerField}Id`
288
-
289
- return `
290
- access: {
291
- operation: {
292
- query: () => true,
293
- create: ({ session }) => !!session,
294
- update: isOwner,
295
- delete: isOwner,
296
- },
297
- filter: {
298
- // Optionally filter queries to only show user's own items
299
- // query: ({ session }) => session ? { ${ownerIdField}: { equals: session.userId } } : true,
300
- },
301
- },`
302
- }
303
-
304
- // Generate based on default access pattern
305
- switch (defaultAccess) {
306
- case 'authenticated-only':
307
- return `
308
- access: {
309
- operation: {
310
- query: ({ session }) => !!session,
311
- create: ({ session }) => !!session,
312
- update: ({ session }) => !!session,
313
- delete: ({ session }) => !!session,
314
- },
315
- },`
316
-
317
- case 'owner-only':
318
- return `
319
- access: {
320
- operation: {
321
- query: ({ session }) => !!session,
322
- create: ({ session }) => !!session,
323
- update: ({ session }) => !!session,
324
- delete: ({ session }) => !!session,
325
- },
326
- // Add filter to scope to user's own items:
327
- // filter: { query: ({ session }) => ({ userId: { equals: session?.userId } }) },
328
- },`
329
-
330
- case 'admin-only':
331
- return `
332
- access: {
333
- operation: {
334
- query: ({ session }) => session?.role === 'admin',
335
- create: ({ session }) => session?.role === 'admin',
336
- update: ({ session }) => session?.role === 'admin',
337
- delete: ({ session }) => session?.role === 'admin',
338
- },
339
- },`
340
-
341
- case 'public-read-auth-write':
342
- default:
343
- return `
344
- access: {
345
- operation: {
346
- query: () => true,
347
- create: ({ session }) => !!session,
348
- update: ({ session }) => !!session,
349
- delete: ({ session }) => !!session,
350
- },
351
- },`
352
- }
353
- }
354
-
355
- /**
356
- * Generate access control helper functions
357
- */
358
- private generateAccessHelpers(answers: Record<string, unknown>): string {
359
- const helpers: string[] = []
360
- const ownerModels = (answers.models_with_owner as string[]) || []
361
-
362
- if (ownerModels.length > 0) {
363
- helpers.push(`/**
364
- * Access control helpers
365
- */
366
-
367
- // Check if user owns the item (based on authorId or userId)
368
- const isOwner: AccessControl = ({ session, item }) => {
369
- if (!session) return false
370
- // Try authorId first, then userId
371
- const ownerId = (item as any)?.authorId || (item as any)?.userId
372
- return ownerId === session.userId
373
- }
374
- `)
375
- }
376
-
377
- return helpers.join('\n')
378
- }
379
-
380
- /**
381
- * Generate database configuration
382
- */
383
- private generateDatabaseConfig(provider: string): {
384
- provider: string
385
- configCode: string
386
- imports: string[]
387
- } {
388
- switch (provider) {
389
- case 'postgresql':
390
- return {
391
- provider: 'postgresql',
392
- imports: ["import { PrismaPg } from '@prisma/adapter-pg'", "import pg from 'pg'"],
393
- configCode: ` db: {
394
- provider: 'postgresql',
395
- prismaClientConstructor: (PrismaClient) => {
396
- const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
397
- const adapter = new PrismaPg(pool)
398
- return new PrismaClient({ adapter })
399
- },
400
- },`,
401
- }
402
-
403
- case 'mysql':
404
- return {
405
- provider: 'mysql',
406
- imports: ["import { PrismaPlanetScale } from '@prisma/adapter-planetscale'"],
407
- configCode: ` db: {
408
- provider: 'mysql',
409
- prismaClientConstructor: (PrismaClient) => {
410
- const adapter = new PrismaPlanetScale({
411
- url: process.env.DATABASE_URL!,
412
- })
413
- return new PrismaClient({ adapter })
414
- },
415
- },`,
416
- }
417
-
418
- case 'sqlite':
419
- default:
420
- return {
421
- provider: 'sqlite',
422
- imports: ["import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'"],
423
- configCode: ` db: {
424
- provider: 'sqlite',
425
- prismaClientConstructor: (PrismaClient) => {
426
- const adapter = new PrismaBetterSqlite3({
427
- url: process.env.DATABASE_URL || 'file:./dev.db',
428
- })
429
- return new PrismaClient({ adapter })
430
- },
431
- },`,
432
- }
433
- }
434
- }
435
-
436
- /**
437
- * Generate import statements
438
- */
439
- private generateImports(
440
- usedFieldTypes: Set<string>,
441
- useAuth: boolean,
442
- dbProvider: string,
443
- ): string {
444
- const imports: string[] = []
445
-
446
- // Core imports
447
- imports.push("import { config, list } from '@opensaas/stack-core'")
448
-
449
- // Field imports
450
- const fieldTypes = Array.from(usedFieldTypes).sort()
451
- imports.push(`import { ${fieldTypes.join(', ')} } from '@opensaas/stack-core/fields'`)
452
-
453
- // Auth imports
454
- if (useAuth) {
455
- imports.push("import { authPlugin } from '@opensaas/stack-auth'")
456
- imports.push("import type { AccessControl } from '@opensaas/stack-core'")
457
- }
458
-
459
- // Database adapter imports
460
- const dbConfig = this.generateDatabaseConfig(dbProvider)
461
- imports.push(...dbConfig.imports)
462
-
463
- return imports.join('\n')
464
- }
465
-
466
- /**
467
- * Assemble the complete config file
468
- */
469
- private assembleConfig(options: {
470
- imports: string
471
- accessHelpers: string
472
- dbConfig: { configCode: string }
473
- lists: string
474
- useAuth: boolean
475
- authMethods: string[]
476
- adminBasePath: string
477
- }): string {
478
- const { imports, accessHelpers, dbConfig, lists, useAuth, authMethods, adminBasePath } = options
479
-
480
- // Generate auth plugin config
481
- let authPluginStr = ''
482
- if (useAuth) {
483
- const authOptions: string[] = []
484
-
485
- if (authMethods.includes('email-password')) {
486
- authOptions.push(` emailAndPassword: {
487
- enabled: true,
488
- minPasswordLength: 8,
489
- },`)
490
- }
491
-
492
- if (authMethods.includes('magic-link')) {
493
- authOptions.push(` magicLink: {
494
- enabled: true,
495
- },`)
496
- }
497
-
498
- // OAuth providers would need additional setup
499
- if (authMethods.includes('google')) {
500
- authOptions.push(` // Uncomment and configure Google OAuth:
501
- // google: {
502
- // clientId: process.env.GOOGLE_CLIENT_ID!,
503
- // clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
504
- // },`)
505
- }
506
-
507
- if (authMethods.includes('github')) {
508
- authOptions.push(` // Uncomment and configure GitHub OAuth:
509
- // github: {
510
- // clientId: process.env.GITHUB_CLIENT_ID!,
511
- // clientSecret: process.env.GITHUB_CLIENT_SECRET!,
512
- // },`)
513
- }
514
-
515
- authOptions.push(` sessionFields: ['userId', 'email', 'name'],`)
516
-
517
- authPluginStr = ` authPlugin({
518
- ${authOptions.join('\n')}
519
- }),`
520
- }
521
-
522
- // Build config body
523
- const pluginsBlock = useAuth
524
- ? ` plugins: [
525
- ${authPluginStr}
526
- ],
527
-
528
- `
529
- : ''
530
-
531
- const configBody = `export default config({
532
- ${pluginsBlock}${dbConfig.configCode}
533
- lists: {
534
- ${lists}
535
- },
536
- ui: {
537
- basePath: '${adminBasePath}',
538
- },
539
- })`
540
-
541
- return `${imports}
542
-
543
- ${accessHelpers}${configBody}
544
- `
545
- }
546
-
547
- /**
548
- * Generate list of dependencies to install
549
- */
550
- private generateDependencies(dbProvider: string, useAuth: boolean): string[] {
551
- const deps: string[] = [
552
- '@opensaas/stack-core',
553
- '@opensaas/stack-ui',
554
- '@prisma/client',
555
- 'prisma',
556
- ]
557
-
558
- // Database adapter deps
559
- switch (dbProvider) {
560
- case 'postgresql':
561
- deps.push('@prisma/adapter-pg', 'pg', '@types/pg')
562
- break
563
- case 'mysql':
564
- deps.push('@prisma/adapter-planetscale')
565
- break
566
- case 'sqlite':
567
- default:
568
- deps.push('@prisma/adapter-better-sqlite3')
569
- break
570
- }
571
-
572
- // Auth deps
573
- if (useAuth) {
574
- deps.push('@opensaas/stack-auth', 'better-auth')
575
- }
576
-
577
- return deps
578
- }
579
-
580
- /**
581
- * Generate additional files if needed
582
- */
583
- private generateAdditionalFiles(
584
- answers: Record<string, unknown>,
585
- dbProvider: string,
586
- ): Array<{
587
- path: string
588
- content: string
589
- language: string
590
- description: string
591
- }> {
592
- const files: Array<{
593
- path: string
594
- content: string
595
- language: string
596
- description: string
597
- }> = []
598
-
599
- // Generate .env.example
600
- const envVars: string[] = ['# Database']
601
-
602
- // Database URL based on provider
603
- switch (dbProvider) {
604
- case 'postgresql':
605
- envVars.push('DATABASE_URL="postgresql://user:password@localhost:5432/mydb"')
606
- break
607
- case 'mysql':
608
- envVars.push('DATABASE_URL="mysql://user:password@localhost:3306/mydb"')
609
- break
610
- case 'sqlite':
611
- default:
612
- envVars.push('DATABASE_URL="file:./dev.db"')
613
- break
614
- }
615
-
616
- envVars.push('')
617
-
618
- if (answers.enable_auth) {
619
- envVars.push('# Auth')
620
- envVars.push('BETTER_AUTH_SECRET="generate-with-openssl-rand-base64-32"')
621
- envVars.push('BETTER_AUTH_URL="http://localhost:3000"')
622
- envVars.push('')
623
-
624
- const authMethods = (answers.auth_methods as string[]) || []
625
- if (authMethods.includes('google')) {
626
- envVars.push('# Google OAuth (optional)')
627
- envVars.push('GOOGLE_CLIENT_ID=""')
628
- envVars.push('GOOGLE_CLIENT_SECRET=""')
629
- envVars.push('')
630
- }
631
- if (authMethods.includes('github')) {
632
- envVars.push('# GitHub OAuth (optional)')
633
- envVars.push('GITHUB_CLIENT_ID=""')
634
- envVars.push('GITHUB_CLIENT_SECRET=""')
635
- envVars.push('')
636
- }
637
- }
638
-
639
- files.push({
640
- path: '.env.example',
641
- content: envVars.join('\n'),
642
- language: 'bash',
643
- description: 'Environment variables template',
644
- })
645
-
646
- return files
647
- }
648
-
649
- /**
650
- * Generate next steps
651
- */
652
- private generateSteps(useAuth: boolean, dbProvider: string): string[] {
653
- const steps = [
654
- 'Save the generated config to `opensaas.config.ts`',
655
- 'Copy `.env.example` to `.env` and fill in values',
656
- ]
657
-
658
- if (useAuth) {
659
- steps.push('Generate BETTER_AUTH_SECRET: `openssl rand -base64 32`')
660
- }
661
-
662
- steps.push(
663
- 'Install dependencies: `pnpm add <dependencies>`',
664
- 'Generate Prisma schema: `pnpm opensaas generate`',
665
- 'Generate Prisma client: `npx prisma generate`',
666
- 'Push schema to database: `npx prisma db push`',
667
- 'Start development server: `pnpm dev`',
668
- )
669
-
670
- const adminPath = useAuth ? '' : '/admin'
671
- steps.push(`Visit admin UI at http://localhost:3000${adminPath}`)
672
-
673
- return steps
674
- }
675
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * Schema introspectors for migration system
3
- *
4
- * These introspectors parse existing schemas and configs to extract
5
- * model and field information for automated migration.
6
- */
7
-
8
- export { PrismaIntrospector } from './prisma-introspector.js'
9
- export { KeystoneIntrospector } from './keystone-introspector.js'
10
- export type { KeystoneList, KeystoneField, KeystoneSchema } from './keystone-introspector.js'
11
- export { NextjsIntrospector } from './nextjs-introspector.js'
12
- export type { NextjsAnalysis } from './nextjs-introspector.js'