@opensaas/stack-cli 0.1.6 → 0.3.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 (94) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +215 -0
  3. package/CLAUDE.md +60 -12
  4. package/dist/commands/generate.d.ts.map +1 -1
  5. package/dist/commands/generate.js +10 -1
  6. package/dist/commands/generate.js.map +1 -1
  7. package/dist/commands/mcp.d.ts +6 -0
  8. package/dist/commands/mcp.d.ts.map +1 -0
  9. package/dist/commands/mcp.js +116 -0
  10. package/dist/commands/mcp.js.map +1 -0
  11. package/dist/generator/context.d.ts.map +1 -1
  12. package/dist/generator/context.js +21 -3
  13. package/dist/generator/context.js.map +1 -1
  14. package/dist/generator/index.d.ts +3 -0
  15. package/dist/generator/index.d.ts.map +1 -1
  16. package/dist/generator/index.js +3 -0
  17. package/dist/generator/index.js.map +1 -1
  18. package/dist/generator/lists.d.ts +31 -0
  19. package/dist/generator/lists.d.ts.map +1 -0
  20. package/dist/generator/lists.js +91 -0
  21. package/dist/generator/lists.js.map +1 -0
  22. package/dist/generator/plugin-types.d.ts +10 -0
  23. package/dist/generator/plugin-types.d.ts.map +1 -0
  24. package/dist/generator/plugin-types.js +122 -0
  25. package/dist/generator/plugin-types.js.map +1 -0
  26. package/dist/generator/prisma-config.d.ts +17 -0
  27. package/dist/generator/prisma-config.d.ts.map +1 -0
  28. package/dist/generator/prisma-config.js +40 -0
  29. package/dist/generator/prisma-config.js.map +1 -0
  30. package/dist/generator/prisma.d.ts.map +1 -1
  31. package/dist/generator/prisma.js +1 -2
  32. package/dist/generator/prisma.js.map +1 -1
  33. package/dist/generator/types.d.ts.map +1 -1
  34. package/dist/generator/types.js +53 -1
  35. package/dist/generator/types.js.map +1 -1
  36. package/dist/index.js +3 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/lib/documentation-provider.d.ts +43 -0
  39. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -0
  40. package/dist/mcp/lib/documentation-provider.js +163 -0
  41. package/dist/mcp/lib/documentation-provider.js.map +1 -0
  42. package/dist/mcp/lib/features/catalog.d.ts +26 -0
  43. package/dist/mcp/lib/features/catalog.d.ts.map +1 -0
  44. package/dist/mcp/lib/features/catalog.js +291 -0
  45. package/dist/mcp/lib/features/catalog.js.map +1 -0
  46. package/dist/mcp/lib/generators/feature-generator.d.ts +35 -0
  47. package/dist/mcp/lib/generators/feature-generator.d.ts.map +1 -0
  48. package/dist/mcp/lib/generators/feature-generator.js +546 -0
  49. package/dist/mcp/lib/generators/feature-generator.js.map +1 -0
  50. package/dist/mcp/lib/types.d.ts +80 -0
  51. package/dist/mcp/lib/types.d.ts.map +1 -0
  52. package/dist/mcp/lib/types.js +5 -0
  53. package/dist/mcp/lib/types.js.map +1 -0
  54. package/dist/mcp/lib/wizards/wizard-engine.d.ts +71 -0
  55. package/dist/mcp/lib/wizards/wizard-engine.d.ts.map +1 -0
  56. package/dist/mcp/lib/wizards/wizard-engine.js +356 -0
  57. package/dist/mcp/lib/wizards/wizard-engine.js.map +1 -0
  58. package/dist/mcp/server/index.d.ts +8 -0
  59. package/dist/mcp/server/index.d.ts.map +1 -0
  60. package/dist/mcp/server/index.js +202 -0
  61. package/dist/mcp/server/index.js.map +1 -0
  62. package/dist/mcp/server/stack-mcp-server.d.ts +92 -0
  63. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -0
  64. package/dist/mcp/server/stack-mcp-server.js +265 -0
  65. package/dist/mcp/server/stack-mcp-server.js.map +1 -0
  66. package/package.json +9 -7
  67. package/src/commands/__snapshots__/generate.test.ts.snap +61 -21
  68. package/src/commands/dev.test.ts +0 -1
  69. package/src/commands/generate.test.ts +18 -8
  70. package/src/commands/generate.ts +12 -0
  71. package/src/commands/mcp.ts +135 -0
  72. package/src/generator/__snapshots__/context.test.ts.snap +8 -8
  73. package/src/generator/__snapshots__/prisma.test.ts.snap +8 -16
  74. package/src/generator/__snapshots__/types.test.ts.snap +605 -9
  75. package/src/generator/context.test.ts +13 -8
  76. package/src/generator/context.ts +21 -3
  77. package/src/generator/index.ts +3 -0
  78. package/src/generator/lists.test.ts +335 -0
  79. package/src/generator/lists.ts +102 -0
  80. package/src/generator/plugin-types.ts +147 -0
  81. package/src/generator/prisma-config.ts +46 -0
  82. package/src/generator/prisma.test.ts +0 -10
  83. package/src/generator/prisma.ts +1 -2
  84. package/src/generator/types.test.ts +0 -12
  85. package/src/generator/types.ts +56 -1
  86. package/src/index.ts +4 -0
  87. package/src/mcp/lib/documentation-provider.ts +203 -0
  88. package/src/mcp/lib/features/catalog.ts +301 -0
  89. package/src/mcp/lib/generators/feature-generator.ts +598 -0
  90. package/src/mcp/lib/types.ts +89 -0
  91. package/src/mcp/lib/wizards/wizard-engine.ts +427 -0
  92. package/src/mcp/server/index.ts +240 -0
  93. package/src/mcp/server/stack-mcp-server.ts +301 -0
  94. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,598 @@
