@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.
Files changed (61) hide show
  1. package/README.md +76 -0
  2. package/dist/commands/migrate.d.ts.map +1 -1
  3. package/dist/commands/migrate.js +91 -265
  4. package/dist/commands/migrate.js.map +1 -1
  5. package/package.json +7 -2
  6. package/plugin/.claude-plugin/plugin.json +15 -0
  7. package/plugin/README.md +112 -0
  8. package/plugin/agents/migration-assistant.md +150 -0
  9. package/plugin/commands/analyze-schema.md +34 -0
  10. package/plugin/commands/generate-config.md +33 -0
  11. package/plugin/commands/validate-migration.md +34 -0
  12. package/plugin/skills/opensaas-migration/SKILL.md +192 -0
  13. package/.turbo/turbo-build.log +0 -4
  14. package/CHANGELOG.md +0 -462
  15. package/CLAUDE.md +0 -298
  16. package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
  17. package/src/commands/dev.test.ts +0 -215
  18. package/src/commands/dev.ts +0 -48
  19. package/src/commands/generate.test.ts +0 -282
  20. package/src/commands/generate.ts +0 -182
  21. package/src/commands/init.ts +0 -34
  22. package/src/commands/mcp.ts +0 -135
  23. package/src/commands/migrate.ts +0 -534
  24. package/src/generator/__snapshots__/context.test.ts.snap +0 -361
  25. package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
  26. package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
  27. package/src/generator/context.test.ts +0 -139
  28. package/src/generator/context.ts +0 -227
  29. package/src/generator/index.ts +0 -7
  30. package/src/generator/lists.test.ts +0 -335
  31. package/src/generator/lists.ts +0 -140
  32. package/src/generator/plugin-types.ts +0 -147
  33. package/src/generator/prisma-config.ts +0 -46
  34. package/src/generator/prisma-extensions.ts +0 -159
  35. package/src/generator/prisma.test.ts +0 -211
  36. package/src/generator/prisma.ts +0 -161
  37. package/src/generator/types.test.ts +0 -268
  38. package/src/generator/types.ts +0 -537
  39. package/src/index.ts +0 -46
  40. package/src/mcp/lib/documentation-provider.ts +0 -710
  41. package/src/mcp/lib/features/catalog.ts +0 -301
  42. package/src/mcp/lib/generators/feature-generator.ts +0 -598
  43. package/src/mcp/lib/types.ts +0 -89
  44. package/src/mcp/lib/wizards/migration-wizard.ts +0 -584
  45. package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
  46. package/src/mcp/server/index.ts +0 -361
  47. package/src/mcp/server/stack-mcp-server.ts +0 -544
  48. package/src/migration/generators/migration-generator.ts +0 -675
  49. package/src/migration/introspectors/index.ts +0 -12
  50. package/src/migration/introspectors/keystone-introspector.ts +0 -296
  51. package/src/migration/introspectors/nextjs-introspector.ts +0 -209
  52. package/src/migration/introspectors/prisma-introspector.ts +0 -233
  53. package/src/migration/types.ts +0 -92
  54. package/tests/introspectors/keystone-introspector.test.ts +0 -255
  55. package/tests/introspectors/nextjs-introspector.test.ts +0 -302
  56. package/tests/introspectors/prisma-introspector.test.ts +0 -268
  57. package/tests/migration-generator.test.ts +0 -592
  58. package/tests/migration-wizard.test.ts +0 -442
  59. package/tsconfig.json +0 -13
  60. package/tsconfig.tsbuildinfo +0 -1
  61. package/vitest.config.ts +0 -26
