@opensaas/stack-cli 0.3.0 → 0.5.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 (105) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +193 -0
  3. package/dist/commands/generate.d.ts.map +1 -1
  4. package/dist/commands/generate.js +4 -13
  5. package/dist/commands/generate.js.map +1 -1
  6. package/dist/commands/migrate.d.ts +9 -0
  7. package/dist/commands/migrate.d.ts.map +1 -0
  8. package/dist/commands/migrate.js +473 -0
  9. package/dist/commands/migrate.js.map +1 -0
  10. package/dist/generator/context.d.ts.map +1 -1
  11. package/dist/generator/context.js +20 -5
  12. package/dist/generator/context.js.map +1 -1
  13. package/dist/generator/index.d.ts +1 -1
  14. package/dist/generator/index.d.ts.map +1 -1
  15. package/dist/generator/index.js +1 -1
  16. package/dist/generator/index.js.map +1 -1
  17. package/dist/generator/lists.d.ts.map +1 -1
  18. package/dist/generator/lists.js +33 -1
  19. package/dist/generator/lists.js.map +1 -1
  20. package/dist/generator/prisma-extensions.d.ts +11 -0
  21. package/dist/generator/prisma-extensions.d.ts.map +1 -0
  22. package/dist/generator/prisma-extensions.js +134 -0
  23. package/dist/generator/prisma-extensions.js.map +1 -0
  24. package/dist/generator/prisma.d.ts.map +1 -1
  25. package/dist/generator/prisma.js +4 -0
  26. package/dist/generator/prisma.js.map +1 -1
  27. package/dist/generator/types.d.ts.map +1 -1
  28. package/dist/generator/types.js +151 -17
  29. package/dist/generator/types.js.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp/lib/documentation-provider.d.ts +23 -0
  33. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -1
  34. package/dist/mcp/lib/documentation-provider.js +471 -0
  35. package/dist/mcp/lib/documentation-provider.js.map +1 -1
  36. package/dist/mcp/lib/wizards/migration-wizard.d.ts +80 -0
  37. package/dist/mcp/lib/wizards/migration-wizard.d.ts.map +1 -0
  38. package/dist/mcp/lib/wizards/migration-wizard.js +499 -0
  39. package/dist/mcp/lib/wizards/migration-wizard.js.map +1 -0
  40. package/dist/mcp/server/index.d.ts.map +1 -1
  41. package/dist/mcp/server/index.js +103 -0
  42. package/dist/mcp/server/index.js.map +1 -1
  43. package/dist/mcp/server/stack-mcp-server.d.ts +85 -0
  44. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -1
  45. package/dist/mcp/server/stack-mcp-server.js +219 -0
  46. package/dist/mcp/server/stack-mcp-server.js.map +1 -1
  47. package/dist/migration/generators/migration-generator.d.ts +60 -0
  48. package/dist/migration/generators/migration-generator.d.ts.map +1 -0
  49. package/dist/migration/generators/migration-generator.js +510 -0
  50. package/dist/migration/generators/migration-generator.js.map +1 -0
  51. package/dist/migration/introspectors/index.d.ts +12 -0
  52. package/dist/migration/introspectors/index.d.ts.map +1 -0
  53. package/dist/migration/introspectors/index.js +10 -0
  54. package/dist/migration/introspectors/index.js.map +1 -0
  55. package/dist/migration/introspectors/keystone-introspector.d.ts +59 -0
  56. package/dist/migration/introspectors/keystone-introspector.d.ts.map +1 -0
  57. package/dist/migration/introspectors/keystone-introspector.js +229 -0
  58. package/dist/migration/introspectors/keystone-introspector.js.map +1 -0
  59. package/dist/migration/introspectors/nextjs-introspector.d.ts +59 -0
  60. package/dist/migration/introspectors/nextjs-introspector.d.ts.map +1 -0
  61. package/dist/migration/introspectors/nextjs-introspector.js +159 -0
  62. package/dist/migration/introspectors/nextjs-introspector.js.map +1 -0
  63. package/dist/migration/introspectors/prisma-introspector.d.ts +45 -0
  64. package/dist/migration/introspectors/prisma-introspector.d.ts.map +1 -0
  65. package/dist/migration/introspectors/prisma-introspector.js +190 -0
  66. package/dist/migration/introspectors/prisma-introspector.js.map +1 -0
  67. package/dist/migration/types.d.ts +86 -0
  68. package/dist/migration/types.d.ts.map +1 -0
  69. package/dist/migration/types.js +5 -0
  70. package/dist/migration/types.js.map +1 -0
  71. package/package.json +12 -9
  72. package/src/commands/__snapshots__/generate.test.ts.snap +92 -21
  73. package/src/commands/generate.ts +8 -19
  74. package/src/commands/migrate.ts +534 -0
  75. package/src/generator/__snapshots__/context.test.ts.snap +60 -15
  76. package/src/generator/__snapshots__/types.test.ts.snap +689 -95
  77. package/src/generator/context.test.ts +3 -1
  78. package/src/generator/context.ts +20 -5
  79. package/src/generator/index.ts +1 -1
  80. package/src/generator/lists.ts +39 -1
  81. package/src/generator/prisma-extensions.ts +159 -0
  82. package/src/generator/prisma.ts +5 -0
  83. package/src/generator/types.ts +204 -17
  84. package/src/index.ts +4 -0
  85. package/src/mcp/lib/documentation-provider.ts +507 -0
  86. package/src/mcp/lib/wizards/migration-wizard.ts +584 -0
  87. package/src/mcp/server/index.ts +121 -0
  88. package/src/mcp/server/stack-mcp-server.ts +243 -0
  89. package/src/migration/generators/migration-generator.ts +675 -0
  90. package/src/migration/introspectors/index.ts +12 -0
  91. package/src/migration/introspectors/keystone-introspector.ts +296 -0
  92. package/src/migration/introspectors/nextjs-introspector.ts +209 -0
  93. package/src/migration/introspectors/prisma-introspector.ts +233 -0
  94. package/src/migration/types.ts +92 -0
  95. package/tests/introspectors/keystone-introspector.test.ts +255 -0
  96. package/tests/introspectors/nextjs-introspector.test.ts +302 -0
  97. package/tests/introspectors/prisma-introspector.test.ts +268 -0
  98. package/tests/migration-generator.test.ts +592 -0
  99. package/tests/migration-wizard.test.ts +442 -0
  100. package/tsconfig.tsbuildinfo +1 -1
  101. package/dist/generator/type-patcher.d.ts +0 -13
  102. package/dist/generator/type-patcher.d.ts.map +0 -1
  103. package/dist/generator/type-patcher.js +0 -68
  104. package/dist/generator/type-patcher.js.map +0 -1
  105. package/src/generator/type-patcher.ts +0 -93
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Migration command - Helps migrate existing projects to OpenSaaS Stack
3
+ */
4
+
5
+ import { Command } from 'commander'
6
+ import * as fs from 'fs'
7
+ import * as path from 'path'
8
+ import chalk from 'chalk'
9
+ import ora from 'ora'
10
+ import type { ProjectAnalysis, ProjectType } from '../migration/types.js'
11
+
12
+ interface MigrateOptions {
13
+ withAi?: boolean
14
+ type?: 'prisma' | 'nextjs' | 'keystone'
15
+ }
16
+
17
+ /**
18
+ * Detect what type of project this is
19
+ */
20
+ async function detectProjectType(cwd: string): Promise<ProjectType[]> {
21
+ const types: ProjectType[] = []
22
+
23
+ // Check for Prisma
24
+ const prismaSchemaPath = path.join(cwd, 'prisma', 'schema.prisma')
25
+ if (fs.existsSync(prismaSchemaPath)) {
26
+ types.push('prisma')
27
+ }
28
+
29
+ // Check for KeystoneJS
30
+ const keystoneConfigPath = path.join(cwd, 'keystone.config.ts')
31
+ const keystoneAltPath = path.join(cwd, 'keystone.ts')
32
+ if (fs.existsSync(keystoneConfigPath) || fs.existsSync(keystoneAltPath)) {
33
+ types.push('keystone')
34
+ }
35
+
36
+ // Check for Next.js
37
+ const packageJsonPath = path.join(cwd, 'package.json')
38
+ if (fs.existsSync(packageJsonPath)) {
39
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
40
+ if (pkg.dependencies?.next || pkg.devDependencies?.next) {
41
+ types.push('nextjs')
42
+ }
43
+ }
44
+
45
+ return types
46
+ }
47
+
48
+ /**
49
+ * Analyze a Prisma schema
50
+ */
51
+ async function analyzePrismaSchema(cwd: string): Promise<{
52
+ models: Array<{ name: string; fieldCount: number }>
53
+ provider: string
54
+ }> {
55
+ const schemaPath = path.join(cwd, 'prisma', 'schema.prisma')
56
+ const schema = fs.readFileSync(schemaPath, 'utf-8')
57
+
58
+ // Extract models
59
+ const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g
60
+ const models: Array<{ name: string; fieldCount: number }> = []
61
+ let match
62
+
63
+ while ((match = modelRegex.exec(schema)) !== null) {
64
+ const name = match[1]
65
+ const body = match[2]
66
+ const fieldCount = body
67
+ .split('\n')
68
+ .filter(
69
+ (line) => line.trim() && !line.trim().startsWith('@@') && !line.trim().startsWith('//'),
70
+ ).length
71
+ models.push({ name, fieldCount })
72
+ }
73
+
74
+ // Extract provider
75
+ const providerMatch = schema.match(/provider\s*=\s*"(\w+)"/)
76
+ const provider = providerMatch ? providerMatch[1] : 'unknown'
77
+
78
+ return { models, provider }
79
+ }
80
+
81
+ /**
82
+ * Ensure directory exists
83
+ */
84
+ function ensureDir(dirPath: string): void {
85
+ if (!fs.existsSync(dirPath)) {
86
+ fs.mkdirSync(dirPath, { recursive: true })
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Generate template content with placeholders replaced
92
+ */
93
+ function generateTemplateContent(template: string, data: ProjectAnalysis): string {
94
+ const projectType = data.projectTypes[0] || 'unknown'
95
+ const projectTypeLower = projectType.toLowerCase()
96
+ const modelCount = data.models?.length || 0
97
+ const modelList = data.models?.map((m) => `- ${m.name} (${m.fieldCount} fields)`).join('\n') || ''
98
+ const modelDetails =
99
+ data.models?.map((m) => `- **${m.name}**: ${m.fieldCount} fields`).join('\n') || ''
100
+
101
+ return template
102
+ .replace(/\{\{PROJECT_TYPES\}\}/g, data.projectTypes.join(', '))
103
+ .replace(/\{\{PROJECT_TYPE\}\}/g, projectType)
104
+ .replace(/\{\{PROJECT_TYPE_LOWER\}\}/g, projectTypeLower)
105
+ .replace(/\{\{PROVIDER\}\}/g, data.provider || 'sqlite')
106
+ .replace(/\{\{MODEL_COUNT\}\}/g, String(modelCount))
107
+ .replace(/\{\{HAS_AUTH\}\}/g, String(data.hasAuth || false))
108
+ .replace(/\{\{MODEL_LIST\}\}/g, modelList)
109
+ .replace(/\{\{MODEL_DETAILS\}\}/g, modelDetails)
110
+ }
111
+
112
+ /**
113
+ * Setup Claude Code integration
114
+ */
115
+ async function setupClaudeCode(cwd: string, analysis: ProjectAnalysis): Promise<void> {
116
+ const claudeDir = path.join(cwd, '.claude')
117
+ const agentsDir = path.join(claudeDir, 'agents')
118
+ const commandsDir = path.join(claudeDir, 'commands')
119
+
120
+ // Create directories
121
+ ensureDir(agentsDir)
122
+ ensureDir(commandsDir)
123
+
124
+ // Create settings.json
125
+ const settings = {
126
+ mcpServers: {
127
+ 'opensaas-migration': {
128
+ command: 'npx',
129
+ args: ['@opensaas/stack-cli', 'mcp', 'start'],
130
+ disabled: false,
131
+ },
132
+ },
133
+ }
134
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify(settings, null, 2))
135
+
136
+ // Create README template
137
+ const readmeTemplate = `# OpenSaaS Stack Migration
138
+
139
+ This project is being migrated to OpenSaaS Stack.
140
+
141
+ ## Project Summary
142
+
143
+ - **Project Type:** {{PROJECT_TYPES}}
144
+ - **Database Provider:** {{PROVIDER}}
145
+ - **Models Detected:** {{MODEL_COUNT}}
146
+
147
+ ### Models
148
+
149
+ {{MODEL_LIST}}
150
+
151
+ ## Quick Start
152
+
153
+ Ask Claude: **"Help me migrate to OpenSaaS Stack"**
154
+
155
+ Claude will guide you through:
156
+ 1. Reviewing your current schema
157
+ 2. Configuring access control
158
+ 3. Setting up authentication (optional)
159
+ 4. Generating \`opensaas.config.ts\`
160
+
161
+ ## Available Commands
162
+
163
+ | Command | Description |
164
+ |---------|-------------|
165
+ | \`/analyze-schema\` | View detailed schema analysis |
166
+ | \`/generate-config\` | Generate the config file |
167
+ | \`/validate-migration\` | Validate generated config |
168
+
169
+ ## Resources
170
+
171
+ - [OpenSaaS Stack Documentation](https://stack.opensaas.au/)
172
+ - [Migration Guide](https://stack.opensaas.au/guides/migration)
173
+ - [GitHub Repository](https://github.com/OpenSaasAU/stack)
174
+
175
+ ## Generated By
176
+
177
+ This migration was set up using:
178
+ \`\`\`bash
179
+ npx @opensaas/stack-cli migrate --with-ai
180
+ \`\`\`
181
+ `
182
+
183
+ fs.writeFileSync(
184
+ path.join(claudeDir, 'README.md'),
185
+ generateTemplateContent(readmeTemplate, analysis),
186
+ )
187
+
188
+ // Create migration assistant agent template
189
+ const agentTemplate = `You are the OpenSaaS Stack Migration Assistant, helping users migrate their existing projects to OpenSaaS Stack.
190
+
191
+ ## Project Context
192
+
193
+ **Project Type:** {{PROJECT_TYPES}}
194
+ **Database Provider:** {{PROVIDER}}
195
+ **Total Models:** {{MODEL_COUNT}}
196
+
197
+ ### Detected Models
198
+
199
+ {{MODEL_DETAILS}}
200
+
201
+ ## Your Role
202
+
203
+ Guide the user through a complete migration to OpenSaaS Stack:
204
+
205
+ 1. **Analyze** their current project structure
206
+ 2. **Explain** what OpenSaaS Stack offers (access control, admin UI, type safety)
207
+ 3. **Guide** them through the migration wizard
208
+ 4. **Generate** a working \`opensaas.config.ts\`
209
+ 5. **Validate** the generated configuration
210
+ 6. **Provide** clear next steps
211
+
212
+ ## Available MCP Tools
213
+
214
+ ### Schema Analysis
215
+ - \`opensaas_introspect_prisma\` - Analyze Prisma schema in detail
216
+ - \`opensaas_introspect_keystone\` - Analyze KeystoneJS config
217
+
218
+ ### Migration Wizard
219
+ - \`opensaas_start_migration\` - Start the interactive wizard
220
+ - \`opensaas_answer_migration\` - Answer wizard questions
221
+
222
+ ### Documentation
223
+ - \`opensaas_search_migration_docs\` - Search migration documentation
224
+ - \`opensaas_get_example\` - Get example code patterns
225
+
226
+ ### Validation
227
+ - \`opensaas_validate_feature\` - Validate implementation
228
+
229
+ ## Conversation Guidelines
230
+
231
+ ### When the user says "help me migrate" or similar:
232
+
233
+ 1. **Acknowledge** their project:
234
+ > "I can see you have a {{PROJECT_TYPE}} project with {{MODEL_COUNT}} models. Let me help you migrate to OpenSaaS Stack!"
235
+
236
+ 2. **Start the wizard** by calling:
237
+ \`\`\`
238
+ opensaas_start_migration({ projectType: "{{PROJECT_TYPE_LOWER}}" })
239
+ \`\`\`
240
+
241
+ 3. **Present questions naturally** - don't mention session IDs or technical details to the user
242
+
243
+ 4. **Explain choices** - help them understand what each option means:
244
+ - Access control patterns
245
+ - Authentication options
246
+ - Database configuration
247
+
248
+ 5. **Show progress** - let them know how far along they are
249
+
250
+ 6. **Generate the config** when complete and explain what was created
251
+
252
+ ### When explaining OpenSaaS Stack:
253
+
254
+ Highlight these benefits:
255
+ - **Built-in access control** - Secure by default
256
+ - **Admin UI** - Auto-generated from your schema
257
+ - **Type safety** - Full TypeScript support
258
+ - **Prisma integration** - Uses familiar ORM
259
+ - **Plugin system** - Easy to extend
260
+
261
+ ### When answering questions:
262
+
263
+ - Use \`opensaas_search_migration_docs\` to find accurate information
264
+ - Use \`opensaas_get_example\` to show code patterns
265
+ - Be honest if something isn't supported
266
+
267
+ ### Tone
268
+
269
+ - Be encouraging and helpful
270
+ - Explain technical concepts simply
271
+ - Celebrate progress ("Great choice!", "Almost there!")
272
+ - Don't overwhelm with information
273
+
274
+ ## Example Conversation
275
+
276
+ **User:** Help me migrate to OpenSaaS Stack
277
+
278
+ **You:** I can see you have a {{PROJECT_TYPE}} project with {{MODEL_COUNT}} models. OpenSaaS Stack will give you:
279
+
280
+ - Automatic admin UI for managing your data
281
+ - Built-in access control to secure your API
282
+ - Type-safe database operations
283
+
284
+ Let me start the migration wizard to configure your project...
285
+
286
+ [Call opensaas_start_migration]
287
+
288
+ **User:** [answers questions]
289
+
290
+ **You:** [Continue through wizard, explain each choice, generate final config]
291
+
292
+ ## Error Handling
293
+
294
+ If something goes wrong:
295
+ 1. Explain what happened in simple terms
296
+ 2. Suggest alternatives or manual steps
297
+ 3. Link to documentation for more help
298
+
299
+ ## After Migration
300
+
301
+ Once the config is generated, guide them through:
302
+ 1. Installing dependencies
303
+ 2. Running \`opensaas generate\`
304
+ 3. Running \`prisma db push\`
305
+ 4. Starting their dev server
306
+ 5. Visiting the admin UI
307
+ `
308
+
309
+ fs.writeFileSync(
310
+ path.join(agentsDir, 'migration-assistant.md'),
311
+ generateTemplateContent(agentTemplate, analysis),
312
+ )
313
+
314
+ // Create analyze-schema command
315
+ const analyzeSchemaTemplate = `Analyze the current project schema and provide a detailed breakdown.
316
+
317
+ ## Instructions
318
+
319
+ 1. Use \`opensaas_introspect_prisma\` or \`opensaas_introspect_keystone\` based on project type
320
+ 2. Present the results in a clear, organized format
321
+ 3. Highlight:
322
+ - All models and their fields
323
+ - Relationships between models
324
+ - Potential access control patterns
325
+ - Any issues or warnings
326
+
327
+ ## Output Format
328
+
329
+ Present like this:
330
+
331
+ ### Models Summary
332
+
333
+ | Model | Fields | Has Relations | Suggested Access |
334
+ |-------|--------|---------------|------------------|
335
+ | ... | ... | ... | ... |
336
+
337
+ ### Detailed Analysis
338
+
339
+ [For each model, show fields and relationships]
340
+
341
+ ### Recommendations
342
+
343
+ [Based on the schema, suggest access control patterns]
344
+ `
345
+
346
+ fs.writeFileSync(path.join(commandsDir, 'analyze-schema.md'), analyzeSchemaTemplate)
347
+
348
+ // Create generate-config command
349
+ const generateConfigTemplate = `Generate the opensaas.config.ts file for this project.
350
+
351
+ ## Instructions
352
+
353
+ 1. If migration wizard hasn't been started, start it:
354
+ \`\`\`
355
+ opensaas_start_migration({ projectType: "{{PROJECT_TYPE_LOWER}}" })
356
+ \`\`\`
357
+
358
+ 2. Guide the user through any remaining questions
359
+
360
+ 3. When complete, display:
361
+ - The generated config file
362
+ - Dependencies to install
363
+ - Next steps to run
364
+
365
+ 4. Offer to explain any part of the generated config
366
+
367
+ ## Quick Mode
368
+
369
+ If the user wants defaults, use these answers:
370
+ - preserve_database: true
371
+ - db_provider: {{PROVIDER}}
372
+ - enable_auth: {{HAS_AUTH}}
373
+ - default_access: "public-read-auth-write"
374
+ - admin_base_path: "/admin"
375
+ `
376
+
377
+ fs.writeFileSync(
378
+ path.join(commandsDir, 'generate-config.md'),
379
+ generateTemplateContent(generateConfigTemplate, analysis),
380
+ )
381
+
382
+ // Create validate-migration command
383
+ const validateMigrationTemplate = `Validate the generated opensaas.config.ts file.
384
+
385
+ ## Instructions
386
+
387
+ 1. Check if opensaas.config.ts exists in the project root
388
+
389
+ 2. If it exists, verify:
390
+ - Syntax is valid TypeScript
391
+ - All imports are correct
392
+ - Database config is complete
393
+ - Lists match original schema
394
+
395
+ 3. Try running:
396
+ \`\`\`bash
397
+ npx @opensaas/stack-cli generate
398
+ \`\`\`
399
+
400
+ 4. Report any errors and suggest fixes
401
+
402
+ 5. If validation passes, confirm next steps:
403
+ - \`npx prisma generate\`
404
+ - \`npx prisma db push\`
405
+ - \`pnpm dev\`
406
+
407
+ ## Common Issues
408
+
409
+ - Missing dependencies → suggest \`pnpm add ...\`
410
+ - Database URL not set → remind about .env file
411
+ - Type errors → suggest specific fixes
412
+ `
413
+
414
+ fs.writeFileSync(path.join(commandsDir, 'validate-migration.md'), validateMigrationTemplate)
415
+ }
416
+
417
+ /**
418
+ * Main migrate command
419
+ */
420
+ async function migrateCommand(options: MigrateOptions): Promise<void> {
421
+ const cwd = process.cwd()
422
+
423
+ console.log(chalk.bold.cyan('\n🚀 OpenSaaS Stack Migration\n'))
424
+
425
+ // Step 1: Detect project type
426
+ const spinner = ora('Detecting project type...').start()
427
+
428
+ let projectTypes: ProjectType[]
429
+ if (options.type) {
430
+ projectTypes = [options.type]
431
+ } else {
432
+ projectTypes = await detectProjectType(cwd)
433
+ }
434
+
435
+ if (projectTypes.length === 0) {
436
+ spinner.fail(chalk.red('No recognizable project found'))
437
+ console.log(chalk.dim('\nThis command works with:'))
438
+ console.log(chalk.dim(' - Prisma projects (prisma/schema.prisma)'))
439
+ console.log(chalk.dim(' - KeystoneJS projects (keystone.config.ts)'))
440
+ console.log(chalk.dim(' - Next.js projects (package.json with next)'))
441
+ console.log(chalk.dim('\nUse --type to force a project type.'))
442
+ process.exit(1)
443
+ }
444
+
445
+ spinner.succeed(chalk.green(`Detected: ${projectTypes.join(', ')}`))
446
+
447
+ // Step 2: Analyze schema
448
+ const analysisSpinner = ora('Analyzing schema...').start()
449
+
450
+ const analysis: ProjectAnalysis = {
451
+ projectTypes,
452
+ cwd,
453
+ }
454
+
455
+ if (projectTypes.includes('prisma')) {
456
+ try {
457
+ const prismaAnalysis = await analyzePrismaSchema(cwd)
458
+ analysis.models = prismaAnalysis.models
459
+ analysis.provider = prismaAnalysis.provider
460
+ } catch (_error) {
461
+ // Prisma analysis failed, continue without it
462
+ }
463
+ }
464
+
465
+ if (analysis.models && analysis.models.length > 0) {
466
+ analysisSpinner.succeed(chalk.green(`Found ${analysis.models.length} models`))
467
+
468
+ // Display model tree
469
+ const lastIndex = analysis.models.length - 1
470
+ analysis.models.forEach((model, index) => {
471
+ const prefix = index === lastIndex ? '└─' : '├─'
472
+ console.log(chalk.dim(` ${prefix} ${model.name} (${model.fieldCount} fields)`))
473
+ })
474
+ } else {
475
+ analysisSpinner.succeed(chalk.yellow('No models found (will create from scratch)'))
476
+ }
477
+
478
+ // Step 3: Setup Claude Code (if --with-ai)
479
+ if (options.withAi) {
480
+ const claudeSpinner = ora('Setting up Claude Code...').start()
481
+
482
+ try {
483
+ await setupClaudeCode(cwd, analysis)
484
+ claudeSpinner.succeed(chalk.green('Claude Code ready'))
485
+
486
+ console.log(chalk.dim(' ├─ Created .claude directory'))
487
+ console.log(chalk.dim(' ├─ Generated migration assistant'))
488
+ console.log(chalk.dim(' └─ Registered MCP server'))
489
+ } catch (error) {
490
+ claudeSpinner.fail(chalk.red('Failed to setup Claude Code'))
491
+ console.error(error)
492
+ }
493
+ }
494
+
495
+ // Step 4: Display next steps
496
+ console.log(chalk.green('\n✅ Analysis complete!\n'))
497
+
498
+ if (options.withAi) {
499
+ console.log(chalk.bold('🤖 Next Steps:\n'))
500
+ console.log(chalk.cyan(' 1. Open this project in Claude Code'))
501
+ console.log(chalk.cyan(' 2. Ask: "Help me migrate to OpenSaaS Stack"'))
502
+ console.log(chalk.cyan(' 3. Follow the interactive wizard'))
503
+ } else {
504
+ console.log(chalk.bold('📝 Next Steps:\n'))
505
+ console.log(chalk.cyan(' 1. Run with --with-ai for AI-guided migration'))
506
+ console.log(chalk.cyan(' 2. Or manually create opensaas.config.ts'))
507
+ console.log(chalk.dim('\n See: https://stack.opensaas.au/guides/migration'))
508
+ }
509
+
510
+ console.log(chalk.dim(`\n📚 Documentation: https://stack.opensaas.au/guides/migration\n`))
511
+ }
512
+
513
+ /**
514
+ * Create the migrate command for Commander
515
+ */
516
+ export function createMigrateCommand(): Command {
517
+ const migrate = new Command('migrate')
518
+ migrate.description('Migrate an existing project to OpenSaaS Stack')
519
+
520
+ migrate
521
+ .option('--with-ai', 'Enable AI-guided migration with Claude Code')
522
+ .option('--type <type>', 'Force project type (prisma, nextjs, keystone)')
523
+ .action(async (options: MigrateOptions) => {
524
+ try {
525
+ await migrateCommand(options)
526
+ process.exit(0)
527
+ } catch (error) {
528
+ console.error(chalk.red('\n❌ Migration failed:'), error)
529
+ process.exit(1)
530
+ }
531
+ })
532
+
533
+ return migrate
534
+ }
@@ -14,6 +14,7 @@ import { getContext as getOpensaasContext } from '@opensaas/stack-core'
14
14
  import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
15
15
  import { PrismaClient } from './prisma-client/client'
16
16
  import type { Context } from './types'
17
+ import { prismaExtensions } from './prisma-extensions'
17
18
  import configOrPromise from '../opensaas.config'
18
19
 
19
20
  // Resolve config if it's a Promise (when plugins are present)
@@ -21,15 +22,29 @@ const configPromise = Promise.resolve(configOrPromise)
21
22
  let resolvedConfig: OpenSaasConfig | null = null
22
23
 
23
24
  // Internal Prisma singleton - managed automatically
24
- const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | null }
25
- let prisma: PrismaClient | null = null
25
+ const globalForPrisma = globalThis as unknown as { prisma: ReturnType<typeof createExtendedPrisma> | null }
26
+ let prisma: ReturnType<typeof createExtendedPrisma> | null = null
27
+
28
+ /**
29
+ * Create Prisma client with result extensions
30
+ */
31
+ function createExtendedPrisma(basePrisma: PrismaClient) {
32
+ // Check if there are any extensions to apply
33
+ if (Object.keys(prismaExtensions).length === 0) {
34
+ return basePrisma
35
+ }
36
+ // Apply result extensions
37
+ return basePrisma.$extends(prismaExtensions)
38
+ }
26
39
 