1
+ /**
2
+ * Feature generator - Generates code, config, and documentation for features
3
+ */
4
+
5
+ import type { Feature, FeatureImplementation, GeneratedFile } from '../types.js'
6
+
7
+ export class FeatureGenerator {
8
+ constructor(
9
+ private feature: Feature,
10
+ private answers: Record<string, string | boolean | string[]>,
11
+ private followUpAnswers: Record<string, string | boolean | string[]>,
12
+ ) {}
13
+
14
+ /**
15
+ * Generate complete feature implementation
16
+ */
17
+ generate(): FeatureImplementation {
18
+ const featureType = this.feature.id
19
+
20
+ switch (featureType) {
21
+ case 'authentication':
22
+ return this.generateAuthentication()
23
+ case 'blog':
24
+ return this.generateBlog()
25
+ case 'comments':
26
+ return this.generateComments()
27
+ case 'file-upload':
28
+ return this.generateFileUpload()
29
+ case 'semantic-search':
30
+ return this.generateSemanticSearch()
31
+ default:
32
+ throw new Error(`Unknown feature type: ${featureType}`)
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Generate authentication feature
38
+ */
39
+ private generateAuthentication(): FeatureImplementation {
40
+ const authMethods = this.answers['auth-methods'] as string[]
41
+ const hasRoles = this.answers['user-roles'] as boolean
42
+ const roles = hasRoles
43
+ ? (this.followUpAnswers['user-roles_followup'] as string)
44
+ ?.split(',')
45
+ .map((r) => r.trim()) || ['admin', 'user']
46
+ : null
47
+ const userFields = (this.answers['user-fields'] as string[]) || []
48
+ const emailVerification = this.answers['email-verification'] as boolean
49
+
50
+ const hasOAuth = authMethods.some((m) => ['Google OAuth', 'GitHub OAuth'].includes(m))
51
+ const hasPassword = authMethods.includes('Email & Password')
52
+ const hasMagicLink = authMethods.includes('Magic Links')
53
+
54
+ // Build User list fields
55
+ const fields = ['email: text({ validation: { isRequired: true } })']
56
+
57
+ if (hasPassword) {
58
+ fields.push('password: password({ validation: { isRequired: true } })')
59
+ }
60
+
61
+ fields.push('name: text()')
62
+
63
+ if (hasRoles && roles) {
64
+ fields.push(
65
+ `role: select({ options: [${roles.map((r) => `'${r}'`).join(', ')}], defaultValue: '${roles[roles.length - 1]}' })`,
66
+ )
67
+ }
68
+
69
+ if (userFields.includes('Avatar')) {
70
+ fields.push('avatar: text()')
71
+ }
72
+ if (userFields.includes('Bio')) {
73
+ fields.push('bio: text({ ui: { displayMode: "textarea" } })')
74
+ }
75
+ if (userFields.includes('Phone')) {
76
+ fields.push('phone: text()')
77
+ }
78
+ if (userFields.includes('Location')) {
79
+ fields.push('location: text()')
80
+ }
81
+ if (userFields.includes('Website')) {
82
+ fields.push('website: text()')
83
+ }
84
+
85
+ // Config updates
86
+ const configUpdates = `import { config, list } from '@opensaas/stack-core';
87
+ import { text, password, select } from '@opensaas/stack-core/fields';
88
+ import { authPlugin } from '@opensaas/stack-auth';
89
+
90
+ export default config({
91
+ plugins: [
92
+ authPlugin({
93
+ emailAndPassword: { enabled: ${hasPassword} },
94
+ ${
95
+ hasOAuth
96
+ ? `oauth: {
97
+ google: { enabled: ${authMethods.includes('Google OAuth')} },
98
+ github: { enabled: ${authMethods.includes('GitHub OAuth')} },
99
+ },`
100
+ : ''
101
+ }
102
+ ${hasMagicLink ? `magicLink: { enabled: true },` : ''}
103
+ ${emailVerification ? `emailVerification: { enabled: true },` : ''}
104
+ sessionFields: ['userId', 'email', 'name'${hasRoles ? ", 'role'" : ''}],
105
+ }),
106
+ ],
107
+ db: {
108
+ provider: 'postgresql', // or 'sqlite'
109
+ url: process.env.DATABASE_URL,
110
+ },
111
+ lists: {
112
+ User: list({
113
+ fields: {
114
+ ${fields.join(',\n ')}
115
+ },
116
+ access: {
117
+ operation: {
118
+ query: () => true,
119
+ create: () => true, // Public sign-up
120
+ update: ({ session, item }) => session?.userId === item.id,
121
+ delete: ({ session }) => session?.role === 'admin',
122
+ },
123
+ },
124
+ }),
125
+ // Add your other lists here
126
+ },
127
+ });`
128
+
129
+ // Generated files
130
+ const files: GeneratedFile[] = []
131
+
132
+ // Sign-in page
133
+ if (hasPassword || hasOAuth) {
134
+ files.push({
135
+ path: 'app/sign-in/page.tsx',
136
+ language: 'tsx',
137
+ description: 'Sign-in page with form and OAuth buttons',
138
+ content: `import { SignInForm } from '@opensaas/stack-auth/ui';
139
+
140
+ export default function SignInPage() {
141
+ return (
142
+ <div className="min-h-screen flex items-center justify-center">
143
+ <div className="w-full max-w-md">
144
+ <h1 className="text-2xl font-bold mb-6">Sign In</h1>
145
+ <SignInForm
146
+ ${hasPassword ? 'emailAndPassword' : ''}
147
+ ${hasOAuth ? `oauth={[${authMethods.includes('Google OAuth') ? "'google'" : ''}${authMethods.includes('GitHub OAuth') ? ", 'github'" : ''}]}` : ''}
148
+ redirectTo="/dashboard"
149
+ />
150
+ </div>
151
+ </div>
152
+ );
153
+ }`,
154
+ })
155
+ }
156
+
157
+ // Sign-up page
158
+ if (hasPassword) {
159
+ files.push({
160
+ path: 'app/sign-up/page.tsx',
161
+ language: 'tsx',
162
+ description: 'Sign-up page with registration form',
163
+ content: `import { SignUpForm } from '@opensaas/stack-auth/ui';
164
+
165
+ export default function SignUpPage() {
166
+ return (
167
+ <div className="min-h-screen flex items-center justify-center">
168
+ <div className="w-full max-w-md">
169
+ <h1 className="text-2xl font-bold mb-6">Create Account</h1>
170
+ <SignUpForm
171
+ fields={['email', 'password', 'name']}
172
+ redirectTo="/dashboard"
173
+ ${emailVerification ? 'requireEmailVerification' : ''}
174
+ />
175
+ </div>
176
+ </div>
177
+ );
178
+ }`,
179
+ })
180
+ }
181
+
182
+ // Access control helpers
183
+ files.push({
184
+ path: 'lib/access-control.ts',
185
+ language: 'typescript',
186
+ description: 'Reusable access control functions',
187
+ content: `import type { AccessControl } from '@opensaas/stack-core';
188
+
189
+ export const isAuthenticated: AccessControl = ({ session }) => {
190
+ return !!session?.userId;
191
+ };
192
+
193
+ ${
194
+ hasRoles
195
+ ? `export const isAdmin: AccessControl = ({ session }) => {
196
+ return session?.role === 'admin';
197
+ };
198
+
199
+ export const isOwner: AccessControl = ({ session, item }) => {
200
+ return session?.userId === item.id;
201
+ };
202
+
203
+ export const isAdminOrOwner: AccessControl = ({ session, item }) => {
204
+ return session?.role === 'admin' || session?.userId === item.id;
205
+ };`
206
+ : ''
207
+ }
208
+
209
+ export const requireAuth: AccessControl = ({ session }) => {
210
+ if (!session?.userId) {
211
+ throw new Error('Authentication required');
212
+ }
213
+ return true;
214
+ };`,
215
+ })
216
+
217
+ // Environment variables
218
+ const envVars: Record<string, string> = {
219
+ DATABASE_URL: 'postgresql://user:password@localhost:5432/mydb',
220
+ BETTER_AUTH_SECRET: '<generate-with-openssl-rand-base64-32>',
221
+ BETTER_AUTH_URL: 'http://localhost:3000',
222
+ }
223
+
224
+ if (authMethods.includes('Google OAuth')) {
225
+ envVars.GOOGLE_CLIENT_ID = '<your-google-client-id>'
226
+ envVars.GOOGLE_CLIENT_SECRET = '<your-google-client-secret>'
227
+ }
228
+
229
+ if (authMethods.includes('GitHub OAuth')) {
230
+ envVars.GITHUB_CLIENT_ID = '<your-github-client-id>'
231
+ envVars.GITHUB_CLIENT_SECRET = '<your-github-client-secret>'
232
+ }
233
+
234
+ // Next steps
235
+ const nextSteps = [
236
+ 'Copy the config updates to your `opensaas.config.ts`',
237
+ 'Create the files shown above in your project',
238
+ 'Add environment variables to your `.env` file',
239
+ hasOAuth ? 'Set up OAuth applications in Google/GitHub developer consoles' : null,
240
+ 'Run `pnpm generate` to update Prisma schema',
241
+ 'Run `pnpm db:push` to update your database',
242
+ 'Start your dev server: `pnpm dev`',
243
+ `Visit http://localhost:3000/${hasPassword ? 'sign-up' : 'sign-in'} to test authentication`,
244
+ ].filter(Boolean) as string[]
245
+
246
+ // Dev guide section
247
+ const devGuideSection = `## Authentication Feature
248
+
249
+ This project uses Better-auth for authentication with the following configuration:
250
+
251
+ ${authMethods.map((m) => `- ${m}`).join('\n')}
252
+ ${hasRoles ? `\n**User Roles**: ${roles?.join(', ')}` : ''}
253
+
254
+ ### Access Control Helpers
255
+
256
+ Use these functions in your list configurations:
257
+
258
+ \`\`\`typescript
259
+ import { isAuthenticated${hasRoles ? ', isAdmin, isOwner' : ''} } from './lib/access-control';
260
+
261
+ // In your list config:
262
+ access: {
263
+ operation: {
264
+ query: () => true,
265
+ create: isAuthenticated,
266
+ update: isOwner,
267
+ delete: ${hasRoles ? 'isAdmin' : 'isOwner'},
268
+ }
269
+ }
270
+ \`\`\`
271
+
272
+ ### Protected Routes
273
+
274
+ To protect a route, check the session in your server components:
275
+
276
+ \`\`\`typescript
277
+ import { auth } from '@/lib/auth';
278
+
279
+ export default async function ProtectedPage() {
280
+ const session = await auth();
281
+
282
+ if (!session) {
283
+ redirect('/sign-in');
284
+ }
285
+
286
+ return <div>Protected content for {session.user.name}</div>;
287
+ }
288
+ \`\`\`
289
+
290
+ ### Getting the Current User
291
+
292
+ In server actions or API routes:
293
+
294
+ \`\`\`typescript
295
+ import { getContext } from '@/.opensaas/context';
296
+
297
+ const context = await getContext();
298
+ const currentUser = await context.db.user.findUnique({
299
+ where: { id: context.session?.userId }
300
+ });
301
+ \`\`\``
302
+
303
+ return {
304
+ configUpdates,
305
+ files,
306
+ instructions: nextSteps,
307
+ devGuideSection,
308
+ envVars,
309
+ nextSteps,
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Generate blog feature
315
+ */
316
+ private generateBlog(): FeatureImplementation {
317
+ const contentEditor = this.answers['content-editor'] as string
318
+ const hasStatus = this.answers['post-status'] as boolean
319
+ const taxonomy = (this.answers['taxonomy'] as string[]) || []
320
+ const postFields = (this.answers['post-fields'] as string[]) || []
321
+
322
+ const useTiptap = contentEditor === 'Rich text editor (Tiptap)'
323
+ const useMarkdown = contentEditor === 'Markdown'
324
+
325
+ // Build Post fields
326
+ const fields = [
327
+ 'title: text({ validation: { isRequired: true } })',
328
+ 'slug: text({ validation: { isRequired: true } })',
329
+ ]
330
+
331
+ if (useTiptap) {
332
+ fields.push('content: richText({ validation: { isRequired: true } })')
333
+ } else if (useMarkdown) {
334
+ fields.push(
335
+ 'content: text({ ui: { displayMode: "textarea" }, validation: { isRequired: true } })',
336
+ )
337
+ } else {
338
+ fields.push(
339
+ 'content: text({ ui: { displayMode: "textarea" }, validation: { isRequired: true } })',
340
+ )
341
+ }
342
+
343
+ fields.push('author: relationship({ ref: "User.posts" })')
344
+
345
+ if (hasStatus) {
346
+ fields.push("status: select({ options: ['draft', 'published'], defaultValue: 'draft' })")
347
+ fields.push('publishedAt: timestamp()')
348
+ }
349
+
350
+ if (postFields.includes('Featured image')) {
351
+ fields.push('featuredImage: text()')
352
+ }
353
+ if (postFields.includes('Excerpt/summary')) {
354
+ fields.push('excerpt: text({ ui: { displayMode: "textarea" } })')
355
+ }
356
+ if (postFields.includes('SEO metadata (title, description)')) {
357
+ fields.push('seoTitle: text()')
358
+ fields.push('seoDescription: text()')
359
+ }
360
+ if (postFields.includes('Reading time estimate')) {
361
+ fields.push('readingTime: integer()')
362
+ }
363
+
364
+ const configUpdates = `import { config, list } from '@opensaas/stack-core';
365
+ import { text, select, relationship, timestamp${useTiptap ? '' : ', integer'} } from '@opensaas/stack-core/fields';
366
+ ${useTiptap ? "import { richText } from '@opensaas/stack-tiptap/fields';" : ''}
367
+
368
+ export default config({
369
+ lists: {
370
+ Post: list({
371
+ fields: {
372
+ ${fields.join(',\n ')},
373
+ },
374
+ access: {
375
+ operation: {
376
+ query: ({ session }) => {
377
+ ${hasStatus ? "if (!session) return { status: { equals: 'published' } };" : ''}
378
+ return true;
379
+ },
380
+ create: ({ session }) => !!session?.userId,
381
+ update: ({ session, item }) => session?.userId === item.authorId,
382
+ delete: ({ session, item }) =>
383
+ session?.role === 'admin' || session?.userId === item.authorId,
384
+ },
385
+ },
386
+ hooks: {
387
+ ${
388
+ hasStatus
389
+ ? `resolveInput: async ({ resolvedData, operation }) => {
390
+ // Auto-set publishedAt when publishing
391
+ if (operation === 'update' && resolvedData.status === 'published' && !resolvedData.publishedAt) {
392
+ resolvedData.publishedAt = new Date();
393
+ }
394
+ return resolvedData;
395
+ },`
396
+ : ''
397
+ }
398
+ },
399
+ }),
400
+ ${
401
+ taxonomy.includes('Categories')
402
+ ? `Category: list({
403
+ fields: {
404
+ name: text({ validation: { isRequired: true } }),
405
+ slug: text({ validation: { isRequired: true } }),
406
+ posts: relationship({ ref: 'Post.category', many: true }),
407
+ },
408
+ }),`
409
+ : ''
410
+ }
411
+ ${
412
+ taxonomy.includes('Tags')
413
+ ? `Tag: list({
414
+ fields: {
415
+ name: text({ validation: { isRequired: true } }),
416
+ posts: relationship({ ref: 'Post.tags', many: true }),
417
+ },
418
+ }),`
419
+ : ''
420
+ }
421
+ },
422
+ });`
423
+
424
+ const files: GeneratedFile[] = []
425
+
426
+ // Blog list page
427
+ files.push({
428
+ path: 'app/blog/page.tsx',
429
+ language: 'tsx',
430
+ description: 'Blog listing page',
431
+ content: `import { getContext } from '@/.opensaas/context';
432
+ import Link from 'next/link';
433
+
434
+ export default async function BlogPage() {
435
+ const context = await getContext();
436
+
437
+ const posts = await context.db.post.findMany({
438
+ ${hasStatus ? "where: { status: 'published' }," : ''}
439
+ orderBy: { ${hasStatus ? 'publishedAt' : 'createdAt'}: 'desc' },
440
+ include: { author: true },
441
+ });
442
+
443
+ return (
444
+ <div className="container mx-auto py-8">
445
+ <h1 className="text-4xl font-bold mb-8">Blog</h1>
446
+ <div className="grid gap-6">
447
+ {posts.map((post) => (
448
+ <article key={post.id} className="border rounded-lg p-6">
449
+ <Link href={\`/blog/\${post.slug}\`}>
450
+ <h2 className="text-2xl font-bold hover:underline">
451
+ {post.title}
452
+ </h2>
453
+ </Link>
454
+ ${postFields.includes('Excerpt/summary') ? '<p className="mt-2 text-gray-600">{post.excerpt}</p>' : ''}
455
+ <div className="mt-4 text-sm text-gray-500">
456
+ By {post.author.name} · ${hasStatus ? '{post.publishedAt?.toLocaleDateString()}' : '{post.createdAt.toLocaleDateString()}'}
457
+ </div>
458
+ </article>
459
+ ))}
460
+ </div>
461
+ </div>
462
+ );
463
+ }`,
464
+ })
465
+
466
+ // Blog post page
467
+ files.push({
468
+ path: 'app/blog/[slug]/page.tsx',
469
+ language: 'tsx',
470
+ description: 'Individual blog post page',
471
+ content: `import { getContext } from '@/.opensaas/context';
472
+ import { notFound } from 'next/navigation';
473
+
474
+ export default async function BlogPostPage({
475
+ params,
476
+ }: {
477
+ params: { slug: string };
478
+ }) {
479
+ const context = await getContext();
480
+
481
+ const post = await context.db.post.findFirst({
482
+ where: {
483
+ slug: params.slug,
484
+ ${hasStatus ? "status: 'published'," : ''}
485
+ },
486
+ include: { author: true },
487
+ });
488
+
489
+ if (!post) {
490
+ notFound();
491
+ }
492
+
493
+ return (
494
+ <article className="container mx-auto py-8 max-w-3xl">
495
+ <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
496
+ <div className="text-gray-600 mb-8">
497
+ By {post.author.name} · ${hasStatus ? '{post.publishedAt?.toLocaleDateString()}' : '{post.createdAt.toLocaleDateString()}'}
498
+ </div>
499
+ ${useTiptap ? '<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: post.content }} />' : useMarkdown ? '<div className="prose max-w-none">{/* Render markdown here */}{post.content}</div>' : '<div className="prose max-w-none whitespace-pre-wrap">{post.content}</div>'}
500
+ </article>
501
+ );
502
+ }`,
503
+ })
504
+
505
+ const nextSteps = [
506
+ 'Copy the config updates to your `opensaas.config.ts`',
507
+ 'Add the `posts` relationship field to your User list',
508
+ useTiptap ? 'Install Tiptap package: `pnpm add @opensaas/stack-tiptap`' : null,
509
+ 'Create the blog pages in your `app/` directory',
510
+ 'Run `pnpm generate` to update Prisma schema',
511
+ 'Run `pnpm db:push` to update database',
512
+ 'Create your first blog post in the admin UI',
513
+ ].filter(Boolean) as string[]
514
+
515
+ const devGuideSection = `## Blog Feature
516
+
517
+ This project includes a blog system with:
518
+
519
+ - ${contentEditor} for writing posts
520
+ ${hasStatus ? '- Draft/publish workflow' : ''}
521
+ ${taxonomy.length > 0 ? `- ${taxonomy.join(' and ')} for organization` : ''}
522
+
523
+ ### Creating a Post
524
+
525
+ ${
526
+ hasStatus
527
+ ? `Posts start as drafts and can be published when ready:
528
+
529
+ \`\`\`typescript
530
+ const post = await context.db.post.create({
531
+ data: {
532
+ title: 'My Post',
533
+ slug: 'my-post',
534
+ content: '...',
535
+ authorId: session.userId,
536
+ status: 'draft', // or 'published'
537
+ }
538
+ });
539
+ \`\`\``
540
+ : ''
541
+ }
542
+
543
+ ### Access Control
544
+
545
+ - Anyone can read ${hasStatus ? 'published posts' : 'posts'}
546
+ - Only authenticated users can create posts
547
+ - Only authors can update their own posts
548
+ - Admins and authors can delete posts`
549
+
550
+ return {
551
+ configUpdates,
552
+ files,
553
+ instructions: nextSteps,
554
+ devGuideSection,
555
+ envVars: useTiptap ? {} : undefined,
556
+ nextSteps,
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Generate comments feature (stub - to be implemented)
562
+ */
563
+ private generateComments(): FeatureImplementation {
564
+ return {
565
+ configUpdates: '// Comments feature implementation coming soon',
566
+ files: [],
567
+ instructions: ['Feature implementation in progress'],
568
+ devGuideSection: '## Comments Feature\n\nComing soon...',
569
+ nextSteps: ['Feature implementation in progress'],
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Generate file upload feature (stub - to be implemented)
575
+ */
576
+ private generateFileUpload(): FeatureImplementation {
577
+ return {
578
+ configUpdates: '// File upload feature implementation coming soon',
579
+ files: [],
580
+ instructions: ['Feature implementation in progress'],
581
+ devGuideSection: '## File Upload Feature\n\nComing soon...',
582
+ nextSteps: ['Feature implementation in progress'],
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Generate semantic search feature (stub - to be implemented)
588
+ */
589
+ private generateSemanticSearch(): FeatureImplementation {
590
+ return {
591
+ configUpdates: '// Semantic search feature implementation coming soon',
592
+ files: [],
593
+ instructions: ['Feature implementation in progress'],
594
+ devGuideSection: '## Semantic Search Feature\n\nComing soon...',
595
+ nextSteps: ['Feature implementation in progress'],
596
+ }
597
+ }
598
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Feature catalog types for OpenSaaS Stack MCP server
3
+ */
4
+
5
+ export type QuestionType = 'text' | 'textarea' | 'select' | 'multiselect' | 'boolean'
6
+
7
+ export interface FeatureQuestion {
8
+ id: string
9
+ text: string
10
+ type: QuestionType
11
+ required?: boolean
12
+ options?: string[]
13
+ defaultValue?: string | boolean | string[]
14
+ dependsOn?: {
15
+ questionId: string
16
+ value: string | boolean
17
+ }
18
+ followUp?: {
19
+ if: string | boolean
20
+ ask: string
21
+ type: QuestionType
22
+ options?: string[]
23
+ }
24
+ }
25
+
26
+ export interface Feature {
27
+ id: string
28
+ name: string
29
+ description: string
30
+ includes: string[]
31
+ dependsOn?: string[] // Other feature IDs required
32
+ questions: FeatureQuestion[]
33
+ category: 'authentication' | 'content' | 'storage' | 'search' | 'custom'
34
+ }
35
+
36
+ export interface WizardSession {
37
+ id: string
38
+ featureId: string
39
+ feature: Feature
40
+ currentQuestionIndex: number
41
+ answers: Record<string, string | boolean | string[]>
42
+ followUpAnswers: Record<string, string | boolean | string[]>
43
+ isComplete: boolean
44
+ createdAt: Date
45
+ updatedAt: Date
46
+ }
47
+
48
+ export interface SessionStorage {
49
+ [sessionId: string]: WizardSession
50
+ }
51
+
52
+ export interface GeneratedFile {
53
+ path: string
54
+ content: string
55
+ language: string
56
+ description: string
57
+ }
58
+
59
+ export interface FeatureImplementation {
60
+ configUpdates: string
61
+ files: GeneratedFile[]
62
+ instructions: string[]
63
+ devGuideSection: string
64
+ envVars?: Record<string, string>
65
+ nextSteps: string[]
66
+ }
67
+
68
+ export interface DocumentationLookup {
69
+ topic: string
70
+ content: string
71
+ url: string
72
+ codeExamples: string[]
73
+ relatedTopics: string[]
74
+ }
75
+
76
+ export interface ValidationError {
77
+ message: string
78
+ location: string
79
+ suggestion: string
80
+ before?: string
81
+ after?: string
82
+ }
83
+
84
+ export interface ValidationResult {
85
+ valid: boolean
86
+ errors: ValidationError[]
87
+ warnings: string[]
88
+ suggestions: string[]
89
+ }