@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +193 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +4 -13
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/migrate.d.ts +9 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +473 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/generator/context.d.ts.map +1 -1
- package/dist/generator/context.js +20 -5
- package/dist/generator/context.js.map +1 -1
- package/dist/generator/index.d.ts +1 -1
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +1 -1
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/lists.d.ts.map +1 -1
- package/dist/generator/lists.js +33 -1
- package/dist/generator/lists.js.map +1 -1
- package/dist/generator/prisma-extensions.d.ts +11 -0
- package/dist/generator/prisma-extensions.d.ts.map +1 -0
- package/dist/generator/prisma-extensions.js +134 -0
- package/dist/generator/prisma-extensions.js.map +1 -0
- package/dist/generator/prisma.d.ts.map +1 -1
- package/dist/generator/prisma.js +4 -0
- package/dist/generator/prisma.js.map +1 -1
- package/dist/generator/types.d.ts.map +1 -1
- package/dist/generator/types.js +151 -17
- package/dist/generator/types.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/lib/documentation-provider.d.ts +23 -0
- package/dist/mcp/lib/documentation-provider.d.ts.map +1 -1
- package/dist/mcp/lib/documentation-provider.js +471 -0
- package/dist/mcp/lib/documentation-provider.js.map +1 -1
- package/dist/mcp/lib/wizards/migration-wizard.d.ts +80 -0
- package/dist/mcp/lib/wizards/migration-wizard.d.ts.map +1 -0
- package/dist/mcp/lib/wizards/migration-wizard.js +499 -0
- package/dist/mcp/lib/wizards/migration-wizard.js.map +1 -0
- package/dist/mcp/server/index.d.ts.map +1 -1
- package/dist/mcp/server/index.js +103 -0
- package/dist/mcp/server/index.js.map +1 -1
- package/dist/mcp/server/stack-mcp-server.d.ts +85 -0
- package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -1
- package/dist/mcp/server/stack-mcp-server.js +219 -0
- package/dist/mcp/server/stack-mcp-server.js.map +1 -1
- package/dist/migration/generators/migration-generator.d.ts +60 -0
- package/dist/migration/generators/migration-generator.d.ts.map +1 -0
- package/dist/migration/generators/migration-generator.js +510 -0
- package/dist/migration/generators/migration-generator.js.map +1 -0
- package/dist/migration/introspectors/index.d.ts +12 -0
- package/dist/migration/introspectors/index.d.ts.map +1 -0
- package/dist/migration/introspectors/index.js +10 -0
- package/dist/migration/introspectors/index.js.map +1 -0
- package/dist/migration/introspectors/keystone-introspector.d.ts +59 -0
- package/dist/migration/introspectors/keystone-introspector.d.ts.map +1 -0
- package/dist/migration/introspectors/keystone-introspector.js +229 -0
- package/dist/migration/introspectors/keystone-introspector.js.map +1 -0
- package/dist/migration/introspectors/nextjs-introspector.d.ts +59 -0
- package/dist/migration/introspectors/nextjs-introspector.d.ts.map +1 -0
- package/dist/migration/introspectors/nextjs-introspector.js +159 -0
- package/dist/migration/introspectors/nextjs-introspector.js.map +1 -0
- package/dist/migration/introspectors/prisma-introspector.d.ts +45 -0
- package/dist/migration/introspectors/prisma-introspector.d.ts.map +1 -0
- package/dist/migration/introspectors/prisma-introspector.js +190 -0
- package/dist/migration/introspectors/prisma-introspector.js.map +1 -0
- package/dist/migration/types.d.ts +86 -0
- package/dist/migration/types.d.ts.map +1 -0
- package/dist/migration/types.js +5 -0
- package/dist/migration/types.js.map +1 -0
- package/package.json +12 -9
- package/src/commands/__snapshots__/generate.test.ts.snap +92 -21
- package/src/commands/generate.ts +8 -19
- package/src/commands/migrate.ts +534 -0
- package/src/generator/__snapshots__/context.test.ts.snap +60 -15
- package/src/generator/__snapshots__/types.test.ts.snap +689 -95
- package/src/generator/context.test.ts +3 -1
- package/src/generator/context.ts +20 -5
- package/src/generator/index.ts +1 -1
- package/src/generator/lists.ts +39 -1
- package/src/generator/prisma-extensions.ts +159 -0
- package/src/generator/prisma.ts +5 -0
- package/src/generator/types.ts +204 -17
- package/src/index.ts +4 -0
- package/src/mcp/lib/documentation-provider.ts +507 -0
- package/src/mcp/lib/wizards/migration-wizard.ts +584 -0
- package/src/mcp/server/index.ts +121 -0
- package/src/mcp/server/stack-mcp-server.ts +243 -0
- package/src/migration/generators/migration-generator.ts +675 -0
- package/src/migration/introspectors/index.ts +12 -0
- package/src/migration/introspectors/keystone-introspector.ts +296 -0
- package/src/migration/introspectors/nextjs-introspector.ts +209 -0
- package/src/migration/introspectors/prisma-introspector.ts +233 -0
- package/src/migration/types.ts +92 -0
- package/tests/introspectors/keystone-introspector.test.ts +255 -0
- package/tests/introspectors/nextjs-introspector.test.ts +302 -0
- package/tests/introspectors/prisma-introspector.test.ts +268 -0
- package/tests/migration-generator.test.ts +592 -0
- package/tests/migration-wizard.test.ts +442 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/generator/type-patcher.d.ts +0 -13
- package/dist/generator/type-patcher.d.ts.map +0 -1
- package/dist/generator/type-patcher.js +0 -68
- package/dist/generator/type-patcher.js.map +0 -1
- 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:
|
|
25
|
-
let prisma:
|
|
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
|
-
|
|
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:
|
|
130
|
-
let prisma:
|
|
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
|
-
|
|
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:
|
|
235
|
-
let prisma:
|
|
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
|
-
|
|
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
|
/**
|