@opensaas/stack-cli 0.3.0 → 0.5.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 (105) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +193 -0
  3. package/dist/commands/generate.d.ts.map +1 -1
  4. package/dist/commands/generate.js +4 -13
  5. package/dist/commands/generate.js.map +1 -1
  6. package/dist/commands/migrate.d.ts +9 -0
  7. package/dist/commands/migrate.d.ts.map +1 -0
  8. package/dist/commands/migrate.js +473 -0
  9. package/dist/commands/migrate.js.map +1 -0
  10. package/dist/generator/context.d.ts.map +1 -1
  11. package/dist/generator/context.js +20 -5
  12. package/dist/generator/context.js.map +1 -1
  13. package/dist/generator/index.d.ts +1 -1
  14. package/dist/generator/index.d.ts.map +1 -1
  15. package/dist/generator/index.js +1 -1
  16. package/dist/generator/index.js.map +1 -1
  17. package/dist/generator/lists.d.ts.map +1 -1
  18. package/dist/generator/lists.js +33 -1
  19. package/dist/generator/lists.js.map +1 -1
  20. package/dist/generator/prisma-extensions.d.ts +11 -0
  21. package/dist/generator/prisma-extensions.d.ts.map +1 -0
  22. package/dist/generator/prisma-extensions.js +134 -0
  23. package/dist/generator/prisma-extensions.js.map +1 -0
  24. package/dist/generator/prisma.d.ts.map +1 -1
  25. package/dist/generator/prisma.js +4 -0
  26. package/dist/generator/prisma.js.map +1 -1
  27. package/dist/generator/types.d.ts.map +1 -1
  28. package/dist/generator/types.js +151 -17
  29. package/dist/generator/types.js.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp/lib/documentation-provider.d.ts +23 -0
  33. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -1
  34. package/dist/mcp/lib/documentation-provider.js +471 -0
  35. package/dist/mcp/lib/documentation-provider.js.map +1 -1
  36. package/dist/mcp/lib/wizards/migration-wizard.d.ts +80 -0
  37. package/dist/mcp/lib/wizards/migration-wizard.d.ts.map +1 -0
  38. package/dist/mcp/lib/wizards/migration-wizard.js +499 -0
  39. package/dist/mcp/lib/wizards/migration-wizard.js.map +1 -0
  40. package/dist/mcp/server/index.d.ts.map +1 -1
  41. package/dist/mcp/server/index.js +103 -0
  42. package/dist/mcp/server/index.js.map +1 -1
  43. package/dist/mcp/server/stack-mcp-server.d.ts +85 -0
  44. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -1
  45. package/dist/mcp/server/stack-mcp-server.js +219 -0
  46. package/dist/mcp/server/stack-mcp-server.js.map +1 -1
  47. package/dist/migration/generators/migration-generator.d.ts +60 -0
  48. package/dist/migration/generators/migration-generator.d.ts.map +1 -0
  49. package/dist/migration/generators/migration-generator.js +510 -0
  50. package/dist/migration/generators/migration-generator.js.map +1 -0
  51. package/dist/migration/introspectors/index.d.ts +12 -0
  52. package/dist/migration/introspectors/index.d.ts.map +1 -0
  53. package/dist/migration/introspectors/index.js +10 -0
  54. package/dist/migration/introspectors/index.js.map +1 -0
  55. package/dist/migration/introspectors/keystone-introspector.d.ts +59 -0
  56. package/dist/migration/introspectors/keystone-introspector.d.ts.map +1 -0
  57. package/dist/migration/introspectors/keystone-introspector.js +229 -0
  58. package/dist/migration/introspectors/keystone-introspector.js.map +1 -0
  59. package/dist/migration/introspectors/nextjs-introspector.d.ts +59 -0
  60. package/dist/migration/introspectors/nextjs-introspector.d.ts.map +1 -0
  61. package/dist/migration/introspectors/nextjs-introspector.js +159 -0
  62. package/dist/migration/introspectors/nextjs-introspector.js.map +1 -0
  63. package/dist/migration/introspectors/prisma-introspector.d.ts +45 -0
  64. package/dist/migration/introspectors/prisma-introspector.d.ts.map +1 -0
  65. package/dist/migration/introspectors/prisma-introspector.js +190 -0
  66. package/dist/migration/introspectors/prisma-introspector.js.map +1 -0
  67. package/dist/migration/types.d.ts +86 -0
  68. package/dist/migration/types.d.ts.map +1 -0
  69. package/dist/migration/types.js +5 -0
  70. package/dist/migration/types.js.map +1 -0
  71. package/package.json +12 -9
  72. package/src/commands/__snapshots__/generate.test.ts.snap +92 -21
  73. package/src/commands/generate.ts +8 -19
  74. package/src/commands/migrate.ts +534 -0
  75. package/src/generator/__snapshots__/context.test.ts.snap +60 -15
  76. package/src/generator/__snapshots__/types.test.ts.snap +689 -95
  77. package/src/generator/context.test.ts +3 -1
  78. package/src/generator/context.ts +20 -5
  79. package/src/generator/index.ts +1 -1
  80. package/src/generator/lists.ts +39 -1
  81. package/src/generator/prisma-extensions.ts +159 -0
  82. package/src/generator/prisma.ts +5 -0
  83. package/src/generator/types.ts +204 -17
  84. package/src/index.ts +4 -0
  85. package/src/mcp/lib/documentation-provider.ts +507 -0
  86. package/src/mcp/lib/wizards/migration-wizard.ts +584 -0
  87. package/src/mcp/server/index.ts +121 -0
  88. package/src/mcp/server/stack-mcp-server.ts +243 -0
  89. package/src/migration/generators/migration-generator.ts +675 -0
  90. package/src/migration/introspectors/index.ts +12 -0
  91. package/src/migration/introspectors/keystone-introspector.ts +296 -0
  92. package/src/migration/introspectors/nextjs-introspector.ts +209 -0
  93. package/src/migration/introspectors/prisma-introspector.ts +233 -0
  94. package/src/migration/types.ts +92 -0
  95. package/tests/introspectors/keystone-introspector.test.ts +255 -0
  96. package/tests/introspectors/nextjs-introspector.test.ts +302 -0
  97. package/tests/introspectors/prisma-introspector.test.ts +268 -0
  98. package/tests/migration-generator.test.ts +592 -0
  99. package/tests/migration-wizard.test.ts +442 -0
  100. package/tsconfig.tsbuildinfo +1 -1
  101. package/dist/generator/type-patcher.d.ts +0 -13
  102. package/dist/generator/type-patcher.d.ts.map +0 -1
  103. package/dist/generator/type-patcher.js +0 -68
  104. package/dist/generator/type-patcher.js.map +0 -1
  105. package/src/generator/type-patcher.ts +0 -93
