@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,544 +0,0 @@
1
- /**
2
- * OpenSaaS Stack MCP Server - Business logic for MCP tools
3
- */
4
-
5
- import { WizardEngine } from '../lib/wizards/wizard-engine.js'
6
- import { MigrationWizard } from '../lib/wizards/migration-wizard.js'
7
- import { OpenSaasDocumentationProvider } from '../lib/documentation-provider.js'
8
- import { getAllFeatures, getFeature } from '../lib/features/catalog.js'
9
- import { PrismaIntrospector } from '../../migration/introspectors/prisma-introspector.js'
10
- import { KeystoneIntrospector } from '../../migration/introspectors/keystone-introspector.js'
11
- import type { ProjectType } from '../../migration/types.js'
12
-
13
- export class StackMCPServer {
14
- private wizardEngine: WizardEngine
15
- private migrationWizard: MigrationWizard
16
- private docsProvider: OpenSaasDocumentationProvider
17
- private prismaIntrospector: PrismaIntrospector
18
- private keystoneIntrospector: KeystoneIntrospector
19
-
20
- constructor() {
21
- this.wizardEngine = new WizardEngine()
22
- this.migrationWizard = new MigrationWizard()
23
- this.docsProvider = new OpenSaasDocumentationProvider()
24
- this.prismaIntrospector = new PrismaIntrospector()
25
- this.keystoneIntrospector = new KeystoneIntrospector()
26
- }
27
-
28
- /**
29
- * Implement a feature - starts the wizard flow
30
- */
31
- async implementFeature({ feature, description }: { feature: string; description?: string }) {
32
- if (feature === 'custom' && !description) {
33
- return {
34
- content: [
35
- {
36
- type: 'text' as const,
37
- text: '❌ For custom features, please provide a description of what you want to build.',
38
- },
39
- ],
40
- }
41
- }
42
-
43
- if (feature === 'custom') {
44
- return {
45
- content: [
46
- {
47
- type: 'text' as const,
48
- text: `🔧 **Custom Feature**: ${description}
49
-
50
- I'll help you build this custom feature. Let me search the docs for relevant patterns...
51
-
52
- ${await this.searchFeatureDocs({ topic: description! })}
53
-
54
- Based on your description, consider using these OpenSaaS patterns:
55
- - Define your data model with \`list()\` and field types
56
- - Add access control with \`access.operation\` and \`access.field\`
57
- - Use hooks for data transformation and side effects
58
- - Consider if any plugins would help (auth, storage, RAG)
59
-
60
- Would you like me to help you design the config for this feature?`,
61
- },
62
- ],
63
- }
64
- }
65
-
66
- return this.wizardEngine.startFeature(feature)
67
- }
68
-
69
- /**
70
- * Answer a wizard question
71
- */
72
- async answerFeatureQuestion({
73
- sessionId,
74
- answer,
75
- }: {
76
- sessionId: string
77
- answer: string | boolean | string[]
78
- }) {
79
- return this.wizardEngine.answerQuestion(sessionId, answer)
80
- }
81
-
82
- /**
83
- * Answer a follow-up question
84
- */
85
- async answerFollowUpQuestion({ sessionId, answer }: { sessionId: string; answer: string }) {
86
- return this.wizardEngine.answerFollowUp(sessionId, answer)
87
- }
88
-
89
- /**
90
- * Search documentation
91
- */
92
- async searchFeatureDocs({ topic }: { topic: string }) {
93
- const docs = await this.docsProvider.getTopicDocs(topic)
94
-
95
- return {
96
- content: [
97
- {
98
- type: 'text' as const,
99
- text: `# ${docs.topic}
100
-
101
- ${docs.content}
102
-
103
- ${docs.codeExamples.length > 0 ? `\n## Code Examples\n\n${docs.codeExamples.join('\n\n')}` : ''}
104
-
105
- ${docs.relatedTopics.length > 0 ? `\n## Related Topics\n\n${docs.relatedTopics.map((t) => `- ${t}`).join('\n')}` : ''}
106
-
107
- ---
108
-
109
- 📚 **Full documentation**: ${docs.url}`,
110
- },
111
- ],
112
- }
113
- }
114
-
115
- /**
116
- * List all available features
117
- */
118
- async listFeatures() {
119
- const features = getAllFeatures()
120
- const categories = {
121
- authentication: [] as typeof features,
122
- content: [] as typeof features,
123
- storage: [] as typeof features,
124
- search: [] as typeof features,
125
- custom: [] as typeof features,
126
- }
127
-
128
- features.forEach((feature) => {
129
- categories[feature.category].push(feature)
130
- })
131
-
132
- const formatFeatureList = (featureList: typeof features) =>
133
- featureList
134
- .map(
135
- (f) =>
136
- `### ${f.name}\n**ID**: \`${f.id}\`\n${f.description}\n\n**Includes**:\n${f.includes.map((i) => `- ${i}`).join('\n')}\n${f.dependsOn && f.dependsOn.length > 0 ? `\n**Requires**: ${f.dependsOn.join(', ')}` : ''}`,
137
- )
138
- .join('\n\n')
139
-
140
- return {
141
- content: [
142
- {
143
- type: 'text' as const,
144
- text: `# Available OpenSaaS Stack Features
145
-
146
- Use \`opensaas_implement_feature\` to start implementing any of these:
147
-
148
- ## 🔐 Authentication
149
-
150
- ${formatFeatureList(categories.authentication)}
151
-
152
- ## 📝 Content Management
153
-
154
- ${formatFeatureList(categories.content)}
155
-
156
- ## 📦 Storage
157
-
158
- ${formatFeatureList(categories.storage)}
159
-
160
- ## 🔍 Search
161
-
162
- ${formatFeatureList(categories.search)}
163
-
164
- ---
165
-
166
- **To implement a feature**, call:
167
- \`\`\`
168
- opensaas_implement_feature({ feature: "authentication" })
169
- \`\`\`
170
-
171
- **For custom features**, call:
172
- \`\`\`
173
- opensaas_implement_feature({
174
- feature: "custom",
175
- description: "what you want to build"
176
- })
177
- \`\`\``,
178
- },
179
- ],
180
- }
181
- }
182
-
183
- /**
184
- * Suggest complementary features based on described features
185
- */
186
- async suggestFeatures({ currentFeatures }: { currentFeatures?: string[] }) {
187
- const allFeatures = getAllFeatures()
188
- const implemented = new Set(currentFeatures || [])
189
-
190
- const suggestions = allFeatures
191
- .filter((f) => !implemented.has(f.id))
192
- .map((f) => {
193
- let reasoning = ''
194
-
195
- // Add context-aware suggestions
196
- if (f.id === 'authentication' && !implemented.has('authentication')) {
197
- reasoning = 'Essential for user management and access control'
198
- } else if (f.id === 'blog' && implemented.has('authentication')) {
199
- reasoning = 'You have auth - add content creation capabilities'
200
- } else if (
201
- f.id === 'comments' &&
202
- (implemented.has('blog') || implemented.has('authentication'))
203
- ) {
204
- reasoning = 'Enhance engagement with user comments'
205
- } else if (
206
- f.id === 'file-upload' &&
207
- (implemented.has('blog') || implemented.has('authentication'))
208
- ) {
209
- reasoning = 'Add media support for richer content'
210
- } else if (f.id === 'semantic-search' && implemented.has('blog')) {
211
- reasoning = 'Make your content more discoverable'
212
- }
213
-
214
- return { feature: f, reasoning }
215
- })
216
- .filter((s) => s.reasoning)
217
-
218
- if (suggestions.length === 0) {
219
- return {
220
- content: [
221
- {
222
- type: 'text' as const,
223
- text: "🎉 You've implemented all our built-in features!\n\nConsider building custom features tailored to your specific needs using `opensaas_implement_feature({ feature: 'custom', description: '...' })`",
224
- },
225
- ],
226
- }
227
- }
228
-
229
- return {
230
- content: [
231
- {
232
- type: 'text' as const,
233
- text: `# Feature Suggestions
234
-
235
- Based on ${currentFeatures && currentFeatures.length > 0 ? `your current features (${currentFeatures.join(', ')})` : 'common patterns'}, consider adding:
236
-
237
- ${suggestions
238
- .map(
239
- (s, i) => `## ${i + 1}. ${s.feature.name}
240
-
241
- ${s.reasoning}
242
-
243
- **What you'll get**:
244
- ${s.feature.includes.map((inc) => `- ${inc}`).join('\n')}
245
-
246
- **To implement**: \`opensaas_implement_feature({ feature: "${s.feature.id}" })\`
247
- `,
248
- )
249
- .join('\n---\n\n')}`,
250
- },
251
- ],
252
- }
253
- }
254
-
255
- /**
256
- * Validate a feature implementation
257
- */
258
- async validateFeature({ feature }: { feature: string; configPath?: string }) {
259
- const featureDefinition = getFeature(feature)
260
-
261
- if (!featureDefinition) {
262
- return {
263
- content: [
264
- {
265
- type: 'text' as const,
266
- text: `❌ Unknown feature: ${feature}\n\nUse \`opensaas_list_features\` to see available features.`,
267
- },
268
- ],
269
- }
270
- }
271
-
272
- // TODO: Implement actual validation by reading the config file
273
- // For now, return validation checklist
274
-
275
- return {
276
- content: [
277
- {
278
- type: 'text' as const,
279
- text: `# ${featureDefinition.name} Validation
280
-
281
- Checking your implementation...
282
-
283
- ## Checklist
284
-
285
- ${featureDefinition.includes.map((item) => `- [ ] ${item}`).join('\n')}
286
-
287
- ${featureDefinition.dependsOn && featureDefinition.dependsOn.length > 0 ? `\n## Dependencies\n\nEnsure these features are implemented:\n${featureDefinition.dependsOn.map((dep) => `- [ ] ${dep}`).join('\n')}\n` : ''}
288
-
289
- ---
290
-
291
- **Manual validation steps**:
292
-
293
- 1. Check that all required lists are defined in your config
294
- 2. Verify access control is properly configured
295
- 3. Test the feature in your development environment
296
- 4. Run \`pnpm generate\` and \`pnpm db:push\` successfully
297
-
298
- **Note**: Full automatic validation coming soon! For now, use this checklist to verify your implementation.`,
299
- },
300
- ],
301
- }
302
- }
303
-
304
- /**
305
- * Start a migration wizard session
306
- */
307
- async startMigration({ projectType }: { projectType: ProjectType }) {
308
- return this.migrationWizard.startMigration(projectType)
309
- }
310
-
311
- /**
312
- * Answer a migration wizard question
313
- */
314
- async answerMigration({
315
- sessionId,
316
- answer,
317
- }: {
318
- sessionId: string
319
- answer: string | boolean | string[]
320
- }) {
321
- return this.migrationWizard.answerQuestion(sessionId, answer)
322
- }
323
-
324
- /**
325
- * Introspect a Prisma schema
326
- */
327
- async introspectPrisma({ schemaPath }: { schemaPath?: string }) {
328
- const cwd = process.cwd()
329
- const path = schemaPath || 'prisma/schema.prisma'
330
-
331
- try {
332
- const schema = await this.prismaIntrospector.introspect(cwd, path)
333
-
334
- const modelList = schema.models
335
- .map((m) => {
336
- const fields = m.fields
337
- .map((f) => {
338
- let type = f.type
339
- if (f.relation) type = `→ ${f.relation.model}`
340
- if (f.isList) type = `${type}[]`
341
- if (!f.isRequired) type = `${type}?`
342
- return ` - ${f.name}: ${type}`
343
- })
344
- .join('\n')
345
- return `### ${m.name}\n${fields}`
346
- })
347
- .join('\n\n')
348
-
349
- const enumList =
350
- schema.enums.length > 0
351
- ? `\n## Enums\n\n${schema.enums.map((e) => `- **${e.name}**: ${e.values.join(', ')}`).join('\n')}`
352
- : ''
353
-
354
- return {
355
- content: [
356
- {
357
- type: 'text' as const,
358
- text: `# Prisma Schema Analysis
359
-
360
- **Provider:** ${schema.provider}
361
- **Models:** ${schema.models.length}
362
- **Enums:** ${schema.enums.length}
363
-
364
- ## Models
365
-
366
- ${modelList}
367
- ${enumList}
368
-
369
- ---
370
-
371
- **Ready to migrate?** Use \`opensaas_start_migration({ projectType: "prisma" })\` to begin the wizard.`,
372
- },
373
- ],
374
- }
375
- } catch (error) {
376
- const message = error instanceof Error ? error.message : String(error)
377
- return {
378
- content: [
379
- {
380
- type: 'text' as const,
381
- text: `❌ Failed to introspect Prisma schema: ${message}\n\nMake sure the file exists at: ${path}`,
382
- },
383
- ],
384
- isError: true,
385
- }
386
- }
387
- }
388
-
389
- /**
390
- * Introspect a KeystoneJS config
391
- */
392
- async introspectKeystone({ configPath }: { configPath?: string }) {
393
- const cwd = process.cwd()
394
- const path = configPath || 'keystone.config.ts'
395
-
396
- try {
397
- const config = await this.keystoneIntrospector.introspect(cwd, path)
398
-
399
- const listInfo = config.models
400
- .map((m) => {
401
- const fields = m.fields.map((f) => ` - ${f.name}: ${f.type}`).join('\n')
402
- return `### ${m.name}\n${fields}`
403
- })
404
- .join('\n\n')
405
-
406
- return {
407
- content: [
408
- {
409
- type: 'text' as const,
410
- text: `# KeystoneJS Config Analysis
411
-
412
- **Lists:** ${config.models.length}
413
-
414
- ## Lists
415
-
416
- ${listInfo}
417
-
418
- ---
419
-
420
- **Note:** KeystoneJS → OpenSaaS migration is mostly 1:1. Field types and access control patterns map directly.
421
-
422
- **Ready to migrate?** Use \`opensaas_start_migration({ projectType: "keystone" })\` to begin.`,
423
- },
424
- ],
425
- }
426
- } catch (error) {
427
- const message = error instanceof Error ? error.message : String(error)
428
- return {
429
- content: [
430
- {
431
- type: 'text' as const,
432
- text: `❌ Failed to introspect KeystoneJS config: ${message}\n\nMake sure the file exists at: ${path}`,
433
- },
434
- ],
435
- isError: true,
436
- }
437
- }
438
- }
439
-
440
- /**
441
- * Search migration documentation
442
- */
443
- async searchMigrationDocs({ query }: { query: string }) {
444
- // First try local CLAUDE.md files
445
- const localDocs = await this.docsProvider.searchLocalDocs(query)
446
-
447
- // Then try hosted docs
448
- const hostedDocs = await this.docsProvider.searchDocs(query)
449
-
450
- const sections: string[] = []
451
-
452
- if (localDocs.content) {
453
- sections.push(`## Local Documentation\n\n${localDocs.content}`)
454
- }
455
-
456
- if (hostedDocs.content && hostedDocs.content !== 'No documentation found for this query.') {
457
- sections.push(`## Online Documentation\n\n${hostedDocs.content}`)
458
- }
459
-
460
- if (sections.length === 0) {
461
- return {
462
- content: [
463
- {
464
- type: 'text' as const,
465
- text: `No documentation found for "${query}".
466
-
467
- Try these searches:
468
- - "access control" - How to restrict access to data
469
- - "field types" - Available field types in OpenSaaS
470
- - "authentication" - Setting up auth with Better-auth
471
- - "hooks" - Data transformation and side effects
472
-
473
- Or visit: https://stack.opensaas.au/`,
474
- },
475
- ],
476
- }
477
- }
478
-
479
- return {
480
- content: [
481
- {
482
- type: 'text' as const,
483
- text: `# Documentation: ${query}\n\n${sections.join('\n\n---\n\n')}`,
484
- },
485
- ],
486
- }
487
- }
488
-
489
- /**
490
- * Get example code for a feature
491
- */
492
- async getExample({ feature }: { feature: string }) {
493
- const example = await this.docsProvider.getExampleConfig(feature)
494
-
495
- if (!example) {
496
- return {
497
- content: [
498
- {
499
- type: 'text' as const,
500
- text: `No example found for "${feature}".
501
-
502
- Available examples:
503
- - **blog-with-auth** - Blog with user authentication
504
- - **access-control** - Access control patterns
505
- - **relationships** - Model relationships
506
- - **hooks** - Data transformation hooks
507
- - **custom-fields** - Custom field types
508
-
509
- Use: \`opensaas_get_example({ feature: "example-name" })\``,
510
- },
511
- ],
512
- }
513
- }
514
-
515
- return {
516
- content: [
517
- {
518
- type: 'text' as const,
519
- text: `# Example: ${feature}
520
-
521
- ${example.description}
522
-
523
- \`\`\`typescript
524
- ${example.code}
525
- \`\`\`
526
-
527
- ${example.notes ? `\n## Notes\n\n${example.notes}` : ''}
528
-
529
- ---
530
-
531
- 📚 Full example at: ${example.sourcePath}`,
532
- },
533
- ],
534
- }
535
- }
536
-
537
- /**
538
- * Cleanup - clear wizard sessions and caches
539
- */
540
- cleanup() {
541
- this.wizardEngine.clearCompletedSessions()
542
- this.docsProvider.clearExpiredCache()
543
- }
544
- }