@opensaas/stack-cli 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +76 -0
  2. package/dist/commands/migrate.d.ts.map +1 -1
  3. package/dist/commands/migrate.js +94 -268
  4. package/dist/commands/migrate.js.map +1 -1
  5. package/package.json +7 -2
  6. package/.turbo/turbo-build.log +0 -4
  7. package/CHANGELOG.md +0 -462
  8. package/CLAUDE.md +0 -298
  9. package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
  10. package/src/commands/dev.test.ts +0 -215
  11. package/src/commands/dev.ts +0 -48
  12. package/src/commands/generate.test.ts +0 -282
  13. package/src/commands/generate.ts +0 -182
  14. package/src/commands/init.ts +0 -34
  15. package/src/commands/mcp.ts +0 -135
  16. package/src/commands/migrate.ts +0 -534
  17. package/src/generator/__snapshots__/context.test.ts.snap +0 -361
  18. package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
  19. package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
  20. package/src/generator/context.test.ts +0 -139
  21. package/src/generator/context.ts +0 -227
  22. package/src/generator/index.ts +0 -7
  23. package/src/generator/lists.test.ts +0 -335
  24. package/src/generator/lists.ts +0 -140
  25. package/src/generator/plugin-types.ts +0 -147
  26. package/src/generator/prisma-config.ts +0 -46
  27. package/src/generator/prisma-extensions.ts +0 -159
  28. package/src/generator/prisma.test.ts +0 -211
  29. package/src/generator/prisma.ts +0 -161
  30. package/src/generator/types.test.ts +0 -268
  31. package/src/generator/types.ts +0 -537
  32. package/src/index.ts +0 -46
  33. package/src/mcp/lib/documentation-provider.ts +0 -710
  34. package/src/mcp/lib/features/catalog.ts +0 -301
  35. package/src/mcp/lib/generators/feature-generator.ts +0 -598
  36. package/src/mcp/lib/types.ts +0 -89
  37. package/src/mcp/lib/wizards/migration-wizard.ts +0 -584
  38. package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
  39. package/src/mcp/server/index.ts +0 -361
  40. package/src/mcp/server/stack-mcp-server.ts +0 -544
  41. package/src/migration/generators/migration-generator.ts +0 -675
  42. package/src/migration/introspectors/index.ts +0 -12
  43. package/src/migration/introspectors/keystone-introspector.ts +0 -296
  44. package/src/migration/introspectors/nextjs-introspector.ts +0 -209
  45. package/src/migration/introspectors/prisma-introspector.ts +0 -233
  46. package/src/migration/types.ts +0 -92
  47. package/tests/introspectors/keystone-introspector.test.ts +0 -255
  48. package/tests/introspectors/nextjs-introspector.test.ts +0 -302
  49. package/tests/introspectors/prisma-introspector.test.ts +0 -268
  50. package/tests/migration-generator.test.ts +0 -592
  51. package/tests/migration-wizard.test.ts +0 -442
  52. package/tsconfig.json +0 -13
  53. package/tsconfig.tsbuildinfo +0 -1
  54. package/vitest.config.ts +0 -26
@@ -1,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
- }