@@ -0,0 +1,296 @@
1
+ /**
2
+ * KeystoneJS Config Introspector
3
+ *
4
+ * Loads keystone.config.ts using jiti and extracts list definitions.
5
+ * KeystoneJS → OpenSaaS migration is mostly 1:1.
6
+ */
7
+
8
+ import path from 'path'
9
+ import fs from 'fs-extra'
10
+ import { createJiti } from 'jiti'
11
+ import type { IntrospectedSchema, IntrospectedModel, IntrospectedField } from '../types.js'
12
+
13
+ export interface KeystoneList {
14
+ name: string
15
+ fields: KeystoneField[]
16
+ access?: Record<string, unknown>
17
+ hooks?: Record<string, unknown>
18
+ }
19
+
20
+ export interface KeystoneField {
21
+ name: string
22
+ type: string
23
+ options?: Record<string, unknown>
24
+ }
25
+
26
+ export interface KeystoneSchema {
27
+ lists: KeystoneList[]
28
+ db?: {
29
+ provider: string
30
+ url?: string
31
+ }
32
+ }
33
+
34
+ export class KeystoneIntrospector {
35
+ /**
36
+ * Introspect a KeystoneJS config file
37
+ */
38
+ async introspect(
39
+ cwd: string,
40
+ configPath: string = 'keystone.config.ts',
41
+ ): Promise<IntrospectedSchema> {
42
+ const fullPath = path.isAbsolute(configPath) ? configPath : path.join(cwd, configPath)
43
+
44
+ // Try alternative paths
45
+ const paths = [fullPath, path.join(cwd, 'keystone.ts'), path.join(cwd, 'keystone.config.js')]
46
+
47
+ let foundPath: string | undefined
48
+ for (const p of paths) {
49
+ if (await fs.pathExists(p)) {
50
+ foundPath = p
51
+ break
52
+ }
53
+ }
54
+
55
+ if (!foundPath) {
56
+ throw new Error(`KeystoneJS config not found. Tried: ${paths.join(', ')}`)
57
+ }
58
+
59
+ try {
60
+ // Use jiti to load TypeScript config
61
+ const jiti = createJiti(import.meta.url, {
62
+ interopDefault: true,
63
+ moduleCache: false,
64
+ })
65
+
66
+ const configModule = (await jiti.import(foundPath)) as { default?: unknown } | unknown
67
+ const config =
68
+ typeof configModule === 'object' && configModule !== null && 'default' in configModule
69
+ ? configModule.default
70
+ : configModule
71
+
72
+ const keystoneSchema = this.parseConfig(config)
73
+
74
+ // Convert KeystoneSchema to IntrospectedSchema
75
+ return this.convertToIntrospectedSchema(keystoneSchema)
76
+ } catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error)
78
+ throw new Error(`Failed to load KeystoneJS config: ${message}`)
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Parse the loaded KeystoneJS config object
84
+ */
85
+ private parseConfig(config: unknown): KeystoneSchema {
86
+ const result: KeystoneSchema = {
87
+ lists: [],
88
+ }
89
+
90
+ if (typeof config !== 'object' || config === null) {
91
+ return result
92
+ }
93
+
94
+ const configObj = config as Record<string, unknown>
95
+
96
+ // Extract database config
97
+ if (configObj.db && typeof configObj.db === 'object' && configObj.db !== null) {
98
+ const db = configObj.db as Record<string, unknown>
99
+ result.db = {
100
+ provider: typeof db.provider === 'string' ? db.provider : 'unknown',
101
+ url: typeof db.url === 'string' ? db.url : undefined,
102
+ }
103
+ }
104
+
105
+ // Extract lists
106
+ if (configObj.lists && typeof configObj.lists === 'object' && configObj.lists !== null) {
107
+ for (const [name, listDef] of Object.entries(configObj.lists)) {
108
+ const list = this.parseList(name, listDef)
109
+ result.lists.push(list)
110
+ }
111
+ }
112
+
113
+ return result
114
+ }
115
+
116
+ /**
117
+ * Parse a single list definition
118
+ */
119
+ private parseList(name: string, listDef: unknown): KeystoneList {
120
+ const list: KeystoneList = {
121
+ name,
122
+ fields: [],
123
+ }
124
+
125
+ if (typeof listDef !== 'object' || listDef === null) {
126
+ return list
127
+ }
128
+
129
+ const listDefObj = listDef as Record<string, unknown>
130
+
131
+ // Extract fields
132
+ if (listDefObj.fields && typeof listDefObj.fields === 'object' && listDefObj.fields !== null) {
133
+ for (const [fieldName, fieldDef] of Object.entries(listDefObj.fields)) {
134
+ const field = this.parseField(fieldName, fieldDef)
135
+ list.fields.push(field)
136
+ }
137
+ }
138
+
139
+ // Store access and hooks for reference (not used in migration but useful)
140
+ if (listDefObj.access && typeof listDefObj.access === 'object') {
141
+ list.access = listDefObj.access as Record<string, unknown>
142
+ }
143
+ if (listDefObj.hooks && typeof listDefObj.hooks === 'object') {
144
+ list.hooks = listDefObj.hooks as Record<string, unknown>
145
+ }
146
+
147
+ return list
148
+ }
149
+
150
+ /**
151
+ * Parse a single field definition
152
+ */
153
+ private parseField(name: string, fieldDef: unknown): KeystoneField {
154
+ // KeystoneJS fields are objects with a type property or function results
155
+ let type = 'unknown'
156
+ let options: Record<string, unknown> = {}
157
+
158
+ if (typeof fieldDef === 'object' && fieldDef !== null) {
159
+ const fieldDefObj = fieldDef as Record<string, unknown>
160
+
161
+ // Check for common field type patterns
162
+ if (typeof fieldDefObj.type === 'string') {
163
+ type = fieldDefObj.type
164
+ } else if (typeof fieldDefObj._type === 'string') {
165
+ type = fieldDefObj._type
166
+ } else if (
167
+ fieldDefObj.constructor &&
168
+ typeof fieldDefObj.constructor === 'object' &&
169
+ fieldDefObj.constructor !== null
170
+ ) {
171
+ const constructor = fieldDefObj.constructor as Record<string, unknown>
172
+ if (typeof constructor.name === 'string') {
173
+ type = constructor.name
174
+ }
175
+ }
176
+
177
+ // Extract common options
178
+ if (fieldDefObj.validation !== undefined) options.validation = fieldDefObj.validation
179
+ if (fieldDefObj.defaultValue !== undefined) options.defaultValue = fieldDefObj.defaultValue
180
+ if (fieldDefObj.isRequired !== undefined) options.isRequired = fieldDefObj.isRequired
181
+ if (fieldDefObj.ref !== undefined) options.ref = fieldDefObj.ref
182
+ if (fieldDefObj.many !== undefined) options.many = fieldDefObj.many
183
+ }
184
+
185
+ return { name, type, options }
186
+ }
187
+
188
+ /**
189
+ * Convert KeystoneSchema to IntrospectedSchema format
190
+ */
191
+ private convertToIntrospectedSchema(keystoneSchema: KeystoneSchema): IntrospectedSchema {
192
+ const models: IntrospectedModel[] = keystoneSchema.lists.map((list) => {
193
+ const fields: IntrospectedField[] = list.fields.map((field) => {
194
+ const isRelationship = field.type.toLowerCase() === 'relationship'
195
+ const isRequired = field.options?.isRequired === true
196
+
197
+ const introspectedField: IntrospectedField = {
198
+ name: field.name,
199
+ type: field.type,
200
+ isRequired,
201
+ isUnique: false, // KeystoneJS doesn't expose this easily
202
+ isId: field.name === 'id',
203
+ isList: field.options?.many === true,
204
+ }
205
+
206
+ if (field.options?.defaultValue !== undefined) {
207
+ introspectedField.defaultValue = String(field.options.defaultValue)
208
+ }
209
+
210
+ if (isRelationship && field.options?.ref) {
211
+ const ref = String(field.options.ref)
212
+ const [model, fieldName] = ref.split('.')
213
+
214
+ introspectedField.relation = {
215
+ name: '',
216
+ model,
217
+ fields: [],
218
+ references: fieldName ? [fieldName] : [],
219
+ }
220
+ }
221
+
222
+ return introspectedField
223
+ })
224
+
225
+ return {
226
+ name: list.name,
227
+ fields,
228
+ hasRelations: fields.some((f) => f.relation !== undefined),
229
+ primaryKey: 'id',
230
+ }
231
+ })
232
+
233
+ return {
234
+ provider: keystoneSchema.db?.provider || 'unknown',
235
+ models,
236
+ enums: [], // KeystoneJS doesn't have enums in the same way
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Map KeystoneJS field type to OpenSaaS equivalent
242
+ */
243
+ mapKeystoneTypeToOpenSaas(keystoneType: string): { type: string; import: string } {
244
+ // KeystoneJS → OpenSaaS is mostly 1:1
245
+ const mappings: Record<string, { type: string; import: string }> = {
246
+ text: { type: 'text', import: 'text' },
247
+ integer: { type: 'integer', import: 'integer' },
248
+ float: { type: 'float', import: 'float' },
249
+ checkbox: { type: 'checkbox', import: 'checkbox' },
250
+ timestamp: { type: 'timestamp', import: 'timestamp' },
251
+ select: { type: 'select', import: 'select' },
252
+ relationship: { type: 'relationship', import: 'relationship' },
253
+ password: { type: 'password', import: 'password' },
254
+ json: { type: 'json', import: 'json' },
255
+ // Storage field types (from @opensaas/stack-storage)
256
+ image: { type: 'image', import: 'image' },
257
+ file: { type: 'file', import: 'file' },
258
+ // Virtual/computed fields
259
+ virtual: { type: 'virtual', import: 'virtual' },
260
+ // Other field types
261
+ calendarDay: { type: 'timestamp', import: 'timestamp' },
262
+ }
263
+
264
+ const lower = keystoneType.toLowerCase()
265
+ return mappings[lower] || { type: 'text', import: 'text' }
266
+ }
267
+
268
+ /**
269
+ * Get warnings for unsupported features
270
+ */
271
+ getWarnings(schema: IntrospectedSchema): string[] {
272
+ const warnings: string[] = []
273
+ const hasFileOrImageFields = schema.models.some((model) =>
274
+ model.fields.some((field) => ['image', 'file'].includes(field.type.toLowerCase())),
275
+ )
276
+ const hasVirtualFields = schema.models.some((model) =>
277
+ model.fields.some((field) => field.type.toLowerCase() === 'virtual'),
278
+ )
279
+
280
+ // Add storage configuration reminder if file/image fields are present
281
+ if (hasFileOrImageFields) {
282
+ warnings.push(
283
+ "Your schema uses file/image fields - you'll need to configure storage providers in your OpenSaaS config",
284
+ )
285
+ }
286
+
287
+ // Add virtual field migration reminder
288
+ if (hasVirtualFields) {
289
+ warnings.push(
290
+ "Your schema uses virtual fields - you'll need to manually migrate the resolveOutput hooks to compute field values",
291
+ )
292
+ }
293
+
294
+ return warnings
295
+ }
296
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Next.js Project Introspector
3
+ *
4
+ * Detects Next.js version, auth libraries, database libraries,
5
+ * and other project characteristics.
6
+ */
7
+
8
+ import path from 'path'
9
+ import fs from 'fs-extra'
10
+
11
+ export interface NextjsAnalysis {
12
+ version: string
13
+ routerType: 'app' | 'pages' | 'both' | 'unknown'
14
+ typescript: boolean
15
+ authLibrary?: string
16
+ databaseLibrary?: string
17
+ hasPrisma: boolean
18
+ hasEnvFile: boolean
19
+ existingDependencies: string[]
20
+ }
21
+
22
+ export class NextjsIntrospector {
23
+ /**
24
+ * Analyze a Next.js project
25
+ */
26
+ async introspect(cwd: string): Promise<NextjsAnalysis> {
27
+ const packageJsonPath = path.join(cwd, 'package.json')
28
+
29
+ if (!(await fs.pathExists(packageJsonPath))) {
30
+ throw new Error('package.json not found')
31
+ }
32
+
33
+ const pkg = await fs.readJSON(packageJsonPath)
34
+
35
+ const analysis: NextjsAnalysis = {
36
+ version: this.getNextVersion(pkg),
37
+ routerType: await this.detectRouterType(cwd),
38
+ typescript: await this.hasTypeScript(cwd),
39
+ hasPrisma:
40
+ this.hasDependency(pkg, '@prisma/client') ||
41
+ (await fs.pathExists(path.join(cwd, 'prisma'))),
42
+ hasEnvFile:
43
+ (await fs.pathExists(path.join(cwd, '.env'))) ||
44
+ (await fs.pathExists(path.join(cwd, '.env.local'))),
45
+ existingDependencies: this.getAllDependencies(pkg),
46
+ }
47
+
48
+ // Detect auth library
49
+ analysis.authLibrary = this.detectAuthLibrary(pkg)
50
+
51
+ // Detect database library
52
+ analysis.databaseLibrary = this.detectDatabaseLibrary(pkg)
53
+
54
+ return analysis
55
+ }
56
+
57
+ /**
58
+ * Get Next.js version from package.json
59
+ */
60
+ private getNextVersion(pkg: Record<string, unknown>): string {
61
+ const deps = pkg.dependencies as Record<string, string> | undefined
62
+ const devDeps = pkg.devDependencies as Record<string, string> | undefined
63
+ const version = deps?.next || devDeps?.next || 'unknown'
64
+ // Strip semver prefixes like ^ or ~
65
+ return version.replace(/^[\^~]/, '')
66
+ }
67
+
68
+ /**
69
+ * Detect if project uses app router, pages router, or both
70
+ */
71
+ private async detectRouterType(cwd: string): Promise<'app' | 'pages' | 'both' | 'unknown'> {
72
+ const hasApp =
73
+ (await fs.pathExists(path.join(cwd, 'app'))) ||
74
+ (await fs.pathExists(path.join(cwd, 'src', 'app')))
75
+ const hasPages =
76
+ (await fs.pathExists(path.join(cwd, 'pages'))) ||
77
+ (await fs.pathExists(path.join(cwd, 'src', 'pages')))
78
+
79
+ if (hasApp && hasPages) return 'both'
80
+ if (hasApp) return 'app'
81
+ if (hasPages) return 'pages'
82
+ return 'unknown'
83
+ }
84
+
85
+ /**
86
+ * Check if project uses TypeScript
87
+ */
88
+ private async hasTypeScript(cwd: string): Promise<boolean> {
89
+ return await fs.pathExists(path.join(cwd, 'tsconfig.json'))
90
+ }
91
+
92
+ /**
93
+ * Check if package.json has a dependency
94
+ */
95
+ private hasDependency(pkg: Record<string, unknown>, name: string): boolean {
96
+ const deps = pkg.dependencies as Record<string, string> | undefined
97
+ const devDeps = pkg.devDependencies as Record<string, string> | undefined
98
+ return !!(deps?.[name] || devDeps?.[name])
99
+ }
100
+
101
+ /**
102
+ * Get all dependencies
103
+ */
104
+ private getAllDependencies(pkg: Record<string, unknown>): string[] {
105
+ const deps = pkg.dependencies as Record<string, string> | undefined
106
+ const devDeps = pkg.devDependencies as Record<string, string> | undefined
107
+ return [...Object.keys(deps || {}), ...Object.keys(devDeps || {})]
108
+ }
109
+
110
+ /**
111
+ * Detect auth library being used
112
+ */
113
+ private detectAuthLibrary(pkg: Record<string, unknown>): string | undefined {
114
+ const authLibraries = [
115
+ { name: 'next-auth', dep: 'next-auth' },
116
+ { name: 'better-auth', dep: 'better-auth' },
117
+ { name: 'clerk', dep: '@clerk/nextjs' },
118
+ { name: 'auth0', dep: '@auth0/nextjs-auth0' },
119
+ { name: 'supabase', dep: '@supabase/auth-helpers-nextjs' },
120
+ { name: 'lucia', dep: 'lucia' },
121
+ { name: 'kinde', dep: '@kinde-oss/kinde-auth-nextjs' },
122
+ ]
123
+
124
+ for (const lib of authLibraries) {
125
+ if (this.hasDependency(pkg, lib.dep)) {
126
+ return lib.name
127
+ }
128
+ }
129
+
130
+ return undefined
131
+ }
132
+
133
+ /**
134
+ * Detect database library being used
135
+ */
136
+ private detectDatabaseLibrary(pkg: Record<string, unknown>): string | undefined {
137
+ const dbLibraries = [
138
+ { name: 'prisma', dep: '@prisma/client' },
139
+ { name: 'drizzle', dep: 'drizzle-orm' },
140
+ { name: 'typeorm', dep: 'typeorm' },
141
+ { name: 'mongoose', dep: 'mongoose' },
142
+ { name: 'knex', dep: 'knex' },
143
+ { name: 'sequelize', dep: 'sequelize' },
144
+ { name: 'kysely', dep: 'kysely' },
145
+ ]
146
+
147
+ for (const lib of dbLibraries) {
148
+ if (this.hasDependency(pkg, lib.dep)) {
149
+ return lib.name
150
+ }
151
+ }
152
+
153
+ return undefined
154
+ }
155
+
156
+ /**
157
+ * Get migration recommendations based on analysis
158
+ */
159
+ getRecommendations(analysis: NextjsAnalysis): string[] {
160
+ const recommendations: string[] = []
161
+
162
+ if (analysis.routerType === 'pages') {
163
+ recommendations.push('Consider migrating to App Router for best OpenSaaS Stack integration')
164
+ }
165
+
166
+ if (analysis.authLibrary && analysis.authLibrary !== 'better-auth') {
167
+ recommendations.push(
168
+ `Consider migrating from ${analysis.authLibrary} to Better-auth (used by OpenSaaS Stack auth plugin)`,
169
+ )
170
+ }
171
+
172
+ if (!analysis.hasPrisma) {
173
+ recommendations.push("OpenSaaS Stack uses Prisma - you'll need to set up your data models")
174
+ }
175
+
176
+ if (analysis.databaseLibrary && analysis.databaseLibrary !== 'prisma') {
177
+ recommendations.push(
178
+ `You're using ${analysis.databaseLibrary} - you may need to migrate to Prisma or run both`,
179
+ )
180
+ }
181
+
182
+ if (!analysis.hasEnvFile) {
183
+ recommendations.push('Create a .env file with DATABASE_URL for your database connection')
184
+ }
185
+
186
+ return recommendations
187
+ }
188
+
189
+ /**
190
+ * Get warnings for potential issues
191
+ */
192
+ getWarnings(analysis: NextjsAnalysis): string[] {
193
+ const warnings: string[] = []
194
+
195
+ if (analysis.version.startsWith('12') || analysis.version.startsWith('11')) {
196
+ warnings.push(
197
+ `Next.js ${analysis.version} is quite old - consider upgrading to 14+ for best results`,
198
+ )
199
+ }
200
+
201
+ if (analysis.databaseLibrary === 'mongoose') {
202
+ warnings.push(
203
+ 'MongoDB/Mongoose is not fully supported by Prisma - migration may require database change',
204
+ )
205
+ }
206
+
207
+ return warnings
208
+ }
209
+ }