@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,147 @@
1
+ import type { OpenSaasConfig } from '@opensaas/stack-core'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+
5
+ /**
6
+ * Generate TypeScript declaration for plugin data types
7
+ * Creates type-safe access to config._pluginData
8
+ */
9
+ function generatePluginDataInterface(config: OpenSaasConfig): string {
10
+ const lines: string[] = []
11
+
12
+ lines.push('/**')
13
+ lines.push(' * Plugin data storage types')
14
+ lines.push(' * Provides type-safe access to config._pluginData')
15
+ lines.push(' */')
16
+ lines.push('export interface PluginData {')
17
+
18
+ // Check if we have plugin data types to generate
19
+ if (config._pluginData && Object.keys(config._pluginData).length > 0) {
20
+ for (const pluginName of Object.keys(config._pluginData)) {
21
+ // Skip internal keys
22
+ if (pluginName.startsWith('__')) continue
23
+
24
+ // For each plugin, we'll add a Record<string, unknown> entry
25
+ // TODO: In the future, plugins could export their data types for proper typing
26
+ lines.push(` ${pluginName}?: Record<string, unknown>`)
27
+ }
28
+ }
29
+
30
+ lines.push('}')
31
+
32
+ return lines.join('\n')
33
+ }
34
+
35
+ /**
36
+ * Generate TypeScript declaration for plugin runtime services
37
+ * Creates type-safe access to context.plugins
38
+ */
39
+ function generatePluginServicesInterface(config: OpenSaasConfig): string {
40
+ const lines: string[] = []
41
+ const imports: string[] = []
42
+
43
+ // Check if we have plugins with runtime functions
44
+ const pluginsWithRuntime = (config._plugins || config.plugins || []).filter((p) => p.runtime)
45
+
46
+ // Collect imports from plugins that provide runtime service types
47
+ if (pluginsWithRuntime.length > 0) {
48
+ for (const plugin of pluginsWithRuntime) {
49
+ if (plugin.runtimeServiceTypes) {
50
+ imports.push(plugin.runtimeServiceTypes.import)
51
+ }
52
+ }
53
+ }
54
+
55
+ // Add imports at the top if any exist
56
+ if (imports.length > 0) {
57
+ lines.push(...imports)
58
+ lines.push('')
59
+ }
60
+
61
+ lines.push('/**')
62
+ lines.push(' * Plugin runtime services')
63
+ lines.push(' * Provides type-safe access to context.plugins')
64
+ lines.push(' * Extends Record to allow compatibility with base AccessContext type')
65
+ lines.push(' */')
66
+ lines.push(
67
+ 'export interface PluginServices extends Record<string, Record<string, any> | undefined> {',
68
+ )
69
+
70
+ if (pluginsWithRuntime.length > 0) {
71
+ for (const plugin of pluginsWithRuntime) {
72
+ if (plugin.runtimeServiceTypes) {
73
+ // Use typed runtime service from plugin
74
+ lines.push(` ${plugin.name}?: ${plugin.runtimeServiceTypes.typeName}`)
75
+ } else {
76
+ // Fallback to Record<string, any> for plugins without type metadata
77
+ lines.push(` ${plugin.name}?: Record<string, any>`)
78
+ }
79
+ }
80
+ }
81
+
82
+ lines.push('}')
83
+
84
+ return lines.join('\n')
85
+ }
86
+
87
+ /**
88
+ * Generate module augmentation for OpenSaaS core types
89
+ * Note: We cannot augment _pluginData or plugins properties directly due to type constraints
90
+ * Instead, users should cast to PluginServices when accessing context.plugins
91
+ */
92
+ function generateModuleAugmentation(): string {
93
+ const lines: string[] = []
94
+
95
+ lines.push('')
96
+ lines.push('/**')
97
+ lines.push(' * Declare this module to make interfaces available globally')
98
+ lines.push(' * Import this file to get typed plugin access')
99
+ lines.push(' */')
100
+ lines.push('declare global {')
101
+ lines.push(' // Plugin types are available as PluginServices interface')
102
+ lines.push('}')
103
+
104
+ return lines.join('\n')
105
+ }
106
+
107
+ /**
108
+ * Generate complete plugin types file
109
+ */
110
+ export function generatePluginTypes(config: OpenSaasConfig): string {
111
+ const lines: string[] = []
112
+
113
+ // Add header comment
114
+ lines.push('/**')
115
+ lines.push(' * Generated plugin types from OpenSaas configuration')
116
+ lines.push(' * DO NOT EDIT - This file is automatically generated')
117
+ lines.push(' *')
118
+ lines.push(' * This file provides type-safe access to:')
119
+ lines.push(' * - config._pluginData - Plugin configuration data')
120
+ lines.push(' * - context.plugins - Plugin runtime services')
121
+ lines.push(' */')
122
+ lines.push('')
123
+
124
+ // Generate interfaces
125
+ lines.push(generatePluginDataInterface(config))
126
+ lines.push(generatePluginServicesInterface(config))
127
+
128
+ // Generate module augmentation
129
+ lines.push(generateModuleAugmentation())
130
+
131
+ return lines.join('\n')
132
+ }
133
+
134
+ /**
135
+ * Write plugin types to file
136
+ */
137
+ export function writePluginTypes(config: OpenSaasConfig, outputPath: string): void {
138
+ const pluginTypes = generatePluginTypes(config)
139
+
140
+ // Ensure directory exists
141
+ const dir = path.dirname(outputPath)
142
+ if (!fs.existsSync(dir)) {
143
+ fs.mkdirSync(dir, { recursive: true })
144
+ }
145
+
146
+ fs.writeFileSync(outputPath, pluginTypes, 'utf-8')
147
+ }
@@ -0,0 +1,46 @@
1
+ import type { OpenSaasConfig } from '@opensaas/stack-core'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+
5
+ /**
6
+ * Generate Prisma config file for CLI commands
7
+ *
8
+ * Prisma 7 requires a prisma.config.ts file at the project root for CLI commands
9
+ * like `prisma db push` and `prisma migrate dev`. This is separate from the
10
+ * runtime configuration (which uses adapters in opensaas.config.ts).
11
+ *
12
+ * The CLI config provides the database URL for schema operations, while the
13
+ * runtime config provides adapters for actual query execution.
14
+ */
15
+ export function generatePrismaConfig(_config: OpenSaasConfig): string {
16
+ const lines: string[] = []
17
+
18
+ // Import dotenv for environment variable loading
19
+ lines.push("import 'dotenv/config'")
20
+ lines.push("import { defineConfig, env } from 'prisma/config'")
21
+ lines.push('')
22
+ lines.push('export default defineConfig({')
23
+ lines.push(" schema: 'prisma/schema.prisma',")
24
+ lines.push(' datasource: {')
25
+ lines.push(" url: env('DATABASE_URL'),")
26
+ lines.push(' },')
27
+ lines.push('})')
28
+ lines.push('')
29
+
30
+ return lines.join('\n')
31
+ }
32
+
33
+ /**
34
+ * Write Prisma config to file
35
+ */
36
+ export function writePrismaConfig(config: OpenSaasConfig, outputPath: string): void {
37
+ const prismaConfig = generatePrismaConfig(config)
38
+
39
+ // Ensure directory exists
40
+ const dir = path.dirname(outputPath)
41
+ if (!fs.existsSync(dir)) {
42
+ fs.mkdirSync(dir, { recursive: true })
43
+ }
44
+
45
+ fs.writeFileSync(outputPath, prismaConfig, 'utf-8')
46
+ }
@@ -9,7 +9,6 @@ describe('Prisma Schema Generator', () => {
9
9
  const config: OpenSaasConfig = {
10
10
  db: {
11
11
  provider: 'sqlite',
12
- url: 'file:./dev.db',
13
12
  },
14
13
  lists: {},
15
14
  }
@@ -23,7 +22,6 @@ describe('Prisma Schema Generator', () => {
23
22
  const config: OpenSaasConfig = {
24
23
  db: {
25
24
  provider: 'sqlite',
26
- url: 'file:./dev.db',
27
25
  },
28
26
  opensaasPath: '.custom-path',
29
27
  lists: {},
@@ -38,7 +36,6 @@ describe('Prisma Schema Generator', () => {
38
36
  const config: OpenSaasConfig = {
39
37
  db: {
40
38
  provider: 'sqlite',
41
- url: 'file:./dev.db',
42
39
  },
43
40
  lists: {
44
41
  User: {
@@ -60,7 +57,6 @@ describe('Prisma Schema Generator', () => {
60
57
  const config: OpenSaasConfig = {
61
58
  db: {
62
59
  provider: 'sqlite',
63
- url: 'file:./dev.db',
64
60
  },
65
61
  lists: {
66
62
  Post: {
@@ -81,7 +77,6 @@ describe('Prisma Schema Generator', () => {
81
77
  const config: OpenSaasConfig = {
82
78
  db: {
83
79
  provider: 'sqlite',
84
- url: 'file:./dev.db',
85
80
  },
86
81
  lists: {
87
82
  Post: {
@@ -102,7 +97,6 @@ describe('Prisma Schema Generator', () => {
102
97
  const config: OpenSaasConfig = {
103
98
  db: {
104
99
  provider: 'sqlite',
105
- url: 'file:./dev.db',
106
100
  },
107
101
  lists: {
108
102
  User: {
@@ -128,7 +122,6 @@ describe('Prisma Schema Generator', () => {
128
122
  const config: OpenSaasConfig = {
129
123
  db: {
130
124
  provider: 'sqlite',
131
- url: 'file:./dev.db',
132
125
  },
133
126
  lists: {
134
127
  User: {
@@ -154,7 +147,6 @@ describe('Prisma Schema Generator', () => {
154
147
  const config: OpenSaasConfig = {
155
148
  db: {
156
149
  provider: 'postgresql',
157
- url: process.env.DATABASE_URL || 'postgresql://localhost:5432/db',
158
150
  },
159
151
  lists: {
160
152
  User: {
@@ -184,7 +176,6 @@ describe('Prisma Schema Generator', () => {
184
176
  const config: OpenSaasConfig = {
185
177
  db: {
186
178
  provider: 'sqlite',
187
- url: 'file:./dev.db',
188
179
  },
189
180
  lists: {
190
181
  User: {
@@ -206,7 +197,6 @@ describe('Prisma Schema Generator', () => {
206
197
  const config: OpenSaasConfig = {
207
198
  db: {
208
199
  provider: 'sqlite',
209
- url: 'file:./dev.db',
210
200
  },
211
201
  lists: {},
212
202
  }
@@ -66,13 +66,12 @@ export function generatePrismaSchema(config: OpenSaasConfig): string {
66
66
 
67
67
  // Generator and datasource
68
68
  lines.push('generator client {')
69
- lines.push(' provider = "prisma-client-js"')
69
+ lines.push(' provider = "prisma-client"')
70
70
  lines.push(` output = "../${opensaasPath}/prisma-client"`)
71
71
  lines.push('}')
72
72
  lines.push('')
73
73
  lines.push('datasource db {')
74
74
  lines.push(` provider = "${config.db.provider}"`)
75
- lines.push(' url = env("DATABASE_URL")')
76
75
  lines.push('}')
77
76
  lines.push('')
78
77
 
@@ -9,7 +9,6 @@ describe('Types Generator', () => {
9
9
  const config: OpenSaasConfig = {
10
10
  db: {
11
11
  provider: 'sqlite',
12
- url: 'file:./dev.db',
13
12
  },
14
13
  lists: {
15
14
  User: {
@@ -30,7 +29,6 @@ describe('Types Generator', () => {
30
29
  const config: OpenSaasConfig = {
31
30
  db: {
32
31
  provider: 'sqlite',
33
- url: 'file:./dev.db',
34
32
  },
35
33
  lists: {
36
34
  Post: {
@@ -51,7 +49,6 @@ describe('Types Generator', () => {
51
49
  const config: OpenSaasConfig = {
52
50
  db: {
53
51
  provider: 'sqlite',
54
- url: 'file:./dev.db',
55
52
  },
56
53
  lists: {
57
54
  Post: {
@@ -72,7 +69,6 @@ describe('Types Generator', () => {
72
69
  const config: OpenSaasConfig = {
73
70
  db: {
74
71
  provider: 'sqlite',
75
- url: 'file:./dev.db',
76
72
  },
77
73
  lists: {
78
74
  User: {
@@ -92,7 +88,6 @@ describe('Types Generator', () => {
92
88
  const config: OpenSaasConfig = {
93
89
  db: {
94
90
  provider: 'sqlite',
95
- url: 'file:./dev.db',
96
91
  },
97
92
  lists: {
98
93
  User: {
@@ -112,7 +107,6 @@ describe('Types Generator', () => {
112
107
  const config: OpenSaasConfig = {
113
108
  db: {
114
109
  provider: 'sqlite',
115
- url: 'file:./dev.db',
116
110
  },
117
111
  lists: {
118
112
  User: {
@@ -139,7 +133,6 @@ describe('Types Generator', () => {
139
133
  const config: OpenSaasConfig = {
140
134
  db: {
141
135
  provider: 'sqlite',
142
- url: 'file:./dev.db',
143
136
  },
144
137
  lists: {
145
138
  Post: {
@@ -165,7 +158,6 @@ describe('Types Generator', () => {
165
158
  const config: OpenSaasConfig = {
166
159
  db: {
167
160
  provider: 'sqlite',
168
- url: 'file:./dev.db',
169
161
  },
170
162
  lists: {
171
163
  Post: {
@@ -191,7 +183,6 @@ describe('Types Generator', () => {
191
183
  const config: OpenSaasConfig = {
192
184
  db: {
193
185
  provider: 'sqlite',
194
- url: 'file:./dev.db',
195
186
  },
196
187
  lists: {
197
188
  User: {
@@ -221,7 +212,6 @@ describe('Types Generator', () => {
221
212
  const config: OpenSaasConfig = {
222
213
  db: {
223
214
  provider: 'sqlite',
224
- url: 'file:./dev.db',
225
215
  },
226
216
  lists: {
227
217
  Product: {
@@ -243,7 +233,6 @@ describe('Types Generator', () => {
243
233
  const config: OpenSaasConfig = {
244
234
  db: {
245
235
  provider: 'sqlite',
246
- url: 'file:./dev.db',
247
236
  },
248
237
  lists: {
249
238
  Post: {
@@ -265,7 +254,6 @@ describe('Types Generator', () => {
265
254
  const config: OpenSaasConfig = {
266
255
  db: {
267
256
  provider: 'sqlite',
268
- url: 'file:./dev.db',
269
257
  },
270
258
  lists: {},
271
259
  }
@@ -169,6 +169,55 @@ function generateWhereInputType(listName: string, fields: Record<string, FieldCo
169
169
  return lines.join('\n')
170
170
  }
171
171
 
172
+ /**
173
+ * Generate hook types that reference Prisma input types
174
+ */
175
+ function generateHookTypes(listName: string): string {
176
+ const lines: string[] = []
177
+
178
+ lines.push(`/**`)
179
+ lines.push(` * Hook types for ${listName} list`)
180
+ lines.push(` * Properly typed to use Prisma's generated input types`)
181
+ lines.push(` */`)
182
+ lines.push(`export type ${listName}Hooks = {`)
183
+ lines.push(` resolveInput?: (args:`)
184
+ lines.push(` | {`)
185
+ lines.push(` operation: 'create'`)
186
+ lines.push(` resolvedData: Prisma.${listName}CreateInput`)
187
+ lines.push(` item: undefined`)
188
+ lines.push(` context: import('@opensaas/stack-core').AccessContext`)
189
+ lines.push(` }`)
190
+ lines.push(` | {`)
191
+ lines.push(` operation: 'update'`)
192
+ lines.push(` resolvedData: Prisma.${listName}UpdateInput`)
193
+ lines.push(` item: ${listName}`)
194
+ lines.push(` context: import('@opensaas/stack-core').AccessContext`)
195
+ lines.push(` }`)
196
+ lines.push(` ) => Promise<Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput>`)
197
+ lines.push(` validateInput?: (args: {`)
198
+ lines.push(` operation: 'create' | 'update'`)
199
+ lines.push(` resolvedData: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
200
+ lines.push(` item?: ${listName}`)
201
+ lines.push(` context: import('@opensaas/stack-core').AccessContext`)
202
+ lines.push(` addValidationError: (msg: string) => void`)
203
+ lines.push(` }) => Promise<void>`)
204
+ lines.push(` beforeOperation?: (args: {`)
205
+ lines.push(` operation: 'create' | 'update' | 'delete'`)
206
+ lines.push(` resolvedData?: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
207
+ lines.push(` item?: ${listName}`)
208
+ lines.push(` context: import('@opensaas/stack-core').AccessContext`)
209
+ lines.push(` }) => Promise<void>`)
210
+ lines.push(` afterOperation?: (args: {`)
211
+ lines.push(` operation: 'create' | 'update' | 'delete'`)
212
+ lines.push(` resolvedData?: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
213
+ lines.push(` item?: ${listName}`)
214
+ lines.push(` context: import('@opensaas/stack-core').AccessContext`)
215
+ lines.push(` }) => Promise<void>`)
216
+ lines.push(`}`)
217
+
218
+ return lines.join('\n')
219
+ }
220
+
172
221
  /**
173
222
  * Generate Context type with all operations
174
223
  */
@@ -180,7 +229,10 @@ function generateContextType(): string {
180
229
  lines.push(' session: TSession')
181
230
  lines.push(' prisma: PrismaClient')
182
231
  lines.push(' storage: StorageUtils')
232
+ lines.push(' plugins: PluginServices')
183
233
  lines.push(' serverAction: (props: ServerActionProps) => Promise<unknown>')
234
+ lines.push(' sudo: () => Context<TSession>')
235
+ lines.push(' _isSudo: boolean')
184
236
  lines.push('}')
185
237
 
186
238
  return lines.join('\n')
@@ -249,7 +301,8 @@ export function generateTypes(config: OpenSaasConfig): string {
249
301
  lines.push(
250
302
  "import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB } from '@opensaas/stack-core'",
251
303
  )
252
- lines.push("import type { PrismaClient } from './prisma-client'")
304
+ lines.push("import type { PrismaClient, Prisma } from './prisma-client/client'")
305
+ lines.push("import type { PluginServices } from './plugin-types'")
253
306
 
254
307
  // Add field-specific imports
255
308
  const fieldImports = collectFieldImports(config)
@@ -271,6 +324,8 @@ export function generateTypes(config: OpenSaasConfig): string {
271
324
  lines.push('')
272
325
  lines.push(generateWhereInputType(listName, listConfig.fields))
273
326
  lines.push('')
327
+ lines.push(generateHookTypes(listName))
328
+ lines.push('')
274
329
  }
275
330
 
276
331
  // Generate Context type
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { Command } from 'commander'
4
4
  import { generateCommand } from './commands/generate.js'
5
5
  import { initCommand } from './commands/init.js'
6
6
  import { devCommand } from './commands/dev.js'
7
+ import { createMCPCommand } from './commands/mcp.js'
7
8
 
8
9
  const program = new Command()
9
10
 
@@ -35,4 +36,7 @@ program
35
36
  await devCommand()
36
37
  })
37
38
 
39
+ // Add MCP command group
40
+ program.addCommand(createMCPCommand())
41
+
38
42
  program.parse()
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Documentation provider - Fetches documentation from the hosted docs site
3
+ */
4
+
5
+ import type { DocumentationLookup } from './types.js'
6
+
7
+ interface SearchResult {
8
+ content: string
9
+ metadata: {
10
+ title?: string
11
+ slug?: string
12
+ section?: string
13
+ }
14
+ score: number
15
+ }
16
+
17
+ interface SearchResponse {
18
+ results: SearchResult[]
19
+ query: string
20
+ count: number
21
+ }
22
+
23
+ export class OpenSaasDocumentationProvider {
24
+ private readonly DOCS_API = 'https://stack.opensaas.au/api/search'
25
+ private cache = new Map<string, { data: DocumentationLookup; timestamp: number }>()
26
+ private readonly CACHE_TTL = 1000 * 60 * 30 // 30 minutes
27
+
28
+ // Topic mappings for user-friendly queries
29
+ private topicMappings: Record<string, string> = {
30
+ fields: 'field-types',
31
+ 'field types': 'field-types',
32
+ 'field type': 'field-types',
33
+ access: 'access-control',
34
+ 'access control': 'access-control',
35
+ permissions: 'access-control',
36
+ auth: 'authentication',
37
+ authentication: 'authentication',
38
+ login: 'authentication',
39
+ 'sign in': 'authentication',
40
+ hooks: 'hooks',
41
+ hook: 'hooks',
42
+ lifecycle: 'hooks',
43
+ plugins: 'plugin-system',
44
+ plugin: 'plugin-system',
45
+ rag: 'rag',
46
+ search: 'semantic-search',
47
+ 'semantic search': 'semantic-search',
48
+ storage: 'file-storage',
49
+ files: 'file-storage',
50
+ upload: 'file-storage',
51
+ config: 'configuration',
52
+ configuration: 'configuration',
53
+ prisma: 'prisma-integration',
54
+ database: 'database-setup',
55
+ deployment: 'deployment',
56
+ deploy: 'deployment',
57
+ }
58
+
59
+ /**
60
+ * Search documentation by query
61
+ */
62
+ async searchDocs(query: string, limit = 5, minScore = 0.7): Promise<DocumentationLookup> {
63
+ const cacheKey = `search:${query}:${limit}:${minScore}`
64
+
65
+ // Check cache
66
+ const cached = this.cache.get(cacheKey)
67
+ if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
68
+ return cached.data
69
+ }
70
+
71
+ try {
72
+ const response = await fetch(this.DOCS_API, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ body: JSON.stringify({ query, limit, minScore }),
78
+ })
79
+
80
+ if (!response.ok) {
81
+ throw new Error(`Docs API error: ${response.statusText}`)
82
+ }
83
+
84
+ const data = (await response.json()) as SearchResponse
85
+
86
+ const docLookup: DocumentationLookup = {
87
+ topic: query,
88
+ content: this.formatSearchResults(data.results),
89
+ url: 'https://stack.opensaas.au/',
90
+ codeExamples: this.extractCodeExamples(data.results),
91
+ relatedTopics: this.extractRelatedTopics(data.results),
92
+ }
93
+
94
+ // Cache the result
95
+ this.cache.set(cacheKey, { data: docLookup, timestamp: Date.now() })
96
+
97
+ return docLookup
98
+ } catch (error) {
99
+ console.error('Error fetching documentation:', error)
100
+ return this.getFallbackDocs(query)
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get documentation for a specific topic
106
+ */
107
+ async getTopicDocs(topic: string): Promise<DocumentationLookup> {
108
+ // Normalize topic using mappings
109
+ const normalizedTopic = this.topicMappings[topic.toLowerCase()] || topic
110
+
111
+ return this.searchDocs(normalizedTopic, 3, 0.8)
112
+ }
113
+
114
+ /**
115
+ * Format search results into readable content
116
+ */
117
+ private formatSearchResults(results: SearchResult[]): string {
118
+ if (results.length === 0) {
119
+ return 'No documentation found for this query.'
120
+ }
121
+
122
+ return results
123
+ .map((result, index) => {
124
+ const title = result.metadata.title || `Section ${index + 1}`
125
+ const section = result.metadata.section || ''
126
+ const score = (result.score * 100).toFixed(0)
127
+
128
+ return `### ${title}${section ? ` (${section})` : ''} [Relevance: ${score}%]\n\n${result.content}\n`
129
+ })
130
+ .join('\n---\n\n')
131
+ }
132
+
133
+ /**
134
+ * Extract code examples from search results
135
+ */
136
+ private extractCodeExamples(results: SearchResult[]): string[] {
137
+ const codeExamples: string[] = []
138
+ const codeBlockRegex = /```[\s\S]*?```/g
139
+
140
+ for (const result of results) {
141
+ const matches = result.content.match(codeBlockRegex)
142
+ if (matches) {
143
+ codeExamples.push(...matches)
144
+ }
145
+ }
146
+
147
+ return codeExamples
148
+ }
149
+
150
+ /**
151
+ * Extract related topics from search results
152
+ */
153
+ private extractRelatedTopics(results: SearchResult[]): string[] {
154
+ const topics = new Set<string>()
155
+
156
+ for (const result of results) {
157
+ if (result.metadata.section) {
158
+ topics.add(result.metadata.section)
159
+ }
160
+ }
161
+
162
+ return Array.from(topics)
163
+ }
164
+
165
+ /**
166
+ * Fallback documentation when API is unavailable
167
+ */
168
+ private getFallbackDocs(query: string): DocumentationLookup {
169
+ return {
170
+ topic: query,
171
+ content: `Unable to fetch documentation from the docs site at this time.
172
+
173
+ Please visit the OpenSaaS Stack documentation directly:
174
+ https://stack.opensaas.au/
175
+
176
+ For ${query}, you can also check:
177
+ - GitHub repository: https://github.com/OpenSaasAU/stack
178
+ - Example projects in the examples/ directory`,
179
+ url: 'https://stack.opensaas.au/',
180
+ codeExamples: [],
181
+ relatedTopics: [],
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Clear expired cache entries
187
+ */
188
+ clearExpiredCache(): void {
189
+ const now = Date.now()
190
+ for (const [key, value] of this.cache.entries()) {
191
+ if (now - value.timestamp >= this.CACHE_TTL) {
192
+ this.cache.delete(key)
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Clear all cache
199
+ */
200
+ clearCache(): void {
201
+ this.cache.clear()
202
+ }
203
+ }