@opensaas/stack-cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +91 -265
- package/dist/commands/migrate.js.map +1 -1
- package/package.json +7 -2
- package/plugin/.claude-plugin/plugin.json +15 -0
- package/plugin/README.md +112 -0
- package/plugin/agents/migration-assistant.md +150 -0
- package/plugin/commands/analyze-schema.md +34 -0
- package/plugin/commands/generate-config.md +33 -0
- package/plugin/commands/validate-migration.md +34 -0
- package/plugin/skills/opensaas-migration/SKILL.md +192 -0
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -462
- package/CLAUDE.md +0 -298
- package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
- package/src/commands/dev.test.ts +0 -215
- package/src/commands/dev.ts +0 -48
- package/src/commands/generate.test.ts +0 -282
- package/src/commands/generate.ts +0 -182
- package/src/commands/init.ts +0 -34
- package/src/commands/mcp.ts +0 -135
- package/src/commands/migrate.ts +0 -534
- package/src/generator/__snapshots__/context.test.ts.snap +0 -361
- package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
- package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
- package/src/generator/context.test.ts +0 -139
- package/src/generator/context.ts +0 -227
- package/src/generator/index.ts +0 -7
- package/src/generator/lists.test.ts +0 -335
- package/src/generator/lists.ts +0 -140
- package/src/generator/plugin-types.ts +0 -147
- package/src/generator/prisma-config.ts +0 -46
- package/src/generator/prisma-extensions.ts +0 -159
- package/src/generator/prisma.test.ts +0 -211
- package/src/generator/prisma.ts +0 -161
- package/src/generator/types.test.ts +0 -268
- package/src/generator/types.ts +0 -537
- package/src/index.ts +0 -46
- package/src/mcp/lib/documentation-provider.ts +0 -710
- package/src/mcp/lib/features/catalog.ts +0 -301
- package/src/mcp/lib/generators/feature-generator.ts +0 -598
- package/src/mcp/lib/types.ts +0 -89
- package/src/mcp/lib/wizards/migration-wizard.ts +0 -584
- package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
- package/src/mcp/server/index.ts +0 -361
- package/src/mcp/server/stack-mcp-server.ts +0 -544
- package/src/migration/generators/migration-generator.ts +0 -675
- package/src/migration/introspectors/index.ts +0 -12
- package/src/migration/introspectors/keystone-introspector.ts +0 -296
- package/src/migration/introspectors/nextjs-introspector.ts +0 -209
- package/src/migration/introspectors/prisma-introspector.ts +0 -233
- package/src/migration/types.ts +0 -92
- package/tests/introspectors/keystone-introspector.test.ts +0 -255
- package/tests/introspectors/nextjs-introspector.test.ts +0 -302
- package/tests/introspectors/prisma-introspector.test.ts +0 -268
- package/tests/migration-generator.test.ts +0 -592
- package/tests/migration-wizard.test.ts +0 -442
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -26
|
@@ -1,675 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration Config Generator
|
|
3
|
-
*
|
|
4
|
-
* Generates opensaas.config.ts from migration session data.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
MigrationSession,
|
|
9
|
-
MigrationOutput,
|
|
10
|
-
IntrospectedSchema,
|
|
11
|
-
IntrospectedModel,
|
|
12
|
-
IntrospectedField,
|
|
13
|
-
} from '../types.js'
|
|
14
|
-
import { PrismaIntrospector } from '../introspectors/prisma-introspector.js'
|
|
15
|
-
import { KeystoneIntrospector } from '../introspectors/keystone-introspector.js'
|
|
16
|
-
|
|
17
|
-
export class MigrationGenerator {
|
|
18
|
-
private prismaIntrospector: PrismaIntrospector
|
|
19
|
-
private keystoneIntrospector: KeystoneIntrospector
|
|
20
|
-
|
|
21
|
-
constructor() {
|
|
22
|
-
this.prismaIntrospector = new PrismaIntrospector()
|
|
23
|
-
this.keystoneIntrospector = new KeystoneIntrospector()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Generate migration output from session
|
|
28
|
-
*/
|
|
29
|
-
async generate(session: MigrationSession): Promise<MigrationOutput> {
|
|
30
|
-
const { projectType, analysis, answers } = session
|
|
31
|
-
|
|
32
|
-
// Get full schema if available
|
|
33
|
-
let schema: IntrospectedSchema | undefined
|
|
34
|
-
try {
|
|
35
|
-
if (projectType === 'prisma') {
|
|
36
|
-
schema = await this.prismaIntrospector.introspect(analysis.cwd)
|
|
37
|
-
} else if (projectType === 'keystone') {
|
|
38
|
-
schema = await this.keystoneIntrospector.introspect(analysis.cwd)
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// Continue without schema - will generate example config
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Collect used field types for imports
|
|
45
|
-
const usedFieldTypes = new Set<string>(['text']) // Always need text
|
|
46
|
-
const warnings: string[] = []
|
|
47
|
-
|
|
48
|
-
// Generate lists
|
|
49
|
-
const lists = this.generateLists(schema, answers, usedFieldTypes, warnings)
|
|
50
|
-
|
|
51
|
-
// Generate access control helpers
|
|
52
|
-
const accessHelpers = this.generateAccessHelpers(answers)
|
|
53
|
-
|
|
54
|
-
// Generate database config
|
|
55
|
-
const dbConfig = this.generateDatabaseConfig(
|
|
56
|
-
(answers.db_provider as string) || analysis.provider || 'sqlite',
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
// Determine if using auth
|
|
60
|
-
const useAuth = answers.enable_auth === true
|
|
61
|
-
|
|
62
|
-
// Generate imports
|
|
63
|
-
const imports = this.generateImports(usedFieldTypes, useAuth, dbConfig.provider)
|
|
64
|
-
|
|
65
|
-
// Generate the full config
|
|
66
|
-
const configContent = this.assembleConfig({
|
|
67
|
-
imports,
|
|
68
|
-
accessHelpers,
|
|
69
|
-
dbConfig,
|
|
70
|
-
lists,
|
|
71
|
-
useAuth,
|
|
72
|
-
authMethods: (answers.auth_methods as string[]) || ['email-password'],
|
|
73
|
-
adminBasePath: (answers.admin_base_path as string) || '/admin',
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
// Generate dependencies list
|
|
77
|
-
const dependencies = this.generateDependencies(dbConfig.provider, useAuth)
|
|
78
|
-
|
|
79
|
-
// Generate additional files
|
|
80
|
-
const files = this.generateAdditionalFiles(answers, dbConfig.provider)
|
|
81
|
-
|
|
82
|
-
// Generate next steps
|
|
83
|
-
const steps = this.generateSteps(useAuth, dbConfig.provider)
|
|
84
|
-
|
|
85
|
-
// Add warnings from introspection
|
|
86
|
-
if (schema) {
|
|
87
|
-
const introspectorWarnings =
|
|
88
|
-
projectType === 'prisma'
|
|
89
|
-
? this.prismaIntrospector.getWarnings(schema)
|
|
90
|
-
: this.keystoneIntrospector.getWarnings(schema)
|
|
91
|
-
warnings.push(...introspectorWarnings)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
configContent,
|
|
96
|
-
dependencies,
|
|
97
|
-
files,
|
|
98
|
-
steps,
|
|
99
|
-
warnings,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Generate list definitions from schema
|
|
105
|
-
*/
|
|
106
|
-
private generateLists(
|
|
107
|
-
schema: IntrospectedSchema | undefined,
|
|
108
|
-
answers: Record<string, unknown>,
|
|
109
|
-
usedFieldTypes: Set<string>,
|
|
110
|
-
warnings: string[],
|
|
111
|
-
): string {
|
|
112
|
-
if (!schema || schema.models.length === 0) {
|
|
113
|
-
// No schema, generate example lists
|
|
114
|
-
usedFieldTypes.add('timestamp')
|
|
115
|
-
return ` // Add your lists here
|
|
116
|
-
// Example:
|
|
117
|
-
// Post: list({
|
|
118
|
-
// fields: {
|
|
119
|
-
// title: text({ validation: { isRequired: true } }),
|
|
120
|
-
// content: text(),
|
|
121
|
-
// createdAt: timestamp({ defaultValue: { kind: 'now' } }),
|
|
122
|
-
// },
|
|
123
|
-
// }),`
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Filter out auth models if using auth plugin
|
|
127
|
-
const skipAuthModels = answers.skip_auth_models === true
|
|
128
|
-
const authModelNames = ['User', 'Account', 'Session', 'Verification']
|
|
129
|
-
|
|
130
|
-
const modelsToGenerate = schema.models.filter((model) => {
|
|
131
|
-
if (skipAuthModels && authModelNames.includes(model.name)) {
|
|
132
|
-
return false
|
|
133
|
-
}
|
|
134
|
-
return true
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// Get models that should have owner access
|
|
138
|
-
const ownerModels = new Set((answers.models_with_owner as string[]) || [])
|
|
139
|
-
|
|
140
|
-
// Generate each list
|
|
141
|
-
const listDefinitions = modelsToGenerate.map((model) => {
|
|
142
|
-
return this.generateList(
|
|
143
|
-
model,
|
|
144
|
-
schema,
|
|
145
|
-
ownerModels.has(model.name),
|
|
146
|
-
answers,
|
|
147
|
-
usedFieldTypes,
|
|
148
|
-
warnings,
|
|
149
|
-
)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
return listDefinitions.join('\n')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Generate a single list definition
|
|
157
|
-
*/
|
|
158
|
-
private generateList(
|
|
159
|
-
model: IntrospectedModel,
|
|
160
|
-
schema: IntrospectedSchema,
|
|
161
|
-
hasOwnerAccess: boolean,
|
|
162
|
-
answers: Record<string, unknown>,
|
|
163
|
-
usedFieldTypes: Set<string>,
|
|
164
|
-
warnings: string[],
|
|
165
|
-
): string {
|
|
166
|
-
const fields: string[] = []
|
|
167
|
-
|
|
168
|
-
// Skip system fields (id, createdAt, updatedAt) - OpenSaaS adds these automatically
|
|
169
|
-
const systemFields = ['id', 'createdAt', 'updatedAt']
|
|
170
|
-
|
|
171
|
-
for (const field of model.fields) {
|
|
172
|
-
if (systemFields.includes(field.name)) continue
|
|
173
|
-
if (field.isId) continue // Skip ID fields
|
|
174
|
-
|
|
175
|
-
const fieldDef = this.generateField(field, schema, usedFieldTypes, warnings)
|
|
176
|
-
if (fieldDef) {
|
|
177
|
-
fields.push(` ${field.name}: ${fieldDef},`)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Generate access control
|
|
182
|
-
const access = this.generateListAccess(hasOwnerAccess, model, answers)
|
|
183
|
-
|
|
184
|
-
const fieldsBlock = fields.length > 0 ? `\n${fields.join('\n')}\n ` : ''
|
|
185
|
-
|
|
186
|
-
return ` ${model.name}: list({
|
|
187
|
-
fields: {${fieldsBlock}},${access}
|
|
188
|
-
}),`
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Generate a field definition
|
|
193
|
-
*/
|
|
194
|
-
private generateField(
|
|
195
|
-
field: IntrospectedField,
|
|
196
|
-
schema: IntrospectedSchema,
|
|
197
|
-
usedFieldTypes: Set<string>,
|
|
198
|
-
warnings: string[],
|
|
199
|
-
): string | null {
|
|
200
|
-
// Handle relationships
|
|
201
|
-
if (field.relation) {
|
|
202
|
-
usedFieldTypes.add('relationship')
|
|
203
|
-
|
|
204
|
-
// Find the related model and back-reference field
|
|
205
|
-
const relatedModel = schema.models.find((m) => m.name === field.relation!.model)
|
|
206
|
-
const backRef = relatedModel?.fields.find(
|
|
207
|
-
(f) => f.relation && f.relation.model === field.type,
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const ref = backRef ? `${field.relation.model}.${backRef.name}` : field.relation.model
|
|
211
|
-
|
|
212
|
-
const many = field.isList ? ', many: true' : ''
|
|
213
|
-
return `relationship({ ref: '${ref}'${many} })`
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Handle enums as select fields
|
|
217
|
-
const enumDef = schema.enums.find((e) => e.name === field.type)
|
|
218
|
-
if (enumDef) {
|
|
219
|
-
usedFieldTypes.add('select')
|
|
220
|
-
const enumOptions = enumDef.values.map((v) => `{ label: '${v}', value: '${v}' }`).join(', ')
|
|
221
|
-
|
|
222
|
-
let selectOptions = `options: [${enumOptions}]`
|
|
223
|
-
if (field.defaultValue) {
|
|
224
|
-
const defaultVal = field.defaultValue.replace(/^["']|["']$/g, '')
|
|
225
|
-
selectOptions += `, defaultValue: '${defaultVal}'`
|
|
226
|
-
}
|
|
227
|
-
return `select({ ${selectOptions} })`
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Map Prisma/Keystone types to OpenSaaS
|
|
231
|
-
const mapping = this.prismaIntrospector.mapPrismaTypeToOpenSaas(field.type)
|
|
232
|
-
usedFieldTypes.add(mapping.import)
|
|
233
|
-
|
|
234
|
-
// Build options
|
|
235
|
-
const options: string[] = []
|
|
236
|
-
|
|
237
|
-
if (field.isRequired && !field.defaultValue) {
|
|
238
|
-
options.push('validation: { isRequired: true }')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (field.isUnique) {
|
|
242
|
-
options.push("isIndexed: 'unique'")
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Handle default values
|
|
246
|
-
if (field.defaultValue) {
|
|
247
|
-
if (field.type === 'DateTime' && field.defaultValue === 'now()') {
|
|
248
|
-
options.push("defaultValue: { kind: 'now' }")
|
|
249
|
-
} else if (field.type === 'Boolean') {
|
|
250
|
-
options.push(`defaultValue: ${field.defaultValue}`)
|
|
251
|
-
}
|
|
252
|
-
// Other defaults are harder to map automatically
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Generate unsupported type warning
|
|
256
|
-
if (['BigInt', 'Decimal', 'Bytes'].includes(field.type)) {
|
|
257
|
-
warnings.push(
|
|
258
|
-
`Field "${field.name}" uses unsupported type "${field.type}" - mapped to text()`,
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const optionsStr = options.length > 0 ? `{ ${options.join(', ')} }` : ''
|
|
263
|
-
return `${mapping.type}(${optionsStr})`
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Generate list access control
|
|
268
|
-
*/
|
|
269
|
-
private generateListAccess(
|
|
270
|
-
hasOwnerAccess: boolean,
|
|
271
|
-
model: IntrospectedModel,
|
|
272
|
-
answers: Record<string, unknown>,
|
|
273
|
-
): string {
|
|
274
|
-
const defaultAccess = (answers.default_access as string) || 'public-read-auth-write'
|
|
275
|
-
|
|
276
|
-
if (hasOwnerAccess) {
|
|
277
|
-
// Find the user relationship field
|
|
278
|
-
const userField = model.fields.find(
|
|
279
|
-
(f) =>
|
|
280
|
-
f.relation?.model === 'User' ||
|
|
281
|
-
f.name.toLowerCase().includes('author') ||
|
|
282
|
-
f.name.toLowerCase().includes('owner') ||
|
|
283
|
-
f.name.toLowerCase().includes('user'),
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
const ownerField = userField?.name || 'author'
|
|
287
|
-
const ownerIdField = ownerField.endsWith('Id') ? ownerField : `${ownerField}Id`
|
|
288
|
-
|
|
289
|
-
return `
|
|
290
|
-
access: {
|
|
291
|
-
operation: {
|
|
292
|
-
query: () => true,
|
|
293
|
-
create: ({ session }) => !!session,
|
|
294
|
-
update: isOwner,
|
|
295
|
-
delete: isOwner,
|
|
296
|
-
},
|
|
297
|
-
filter: {
|
|
298
|
-
// Optionally filter queries to only show user's own items
|
|
299
|
-
// query: ({ session }) => session ? { ${ownerIdField}: { equals: session.userId } } : true,
|
|
300
|
-
},
|
|
301
|
-
},`
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Generate based on default access pattern
|
|
305
|
-
switch (defaultAccess) {
|
|
306
|
-
case 'authenticated-only':
|
|
307
|
-
return `
|
|
308
|
-
access: {
|
|
309
|
-
operation: {
|
|
310
|
-
query: ({ session }) => !!session,
|
|
311
|
-
create: ({ session }) => !!session,
|
|
312
|
-
update: ({ session }) => !!session,
|
|
313
|
-
delete: ({ session }) => !!session,
|
|
314
|
-
},
|
|
315
|
-
},`
|
|
316
|
-
|
|
317
|
-
case 'owner-only':
|
|
318
|
-
return `
|
|
319
|
-
access: {
|
|
320
|
-
operation: {
|
|
321
|
-
query: ({ session }) => !!session,
|
|
322
|
-
create: ({ session }) => !!session,
|
|
323
|
-
update: ({ session }) => !!session,
|
|
324
|
-
delete: ({ session }) => !!session,
|
|
325
|
-
},
|
|
326
|
-
// Add filter to scope to user's own items:
|
|
327
|
-
// filter: { query: ({ session }) => ({ userId: { equals: session?.userId } }) },
|
|
328
|
-
},`
|
|
329
|
-
|
|
330
|
-
case 'admin-only':
|
|
331
|
-
return `
|
|
332
|
-
access: {
|
|
333
|
-
operation: {
|
|
334
|
-
query: ({ session }) => session?.role === 'admin',
|
|
335
|
-
create: ({ session }) => session?.role === 'admin',
|
|
336
|
-
update: ({ session }) => session?.role === 'admin',
|
|
337
|
-
delete: ({ session }) => session?.role === 'admin',
|
|
338
|
-
},
|
|
339
|
-
},`
|
|
340
|
-
|
|
341
|
-
case 'public-read-auth-write':
|
|
342
|
-
default:
|
|
343
|
-
return `
|
|
344
|
-
access: {
|
|
345
|
-
operation: {
|
|
346
|
-
query: () => true,
|
|
347
|
-
create: ({ session }) => !!session,
|
|
348
|
-
update: ({ session }) => !!session,
|
|
349
|
-
delete: ({ session }) => !!session,
|
|
350
|
-
},
|
|
351
|
-
},`
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Generate access control helper functions
|
|
357
|
-
*/
|
|
358
|
-
private generateAccessHelpers(answers: Record<string, unknown>): string {
|
|
359
|
-
const helpers: string[] = []
|
|
360
|
-
const ownerModels = (answers.models_with_owner as string[]) || []
|
|
361
|
-
|
|
362
|
-
if (ownerModels.length > 0) {
|
|
363
|
-
helpers.push(`/**
|
|
364
|
-
* Access control helpers
|
|
365
|
-
*/
|
|
366
|
-
|
|
367
|
-
// Check if user owns the item (based on authorId or userId)
|
|
368
|
-
const isOwner: AccessControl = ({ session, item }) => {
|
|
369
|
-
if (!session) return false
|
|
370
|
-
// Try authorId first, then userId
|
|
371
|
-
const ownerId = (item as any)?.authorId || (item as any)?.userId
|
|
372
|
-
return ownerId === session.userId
|
|
373
|
-
}
|
|
374
|
-
`)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return helpers.join('\n')
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Generate database configuration
|
|
382
|
-
*/
|
|
383
|
-
private generateDatabaseConfig(provider: string): {
|
|
384
|
-
provider: string
|
|
385
|
-
configCode: string
|
|
386
|
-
imports: string[]
|
|
387
|
-
} {
|
|
388
|
-
switch (provider) {
|
|
389
|
-
case 'postgresql':
|
|
390
|
-
return {
|
|
391
|
-
provider: 'postgresql',
|
|
392
|
-
imports: ["import { PrismaPg } from '@prisma/adapter-pg'", "import pg from 'pg'"],
|
|
393
|
-
configCode: ` db: {
|
|
394
|
-
provider: 'postgresql',
|
|
395
|
-
prismaClientConstructor: (PrismaClient) => {
|
|
396
|
-
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
|
|
397
|
-
const adapter = new PrismaPg(pool)
|
|
398
|
-
return new PrismaClient({ adapter })
|
|
399
|
-
},
|
|
400
|
-
},`,
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
case 'mysql':
|
|
404
|
-
return {
|
|
405
|
-
provider: 'mysql',
|
|
406
|
-
imports: ["import { PrismaPlanetScale } from '@prisma/adapter-planetscale'"],
|
|
407
|
-
configCode: ` db: {
|
|
408
|
-
provider: 'mysql',
|
|
409
|
-
prismaClientConstructor: (PrismaClient) => {
|
|
410
|
-
const adapter = new PrismaPlanetScale({
|
|
411
|
-
url: process.env.DATABASE_URL!,
|
|
412
|
-
})
|
|
413
|
-
return new PrismaClient({ adapter })
|
|
414
|
-
},
|
|
415
|
-
},`,
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
case 'sqlite':
|
|
419
|
-
default:
|
|
420
|
-
return {
|
|
421
|
-
provider: 'sqlite',
|
|
422
|
-
imports: ["import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'"],
|
|
423
|
-
configCode: ` db: {
|
|
424
|
-
provider: 'sqlite',
|
|
425
|
-
prismaClientConstructor: (PrismaClient) => {
|
|
426
|
-
const adapter = new PrismaBetterSqlite3({
|
|
427
|
-
url: process.env.DATABASE_URL || 'file:./dev.db',
|
|
428
|
-
})
|
|
429
|
-
return new PrismaClient({ adapter })
|
|
430
|
-
},
|
|
431
|
-
},`,
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Generate import statements
|
|
438
|
-
*/
|
|
439
|
-
private generateImports(
|
|
440
|
-
usedFieldTypes: Set<string>,
|
|
441
|
-
useAuth: boolean,
|
|
442
|
-
dbProvider: string,
|
|
443
|
-
): string {
|
|
444
|
-
const imports: string[] = []
|
|
445
|
-
|
|
446
|
-
// Core imports
|
|
447
|
-
imports.push("import { config, list } from '@opensaas/stack-core'")
|
|
448
|
-
|
|
449
|
-
// Field imports
|
|
450
|
-
const fieldTypes = Array.from(usedFieldTypes).sort()
|
|
451
|
-
imports.push(`import { ${fieldTypes.join(', ')} } from '@opensaas/stack-core/fields'`)
|
|
452
|
-
|
|
453
|
-
// Auth imports
|
|
454
|
-
if (useAuth) {
|
|
455
|
-
imports.push("import { authPlugin } from '@opensaas/stack-auth'")
|
|
456
|
-
imports.push("import type { AccessControl } from '@opensaas/stack-core'")
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Database adapter imports
|
|
460
|
-
const dbConfig = this.generateDatabaseConfig(dbProvider)
|
|
461
|
-
imports.push(...dbConfig.imports)
|
|
462
|
-
|
|
463
|
-
return imports.join('\n')
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Assemble the complete config file
|
|
468
|
-
*/
|
|
469
|
-
private assembleConfig(options: {
|
|
470
|
-
imports: string
|
|
471
|
-
accessHelpers: string
|
|
472
|
-
dbConfig: { configCode: string }
|
|
473
|
-
lists: string
|
|
474
|
-
useAuth: boolean
|
|
475
|
-
authMethods: string[]
|
|
476
|
-
adminBasePath: string
|
|
477
|
-
}): string {
|
|
478
|
-
const { imports, accessHelpers, dbConfig, lists, useAuth, authMethods, adminBasePath } = options
|
|
479
|
-
|
|
480
|
-
// Generate auth plugin config
|
|
481
|
-
let authPluginStr = ''
|
|
482
|
-
if (useAuth) {
|
|
483
|
-
const authOptions: string[] = []
|
|
484
|
-
|
|
485
|
-
if (authMethods.includes('email-password')) {
|
|
486
|
-
authOptions.push(` emailAndPassword: {
|
|
487
|
-
enabled: true,
|
|
488
|
-
minPasswordLength: 8,
|
|
489
|
-
},`)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (authMethods.includes('magic-link')) {
|
|
493
|
-
authOptions.push(` magicLink: {
|
|
494
|
-
enabled: true,
|
|
495
|
-
},`)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// OAuth providers would need additional setup
|
|
499
|
-
if (authMethods.includes('google')) {
|
|
500
|
-
authOptions.push(` // Uncomment and configure Google OAuth:
|
|
501
|
-
// google: {
|
|
502
|
-
// clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
503
|
-
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
504
|
-
// },`)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (authMethods.includes('github')) {
|
|
508
|
-
authOptions.push(` // Uncomment and configure GitHub OAuth:
|
|
509
|
-
// github: {
|
|
510
|
-
// clientId: process.env.GITHUB_CLIENT_ID!,
|
|
511
|
-
// clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
512
|
-
// },`)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
authOptions.push(` sessionFields: ['userId', 'email', 'name'],`)
|
|
516
|
-
|
|
517
|
-
authPluginStr = ` authPlugin({
|
|
518
|
-
${authOptions.join('\n')}
|
|
519
|
-
}),`
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Build config body
|
|
523
|
-
const pluginsBlock = useAuth
|
|
524
|
-
? ` plugins: [
|
|
525
|
-
${authPluginStr}
|
|
526
|
-
],
|
|
527
|
-
|
|
528
|
-
`
|
|
529
|
-
: ''
|
|
530
|
-
|
|
531
|
-
const configBody = `export default config({
|
|
532
|
-
${pluginsBlock}${dbConfig.configCode}
|
|
533
|
-
lists: {
|
|
534
|
-
${lists}
|
|
535
|
-
},
|
|
536
|
-
ui: {
|
|
537
|
-
basePath: '${adminBasePath}',
|
|
538
|
-
},
|
|
539
|
-
})`
|
|
540
|
-
|
|
541
|
-
return `${imports}
|
|
542
|
-
|
|
543
|
-
${accessHelpers}${configBody}
|
|
544
|
-
`
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Generate list of dependencies to install
|
|
549
|
-
*/
|
|
550
|
-
private generateDependencies(dbProvider: string, useAuth: boolean): string[] {
|
|
551
|
-
const deps: string[] = [
|
|
552
|
-
'@opensaas/stack-core',
|
|
553
|
-
'@opensaas/stack-ui',
|
|
554
|
-
'@prisma/client',
|
|
555
|
-
'prisma',
|
|
556
|
-
]
|
|
557
|
-
|
|
558
|
-
// Database adapter deps
|
|
559
|
-
switch (dbProvider) {
|
|
560
|
-
case 'postgresql':
|
|
561
|
-
deps.push('@prisma/adapter-pg', 'pg', '@types/pg')
|
|
562
|
-
break
|
|
563
|
-
case 'mysql':
|
|
564
|
-
deps.push('@prisma/adapter-planetscale')
|
|
565
|
-
break
|
|
566
|
-
case 'sqlite':
|
|
567
|
-
default:
|
|
568
|
-
deps.push('@prisma/adapter-better-sqlite3')
|
|
569
|
-
break
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Auth deps
|
|
573
|
-
if (useAuth) {
|
|
574
|
-
deps.push('@opensaas/stack-auth', 'better-auth')
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return deps
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Generate additional files if needed
|
|
582
|
-
*/
|
|
583
|
-
private generateAdditionalFiles(
|
|
584
|
-
answers: Record<string, unknown>,
|
|
585
|
-
dbProvider: string,
|
|
586
|
-
): Array<{
|
|
587
|
-
path: string
|
|
588
|
-
content: string
|
|
589
|
-
language: string
|
|
590
|
-
description: string
|
|
591
|
-
}> {
|
|
592
|
-
const files: Array<{
|
|
593
|
-
path: string
|
|
594
|
-
content: string
|
|
595
|
-
language: string
|
|
596
|
-
description: string
|
|
597
|
-
}> = []
|
|
598
|
-
|
|
599
|
-
// Generate .env.example
|
|
600
|
-
const envVars: string[] = ['# Database']
|
|
601
|
-
|
|
602
|
-
// Database URL based on provider
|
|
603
|
-
switch (dbProvider) {
|
|
604
|
-
case 'postgresql':
|
|
605
|
-
envVars.push('DATABASE_URL="postgresql://user:password@localhost:5432/mydb"')
|
|
606
|
-
break
|
|
607
|
-
case 'mysql':
|
|
608
|
-
envVars.push('DATABASE_URL="mysql://user:password@localhost:3306/mydb"')
|
|
609
|
-
break
|
|
610
|
-
case 'sqlite':
|
|
611
|
-
default:
|
|
612
|
-
envVars.push('DATABASE_URL="file:./dev.db"')
|
|
613
|
-
break
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
envVars.push('')
|
|
617
|
-
|
|
618
|
-
if (answers.enable_auth) {
|
|
619
|
-
envVars.push('# Auth')
|
|
620
|
-
envVars.push('BETTER_AUTH_SECRET="generate-with-openssl-rand-base64-32"')
|
|
621
|
-
envVars.push('BETTER_AUTH_URL="http://localhost:3000"')
|
|
622
|
-
envVars.push('')
|
|
623
|
-
|
|
624
|
-
const authMethods = (answers.auth_methods as string[]) || []
|
|
625
|
-
if (authMethods.includes('google')) {
|
|
626
|
-
envVars.push('# Google OAuth (optional)')
|
|
627
|
-
envVars.push('GOOGLE_CLIENT_ID=""')
|
|
628
|
-
envVars.push('GOOGLE_CLIENT_SECRET=""')
|
|
629
|
-
envVars.push('')
|
|
630
|
-
}
|
|
631
|
-
if (authMethods.includes('github')) {
|
|
632
|
-
envVars.push('# GitHub OAuth (optional)')
|
|
633
|
-
envVars.push('GITHUB_CLIENT_ID=""')
|
|
634
|
-
envVars.push('GITHUB_CLIENT_SECRET=""')
|
|
635
|
-
envVars.push('')
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
files.push({
|
|
640
|
-
path: '.env.example',
|
|
641
|
-
content: envVars.join('\n'),
|
|
642
|
-
language: 'bash',
|
|
643
|
-
description: 'Environment variables template',
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
return files
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Generate next steps
|
|
651
|
-
*/
|
|
652
|
-
private generateSteps(useAuth: boolean, dbProvider: string): string[] {
|
|
653
|
-
const steps = [
|
|
654
|
-
'Save the generated config to `opensaas.config.ts`',
|
|
655
|
-
'Copy `.env.example` to `.env` and fill in values',
|
|
656
|
-
]
|
|
657
|
-
|
|
658
|
-
if (useAuth) {
|
|
659
|
-
steps.push('Generate BETTER_AUTH_SECRET: `openssl rand -base64 32`')
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
steps.push(
|
|
663
|
-
'Install dependencies: `pnpm add <dependencies>`',
|
|
664
|
-
'Generate Prisma schema: `pnpm opensaas generate`',
|
|
665
|
-
'Generate Prisma client: `npx prisma generate`',
|
|
666
|
-
'Push schema to database: `npx prisma db push`',
|
|
667
|
-
'Start development server: `pnpm dev`',
|
|
668
|
-
)
|
|
669
|
-
|
|
670
|
-
const adminPath = useAuth ? '' : '/admin'
|
|
671
|
-
steps.push(`Visit admin UI at http://localhost:3000${adminPath}`)
|
|
672
|
-
|
|
673
|
-
return steps
|
|
674
|
-
}
|
|
675
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema introspectors for migration system
|
|
3
|
-
*
|
|
4
|
-
* These introspectors parse existing schemas and configs to extract
|
|
5
|
-
* model and field information for automated migration.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export { PrismaIntrospector } from './prisma-introspector.js'
|
|
9
|
-
export { KeystoneIntrospector } from './keystone-introspector.js'
|
|
10
|
-
export type { KeystoneList, KeystoneField, KeystoneSchema } from './keystone-introspector.js'
|
|
11
|
-
export { NextjsIntrospector } from './nextjs-introspector.js'
|
|
12
|
-
export type { NextjsAnalysis } from './nextjs-introspector.js'
|