27
40
  async function getPrisma() {
28
41
  if (!prisma) {
29
42
  if (!resolvedConfig) {
30
43
  resolvedConfig = await configPromise
31
44
  }
32
- prisma = globalForPrisma.prisma ?? resolvedConfig.db.prismaClientConstructor!(PrismaClient)
45
+ const basePrisma = resolvedConfig.db.prismaClientConstructor!(PrismaClient)
46
+ const extendedPrisma = createExtendedPrisma(basePrisma)
47
+ prisma = globalForPrisma.prisma ?? extendedPrisma
33
48
  if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
34
49
  }
35
50
  return prisma
@@ -84,7 +99,7 @@ const storage = {
84
99
  export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
85
100
  const config = await getConfig()
86
101
  const prismaClient = await getPrisma()
87
- return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
102
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as unknown as Context<TSession>
88
103
  }
89
104
 
90
105
  /**
@@ -94,7 +109,7 @@ export async function getContext<TSession extends OpensaasSession = OpensaasSess
94
109
  export const rawOpensaasContext = (async () => {
95
110
  const config = await getConfig()
96
111
  const prismaClient = await getPrisma()
97
- return getOpensaasContext(config, prismaClient, null, storage)
112
+ return getOpensaasContext(config, prismaClient, null, storage) as unknown as Context
98
113
  })()
99
114
 
100
115
  /**
@@ -119,6 +134,7 @@ import { getContext as getOpensaasContext } from '@opensaas/stack-core'
119
134
  import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
120
135
  import { PrismaClient } from './prisma-client/client'
121
136
  import type { Context } from './types'
137
+ import { prismaExtensions } from './prisma-extensions'
122
138
  import configOrPromise from '../opensaas.config'
123
139
 
124
140
  // Resolve config if it's a Promise (when plugins are present)
@@ -126,15 +142,29 @@ const configPromise = Promise.resolve(configOrPromise)
126
142
  let resolvedConfig: OpenSaasConfig | null = null
127
143
 
128
144
  // Internal Prisma singleton - managed automatically
129
- const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | null }
130
- let prisma: PrismaClient | null = null
145
+ const globalForPrisma = globalThis as unknown as { prisma: ReturnType<typeof createExtendedPrisma> | null }
146
+ let prisma: ReturnType<typeof createExtendedPrisma> | null = null
147
+
148
+ /**
149
+ * Create Prisma client with result extensions
150
+ */
151
+ function createExtendedPrisma(basePrisma: PrismaClient) {
152
+ // Check if there are any extensions to apply
153
+ if (Object.keys(prismaExtensions).length === 0) {
154
+ return basePrisma
155
+ }
156
+ // Apply result extensions
157
+ return basePrisma.$extends(prismaExtensions)
158
+ }
131
159
 
132
160
  async function getPrisma() {
133
161
  if (!prisma) {
134
162
  if (!resolvedConfig) {
135
163
  resolvedConfig = await configPromise
136
164
  }
137
- prisma = globalForPrisma.prisma ?? resolvedConfig.db.prismaClientConstructor!(PrismaClient)
165
+ const basePrisma = resolvedConfig.db.prismaClientConstructor!(PrismaClient)
166
+ const extendedPrisma = createExtendedPrisma(basePrisma)
167
+ prisma = globalForPrisma.prisma ?? extendedPrisma
138
168
  if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
139
169
  }
140
170
  return prisma
@@ -189,7 +219,7 @@ const storage = {
189
219
  export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
190
220
  const config = await getConfig()
191
221
  const prismaClient = await getPrisma()
192
- return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
222
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as unknown as Context<TSession>
193
223
  }
194
224
 
195
225
  /**
@@ -199,7 +229,7 @@ export async function getContext<TSession extends OpensaasSession = OpensaasSess
199
229
  export const rawOpensaasContext = (async () => {
200
230
  const config = await getConfig()
201
231
  const prismaClient = await getPrisma()
202
- return getOpensaasContext(config, prismaClient, null, storage)
232
+ return getOpensaasContext(config, prismaClient, null, storage) as unknown as Context
203
233
  })()
204
234
 
205
235
  /**
@@ -224,6 +254,7 @@ import { getContext as getOpensaasContext } from '@opensaas/stack-core'
224
254
  import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
225
255
  import { PrismaClient } from './prisma-client/client'
226
256
  import type { Context } from './types'
257
+ import { prismaExtensions } from './prisma-extensions'
227
258
  import configOrPromise from '../opensaas.config'
228
259
 
229
260
  // Resolve config if it's a Promise (when plugins are present)
@@ -231,15 +262,29 @@ const configPromise = Promise.resolve(configOrPromise)
231
262
  let resolvedConfig: OpenSaasConfig | null = null
232
263
 
233
264
  // Internal Prisma singleton - managed automatically
234
- const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | null }
235
- let prisma: PrismaClient | null = null
265
+ const globalForPrisma = globalThis as unknown as { prisma: ReturnType<typeof createExtendedPrisma> | null }
266
+ let prisma: ReturnType<typeof createExtendedPrisma> | null = null
267
+
268
+ /**
269
+ * Create Prisma client with result extensions
270
+ */
271
+ function createExtendedPrisma(basePrisma: PrismaClient) {
272
+ // Check if there are any extensions to apply
273
+ if (Object.keys(prismaExtensions).length === 0) {
274
+ return basePrisma
275
+ }
276
+ // Apply result extensions
277
+ return basePrisma.$extends(prismaExtensions)
278
+ }
236
279
 
237
280
  async function getPrisma() {
238
281
  if (!prisma) {
239
282
  if (!resolvedConfig) {
240
283
  resolvedConfig = await configPromise
241
284
  }
242
- prisma = globalForPrisma.prisma ?? resolvedConfig.db.prismaClientConstructor!(PrismaClient)
285
+ const basePrisma = resolvedConfig.db.prismaClientConstructor!(PrismaClient)
286
+ const extendedPrisma = createExtendedPrisma(basePrisma)
287
+ prisma = globalForPrisma.prisma ?? extendedPrisma
243
288
  if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
244
289
  }
245
290
  return prisma
@@ -294,7 +339,7 @@ const storage = {
294
339
  export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
295
340
  const config = await getConfig()
296
341
  const prismaClient = await getPrisma()
297
- return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
342
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as unknown as Context<TSession>
298
343
  }
299
344
 
300
345
  /**
@@ -304,7 +349,7 @@ export async function getContext<TSession extends OpensaasSession = OpensaasSess
304
349
  export const rawOpensaasContext = (async () => {
305
350
  const config = await getConfig()
306
351
  const prismaClient = await getPrisma()
307
- return getOpensaasContext(config, prismaClient, null, storage)
352
+ return getOpensaasContext(config, prismaClient, null, storage) as unknown as Context
308
353
  })()
309
354
 
310
355
  /**