@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +215 -0
- package/CLAUDE.md +60 -12
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +10 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +116 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/generator/context.d.ts.map +1 -1
- package/dist/generator/context.js +21 -3
- package/dist/generator/context.js.map +1 -1
- package/dist/generator/index.d.ts +3 -0
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +3 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/lists.d.ts +31 -0
- package/dist/generator/lists.d.ts.map +1 -0
- package/dist/generator/lists.js +91 -0
- package/dist/generator/lists.js.map +1 -0
- package/dist/generator/plugin-types.d.ts +10 -0
- package/dist/generator/plugin-types.d.ts.map +1 -0
- package/dist/generator/plugin-types.js +122 -0
- package/dist/generator/plugin-types.js.map +1 -0
- package/dist/generator/prisma-config.d.ts +17 -0
- package/dist/generator/prisma-config.d.ts.map +1 -0
- package/dist/generator/prisma-config.js +40 -0
- package/dist/generator/prisma-config.js.map +1 -0
- package/dist/generator/prisma.d.ts.map +1 -1
- package/dist/generator/prisma.js +1 -2
- package/dist/generator/prisma.js.map +1 -1
- package/dist/generator/types.d.ts.map +1 -1
- package/dist/generator/types.js +53 -1
- 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 +43 -0
- package/dist/mcp/lib/documentation-provider.d.ts.map +1 -0
- package/dist/mcp/lib/documentation-provider.js +163 -0
- package/dist/mcp/lib/documentation-provider.js.map +1 -0
- package/dist/mcp/lib/features/catalog.d.ts +26 -0
- package/dist/mcp/lib/features/catalog.d.ts.map +1 -0
- package/dist/mcp/lib/features/catalog.js +291 -0
- package/dist/mcp/lib/features/catalog.js.map +1 -0
- package/dist/mcp/lib/generators/feature-generator.d.ts +35 -0
- package/dist/mcp/lib/generators/feature-generator.d.ts.map +1 -0
- package/dist/mcp/lib/generators/feature-generator.js +546 -0
- package/dist/mcp/lib/generators/feature-generator.js.map +1 -0
- package/dist/mcp/lib/types.d.ts +80 -0
- package/dist/mcp/lib/types.d.ts.map +1 -0
- package/dist/mcp/lib/types.js +5 -0
- package/dist/mcp/lib/types.js.map +1 -0
- package/dist/mcp/lib/wizards/wizard-engine.d.ts +71 -0
- package/dist/mcp/lib/wizards/wizard-engine.d.ts.map +1 -0
- package/dist/mcp/lib/wizards/wizard-engine.js +356 -0
- package/dist/mcp/lib/wizards/wizard-engine.js.map +1 -0
- package/dist/mcp/server/index.d.ts +8 -0
- package/dist/mcp/server/index.d.ts.map +1 -0
- package/dist/mcp/server/index.js +202 -0
- package/dist/mcp/server/index.js.map +1 -0
- package/dist/mcp/server/stack-mcp-server.d.ts +92 -0
- package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -0
- package/dist/mcp/server/stack-mcp-server.js +265 -0
- package/dist/mcp/server/stack-mcp-server.js.map +1 -0
- package/package.json +9 -7
- package/src/commands/__snapshots__/generate.test.ts.snap +61 -21
- package/src/commands/dev.test.ts +0 -1
- package/src/commands/generate.test.ts +18 -8
- package/src/commands/generate.ts +12 -0
- package/src/commands/mcp.ts +135 -0
- package/src/generator/__snapshots__/context.test.ts.snap +8 -8
- package/src/generator/__snapshots__/prisma.test.ts.snap +8 -16
- package/src/generator/__snapshots__/types.test.ts.snap +605 -9
- package/src/generator/context.test.ts +13 -8
- package/src/generator/context.ts +21 -3
- package/src/generator/index.ts +3 -0
- package/src/generator/lists.test.ts +335 -0
- package/src/generator/lists.ts +102 -0
- package/src/generator/plugin-types.ts +147 -0
- package/src/generator/prisma-config.ts +46 -0
- package/src/generator/prisma.test.ts +0 -10
- package/src/generator/prisma.ts +1 -2
- package/src/generator/types.test.ts +0 -12
- package/src/generator/types.ts +56 -1
- package/src/index.ts +4 -0
- package/src/mcp/lib/documentation-provider.ts +203 -0
- package/src/mcp/lib/features/catalog.ts +301 -0
- package/src/mcp/lib/generators/feature-generator.ts +598 -0
- package/src/mcp/lib/types.ts +89 -0
- package/src/mcp/lib/wizards/wizard-engine.ts +427 -0
- package/src/mcp/server/index.ts +240 -0
- package/src/mcp/server/stack-mcp-server.ts +301 -0
- 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
|
}
|
package/src/generator/prisma.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/generator/types.ts
CHANGED
|
@@ -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
|
+
}
|