@@ -1,302 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import { NextjsIntrospector } from '../../src/migration/introspectors/nextjs-introspector.js'
3
- import fs from 'fs-extra'
4
- import path from 'path'
5
- import os from 'os'
6
-
7
- describe('NextjsIntrospector', () => {
8
- let introspector: NextjsIntrospector
9
- let tempDir: string
10
-
11
- beforeEach(async () => {
12
- introspector = new NextjsIntrospector()
13
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'nextjs-test-'))
14
- })
15
-
16
- afterEach(async () => {
17
- await fs.remove(tempDir)
18
- })
19
-
20
- it('should detect Next.js version', async () => {
21
- const pkg = {
22
- dependencies: {
23
- next: '^14.0.0',
24
- },
25
- }
26
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
27
-
28
- const result = await introspector.introspect(tempDir)
29
-
30
- expect(result.version).toBe('14.0.0')
31
- })
32
-
33
- it('should detect app router', async () => {
34
- const pkg = {
35
- dependencies: {
36
- next: '14.0.0',
37
- },
38
- }
39
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
40
- await fs.ensureDir(path.join(tempDir, 'app'))
41
-
42
- const result = await introspector.introspect(tempDir)
43
-
44
- expect(result.routerType).toBe('app')
45
- })
46
-
47
- it('should detect pages router', async () => {
48
- const pkg = {
49
- dependencies: {
50
- next: '14.0.0',
51
- },
52
- }
53
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
54
- await fs.ensureDir(path.join(tempDir, 'pages'))
55
-
56
- const result = await introspector.introspect(tempDir)
57
-
58
- expect(result.routerType).toBe('pages')
59
- })
60
-
61
- it('should detect both routers', async () => {
62
- const pkg = {
63
- dependencies: {
64
- next: '14.0.0',
65
- },
66
- }
67
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
68
- await fs.ensureDir(path.join(tempDir, 'app'))
69
- await fs.ensureDir(path.join(tempDir, 'pages'))
70
-
71
- const result = await introspector.introspect(tempDir)
72
-
73
- expect(result.routerType).toBe('both')
74
- })
75
-
76
- it('should detect TypeScript usage', async () => {
77
- const pkg = {
78
- dependencies: {
79
- next: '14.0.0',
80
- },
81
- }
82
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
83
- await fs.writeJSON(path.join(tempDir, 'tsconfig.json'), {
84
- compilerOptions: {
85
- target: 'es2015',
86
- },
87
- })
88
-
89
- const result = await introspector.introspect(tempDir)
90
-
91
- expect(result.typescript).toBe(true)
92
- })
93
-
94
- it('should detect auth libraries', async () => {
95
- const pkg = {
96
- dependencies: {
97
- next: '14.0.0',
98
- 'next-auth': '^4.0.0',
99
- },
100
- }
101
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
102
-
103
- const result = await introspector.introspect(tempDir)
104
-
105
- expect(result.authLibrary).toBe('next-auth')
106
- })
107
-
108
- it('should detect better-auth', async () => {
109
- const pkg = {
110
- dependencies: {
111
- next: '14.0.0',
112
- 'better-auth': '^1.0.0',
113
- },
114
- }
115
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
116
-
117
- const result = await introspector.introspect(tempDir)
118
-
119
- expect(result.authLibrary).toBe('better-auth')
120
- })
121
-
122
- it('should detect database libraries', async () => {
123
- const pkg = {
124
- dependencies: {
125
- next: '14.0.0',
126
- '@prisma/client': '^5.0.0',
127
- },
128
- }
129
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
130
-
131
- const result = await introspector.introspect(tempDir)
132
-
133
- expect(result.databaseLibrary).toBe('prisma')
134
- expect(result.hasPrisma).toBe(true)
135
- })
136
-
137
- it('should detect Prisma directory', async () => {
138
- const pkg = {
139
- dependencies: {
140
- next: '14.0.0',
141
- },
142
- }
143
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
144
- await fs.ensureDir(path.join(tempDir, 'prisma'))
145
-
146
- const result = await introspector.introspect(tempDir)
147
-
148
- expect(result.hasPrisma).toBe(true)
149
- })
150
-
151
- it('should detect .env file', async () => {
152
- const pkg = {
153
- dependencies: {
154
- next: '14.0.0',
155
- },
156
- }
157
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
158
- await fs.writeFile(path.join(tempDir, '.env'), 'DATABASE_URL=...')
159
-
160
- const result = await introspector.introspect(tempDir)
161
-
162
- expect(result.hasEnvFile).toBe(true)
163
- })
164
-
165
- it('should list all dependencies', async () => {
166
- const pkg = {
167
- dependencies: {
168
- next: '14.0.0',
169
- react: '18.0.0',
170
- },
171
- devDependencies: {
172
- typescript: '5.0.0',
173
- },
174
- }
175
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
176
-
177
- const result = await introspector.introspect(tempDir)
178
-
179
- expect(result.existingDependencies).toContain('next')
180
- expect(result.existingDependencies).toContain('react')
181
- expect(result.existingDependencies).toContain('typescript')
182
- })
183
-
184
- it('should generate recommendations for pages router', async () => {
185
- const pkg = {
186
- dependencies: {
187
- next: '14.0.0',
188
- },
189
- }
190
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
191
- await fs.ensureDir(path.join(tempDir, 'pages'))
192
-
193
- const result = await introspector.introspect(tempDir)
194
- const recommendations = introspector.getRecommendations(result)
195
-
196
- expect(recommendations.some((r) => r.includes('App Router'))).toBe(true)
197
- })
198
-
199
- it('should recommend Better-auth for non-better-auth projects', async () => {
200
- const pkg = {
201
- dependencies: {
202
- next: '14.0.0',
203
- 'next-auth': '4.0.0',
204
- },
205
- }
206
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
207
-
208
- const result = await introspector.introspect(tempDir)
209
- const recommendations = introspector.getRecommendations(result)
210
-
211
- expect(recommendations.some((r) => r.includes('Better-auth'))).toBe(true)
212
- })
213
-
214
- it('should recommend Prisma setup if missing', async () => {
215
- const pkg = {
216
- dependencies: {
217
- next: '14.0.0',
218
- },
219
- }
220
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
221
-
222
- const result = await introspector.introspect(tempDir)
223
- const recommendations = introspector.getRecommendations(result)
224
-
225
- expect(recommendations.some((r) => r.includes('Prisma'))).toBe(true)
226
- })
227
-
228
- it('should generate warnings for old Next.js versions', async () => {
229
- const pkg = {
230
- dependencies: {
231
- next: '12.0.0',
232
- },
233
- }
234
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
235
-
236
- const result = await introspector.introspect(tempDir)
237
- const warnings = introspector.getWarnings(result)
238
-
239
- expect(warnings.some((w) => w.includes('12.0.0'))).toBe(true)
240
- })
241
-
242
- it('should warn about MongoDB/Mongoose', async () => {
243
- const pkg = {
244
- dependencies: {
245
- next: '14.0.0',
246
- mongoose: '7.0.0',
247
- },
248
- }
249
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
250
-
251
- const result = await introspector.introspect(tempDir)
252
- const warnings = introspector.getWarnings(result)
253
-
254
- expect(warnings.some((w) => w.includes('MongoDB'))).toBe(true)
255
- })
256
-
257
- it('should throw for missing package.json', async () => {
258
- await expect(introspector.introspect(tempDir)).rejects.toThrow('package.json not found')
259
- })
260
-
261
- it('should detect router in src directory', async () => {
262
- const pkg = {
263
- dependencies: {
264
- next: '14.0.0',
265
- },
266
- }
267
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
268
- await fs.ensureDir(path.join(tempDir, 'src', 'app'))
269
-
270
- const result = await introspector.introspect(tempDir)
271
-
272
- expect(result.routerType).toBe('app')
273
- })
274
-
275
- it('should handle multiple auth libraries', async () => {
276
- const pkg = {
277
- dependencies: {
278
- next: '14.0.0',
279
- '@clerk/nextjs': '4.0.0',
280
- },
281
- }
282
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
283
-
284
- const result = await introspector.introspect(tempDir)
285
-
286
- expect(result.authLibrary).toBe('clerk')
287
- })
288
-
289
- it('should detect drizzle', async () => {
290
- const pkg = {
291
- dependencies: {
292
- next: '14.0.0',
293
- 'drizzle-orm': '0.28.0',
294
- },
295
- }
296
- await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
297
-
298
- const result = await introspector.introspect(tempDir)
299
-
300
- expect(result.databaseLibrary).toBe('drizzle')
301
- })
302
- })
@@ -1,268 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import { PrismaIntrospector } from '../../src/migration/introspectors/prisma-introspector.js'
3
- import fs from 'fs-extra'
4
- import path from 'path'
5
- import os from 'os'
6
-
7
- describe('PrismaIntrospector', () => {
8
- let introspector: PrismaIntrospector
9
- let tempDir: string
10
-
11
- beforeEach(async () => {
12
- introspector = new PrismaIntrospector()
13
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prisma-test-'))
14
- await fs.ensureDir(path.join(tempDir, 'prisma'))
15
- })
16
-
17
- afterEach(async () => {
18
- await fs.remove(tempDir)
19
- })
20
-
21
- it('should parse a simple schema', async () => {
22
- const schema = `
23
- datasource db {
24
- provider = "sqlite"
25
- url = "file:./dev.db"
26
- }
27
-
28
- model User {
29
- id String @id @default(cuid())
30
- email String @unique
31
- name String?
32
- posts Post[]
33
- }
34
-
35
- model Post {
36
- id String @id @default(cuid())
37
- title String
38
- author User @relation(fields: [authorId], references: [id])
39
- authorId String
40
- }
41
- `
42
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
43
-
44
- const result = await introspector.introspect(tempDir)
45
-
46
- expect(result.provider).toBe('sqlite')
47
- expect(result.models).toHaveLength(2)
48
-
49
- const user = result.models.find((m) => m.name === 'User')
50
- expect(user).toBeDefined()
51
- expect(user!.fields).toHaveLength(4)
52
-
53
- const post = result.models.find((m) => m.name === 'Post')
54
- expect(post).toBeDefined()
55
- expect(post!.hasRelations).toBe(true)
56
- })
57
-
58
- it('should parse enums', async () => {
59
- const schema = `
60
- datasource db {
61
- provider = "postgresql"
62
- url = env("DATABASE_URL")
63
- }
64
-
65
- enum Role {
66
- USER
67
- ADMIN
68
- MODERATOR
69
- }
70
-
71
- model User {
72
- id String @id
73
- role Role @default(USER)
74
- }
75
- `
76
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
77
-
78
- const result = await introspector.introspect(tempDir)
79
-
80
- expect(result.enums).toHaveLength(1)
81
- expect(result.enums[0].name).toBe('Role')
82
- expect(result.enums[0].values).toEqual(['USER', 'ADMIN', 'MODERATOR'])
83
- })
84
-
85
- it('should handle optional and list fields', async () => {
86
- const schema = `
87
- datasource db {
88
- provider = "sqlite"
89
- url = "file:./dev.db"
90
- }
91
-
92
- model User {
93
- id String @id
94
- name String?
95
- emails String[]
96
- isActive Boolean @default(true)
97
- }
98
- `
99
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
100
-
101
- const result = await introspector.introspect(tempDir)
102
- const user = result.models[0]
103
-
104
- const nameField = user.fields.find((f) => f.name === 'name')
105
- expect(nameField!.isRequired).toBe(false)
106
-
107
- const emailsField = user.fields.find((f) => f.name === 'emails')
108
- expect(emailsField!.isList).toBe(true)
109
-
110
- const isActiveField = user.fields.find((f) => f.name === 'isActive')
111
- expect(isActiveField!.defaultValue).toBe('true')
112
- })
113
-
114
- it('should parse field attributes correctly', async () => {
115
- const schema = `
116
- datasource db {
117
- provider = "postgresql"
118
- url = env("DATABASE_URL")
119
- }
120
-
121
- model User {
122
- id String @id @default(cuid())
123
- email String @unique
124
- username String
125
- age Int
126
- }
127
- `
128
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
129
-
130
- const result = await introspector.introspect(tempDir)
131
- const user = result.models[0]
132
-
133
- const idField = user.fields.find((f) => f.name === 'id')
134
- expect(idField!.isId).toBe(true)
135
- expect(idField!.defaultValue).toBe('cuid()')
136
-
137
- const emailField = user.fields.find((f) => f.name === 'email')
138
- expect(emailField!.isUnique).toBe(true)
139
- })
140
-
141
- it('should parse relationships correctly', async () => {
142
- const schema = `
143
- datasource db {
144
- provider = "sqlite"
145
- url = "file:./dev.db"
146
- }
147
-
148
- model Post {
149
- id String @id
150
- author User @relation(fields: [authorId], references: [id])
151
- authorId String
152
- }
153
-
154
- model User {
155
- id String @id
156
- posts Post[]
157
- }
158
- `
159
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
160
-
161
- const result = await introspector.introspect(tempDir)
162
- const post = result.models.find((m) => m.name === 'Post')
163
-
164
- const authorField = post!.fields.find((f) => f.name === 'author')
165
- expect(authorField!.relation).toBeDefined()
166
- expect(authorField!.relation!.model).toBe('User')
167
- expect(authorField!.relation!.fields).toEqual(['authorId'])
168
- expect(authorField!.relation!.references).toEqual(['id'])
169
- })
170
-
171
- it('should map Prisma types to OpenSaaS types', () => {
172
- expect(introspector.mapPrismaTypeToOpenSaas('String')).toEqual({ type: 'text', import: 'text' })
173
- expect(introspector.mapPrismaTypeToOpenSaas('Int')).toEqual({
174
- type: 'integer',
175
- import: 'integer',
176
- })
177
- expect(introspector.mapPrismaTypeToOpenSaas('Boolean')).toEqual({
178
- type: 'checkbox',
179
- import: 'checkbox',
180
- })
181
- expect(introspector.mapPrismaTypeToOpenSaas('DateTime')).toEqual({
182
- type: 'timestamp',
183
- import: 'timestamp',
184
- })
185
- expect(introspector.mapPrismaTypeToOpenSaas('Json')).toEqual({ type: 'json', import: 'json' })
186
- })
187
-
188
- it('should generate warnings for unsupported types', async () => {
189
- const schema = `
190
- datasource db {
191
- provider = "postgresql"
192
- url = env("DATABASE_URL")
193
- }
194
-
195
- model Data {
196
- id String @id
197
- bigValue BigInt
198
- decimal Decimal
199
- bytes Bytes
200
- }
201
- `
202
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
203
-
204
- const result = await introspector.introspect(tempDir)
205
- const warnings = introspector.getWarnings(result)
206
-
207
- expect(warnings).toHaveLength(3)
208
- expect(warnings[0]).toContain('BigInt')
209
- expect(warnings[1]).toContain('Decimal')
210
- expect(warnings[2]).toContain('Bytes')
211
- })
212
-
213
- it('should throw for missing schema', async () => {
214
- await expect(introspector.introspect(tempDir, 'nonexistent.prisma')).rejects.toThrow(
215
- 'Schema file not found',
216
- )
217
- })
218
-
219
- it('should handle comments in schema', async () => {
220
- const schema = `
221
- datasource db {
222
- provider = "sqlite"
223
- url = "file:./dev.db"
224
- }
225
-
226
- // This is a comment
227
- model User {
228
- id String @id // Comment after field
229
- // Another comment
230
- email String
231
- }
232
- `
233
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
234
-
235
- const result = await introspector.introspect(tempDir)
236
- const user = result.models[0]
237
-
238
- expect(user.fields).toHaveLength(2)
239
- expect(user.fields.find((f) => f.name === 'id')).toBeDefined()
240
- expect(user.fields.find((f) => f.name === 'email')).toBeDefined()
241
- })
242
-
243
- it('should handle model-level attributes', async () => {
244
- const schema = `
245
- datasource db {
246
- provider = "postgresql"
247
- url = env("DATABASE_URL")
248
- }
249
-
250
- model User {
251
- firstName String
252
- lastName String
253
-
254
- @@unique([firstName, lastName])
255
- @@index([lastName])
256
- }
257
- `
258
- await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
259
-
260
- const result = await introspector.introspect(tempDir)
261
- const user = result.models[0]
262
-
263
- // Should only include actual fields, not model-level attributes
264
- expect(user.fields).toHaveLength(2)
265
- expect(user.fields.find((f) => f.name === 'firstName')).toBeDefined()
266
- expect(user.fields.find((f) => f.name === 'lastName')).toBeDefined()
267
- })
268
- })