@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,442 +0,0 @@
1
- /**
2
- * Tests for MigrationWizard
3
- */
4
-
5
- import { describe, it, expect, beforeEach } from 'vitest'
6
- import { MigrationWizard } from '../src/mcp/lib/wizards/migration-wizard.js'
7
- import type { IntrospectedSchema } from '../src/migration/types.js'
8
-
9
- describe('MigrationWizard', () => {
10
- let wizard: MigrationWizard
11
-
12
- beforeEach(() => {
13
- wizard = new MigrationWizard()
14
- })
15
-
16
- describe('startMigration', () => {
17
- it('should start a migration session without analysis', async () => {
18
- const result = await wizard.startMigration('prisma')
19
-
20
- expect(result.content).toHaveLength(1)
21
- expect(result.content[0].text).toContain('Migration Wizard')
22
- expect(result.content[0].text).toContain('**Project Type:** prisma')
23
- expect(result.content[0].text).toContain('sessionId')
24
- })
25
-
26
- it('should start a migration session with analysis', async () => {
27
- const analysis: IntrospectedSchema = {
28
- provider: 'postgresql',
29
- models: [
30
- {
31
- name: 'Post',
32
- fields: [
33
- {
34
- name: 'id',
35
- type: 'String',
36
- isRequired: true,
37
- isUnique: true,
38
- isId: true,
39
- isList: false,
40
- },
41
- {
42
- name: 'title',
43
- type: 'String',
44
- isRequired: true,
45
- isUnique: false,
46
- isId: false,
47
- isList: false,
48
- },
49
- ],
50
- hasRelations: false,
51
- primaryKey: 'id',
52
- },
53
- ],
54
- enums: [],
55
- }
56
-
57
- const result = await wizard.startMigration('prisma', analysis)
58
-
59
- expect(result.content[0].text).toContain('**Models:** 1')
60
- expect(result.content[0].text).toContain('**Database:** postgresql')
61
- })
62
-
63
- it('should extract session ID from response', async () => {
64
- const result = await wizard.startMigration('prisma')
65
- const sessionIdMatch = result.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
66
-
67
- expect(sessionIdMatch).toBeDefined()
68
- expect(sessionIdMatch![1]).toMatch(/^migration_\d+_[a-z0-9]+$/)
69
- })
70
- })
71
-
72
- describe('answerQuestion', () => {
73
- it('should handle invalid session ID', async () => {
74
- const result = await wizard.answerQuestion('invalid-session-id', true)
75
-
76
- expect(result.content[0].text).toContain('Session not found')
77
- })
78
-
79
- it('should accept valid boolean answer and progress to next question', async () => {
80
- const startResult = await wizard.startMigration('prisma')
81
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
82
- const sessionId = sessionIdMatch![1]
83
-
84
- // Answer first question (boolean: preserve_database)
85
- const answerResult = await wizard.answerQuestion(sessionId, true)
86
-
87
- expect(answerResult.content[0].text).toContain('Recorded')
88
- expect(answerResult.content[0].text).toContain('Question 2')
89
- expect(answerResult.content[0].text).toContain('Progress')
90
- })
91
-
92
- it('should validate boolean answers', async () => {
93
- const startResult = await wizard.startMigration('prisma')
94
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
95
- const sessionId = sessionIdMatch![1]
96
-
97
- // Try invalid boolean answer
98
- const invalidResult = await wizard.answerQuestion(sessionId, 'maybe')
99
-
100
- expect(invalidResult.content[0].text).toContain('Invalid answer')
101
- expect(invalidResult.content[0].text).toContain('yes/no')
102
- })
103
-
104
- it('should accept yes/no as boolean values', async () => {
105
- const startResult = await wizard.startMigration('prisma')
106
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
107
- const sessionId = sessionIdMatch![1]
108
-
109
- // Answer with "yes"
110
- const answerResult = await wizard.answerQuestion(sessionId, 'yes')
111
-
112
- expect(answerResult.content[0].text).toContain('Recorded')
113
- expect(answerResult.content[0].text).toContain('Question 2')
114
- })
115
-
116
- it('should validate select answers', async () => {
117
- const startResult = await wizard.startMigration('prisma')
118
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
119
- const sessionId = sessionIdMatch![1]
120
-
121
- // Answer first question (boolean)
122
- await wizard.answerQuestion(sessionId, true)
123
-
124
- // Try invalid select answer for db_provider
125
- const invalidResult = await wizard.answerQuestion(sessionId, 'mongodb')
126
-
127
- expect(invalidResult.content[0].text).toContain('Invalid')
128
- expect(invalidResult.content[0].text).toContain('sqlite, postgresql, mysql')
129
- })
130
-
131
- it('should accept valid select answer', async () => {
132
- const startResult = await wizard.startMigration('prisma')
133
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
134
- const sessionId = sessionIdMatch![1]
135
-
136
- // Answer first question
137
- await wizard.answerQuestion(sessionId, true)
138
-
139
- // Answer second question with valid select
140
- const answerResult = await wizard.answerQuestion(sessionId, 'postgresql')
141
-
142
- expect(answerResult.content[0].text).toContain('Recorded')
143
- expect(answerResult.content[0].text).toContain('Question 3')
144
- })
145
-
146
- it('should validate multiselect answers', async () => {
147
- const startResult = await wizard.startMigration('prisma')
148
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
149
- const sessionId = sessionIdMatch![1]
150
-
151
- // Progress to auth_methods question (multiselect)
152
- await wizard.answerQuestion(sessionId, true) // preserve_database
153
- await wizard.answerQuestion(sessionId, 'sqlite') // db_provider
154
- await wizard.answerQuestion(sessionId, true) // enable_auth
155
-
156
- // Try invalid multiselect
157
- const invalidResult = await wizard.answerQuestion(sessionId, 'invalid-method')
158
-
159
- expect(invalidResult.content[0].text).toContain('Invalid')
160
- })
161
-
162
- it('should accept valid multiselect answers as array', async () => {
163
- const startResult = await wizard.startMigration('prisma')
164
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
165
- const sessionId = sessionIdMatch![1]
166
-
167
- // Progress to auth_methods question
168
- await wizard.answerQuestion(sessionId, true)
169
- await wizard.answerQuestion(sessionId, 'sqlite')
170
- await wizard.answerQuestion(sessionId, true)
171
-
172
- // Answer with array
173
- const answerResult = await wizard.answerQuestion(sessionId, ['email-password', 'google'])
174
-
175
- expect(answerResult.content[0].text).toContain('Recorded')
176
- expect(answerResult.content[0].text).toContain('email-password, google')
177
- })
178
-
179
- it('should accept valid multiselect answers as comma-separated string', async () => {
180
- const startResult = await wizard.startMigration('prisma')
181
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
182
- const sessionId = sessionIdMatch![1]
183
-
184
- // Progress to auth_methods question
185
- await wizard.answerQuestion(sessionId, true)
186
- await wizard.answerQuestion(sessionId, 'sqlite')
187
- await wizard.answerQuestion(sessionId, true)
188
-
189
- // Answer with comma-separated string
190
- const answerResult = await wizard.answerQuestion(sessionId, 'email-password, github')
191
-
192
- expect(answerResult.content[0].text).toContain('Recorded')
193
- })
194
- })
195
-
196
- describe('generateMigrationConfig', () => {
197
- it('should complete wizard and generate config', async () => {
198
- const startResult = await wizard.startMigration('prisma')
199
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
200
- const sessionId = sessionIdMatch![1]
201
-
202
- // Answer all questions
203
- await wizard.answerQuestion(sessionId, true) // preserve_database
204
- await wizard.answerQuestion(sessionId, 'sqlite') // db_provider
205
- await wizard.answerQuestion(sessionId, 'no') // enable_auth (use string instead of boolean)
206
- await wizard.answerQuestion(sessionId, 'public-read-auth-write') // default_access
207
- await wizard.answerQuestion(sessionId, '/admin') // admin_base_path
208
- await wizard.answerQuestion(sessionId, []) // additional_features
209
- const finalResult = await wizard.answerQuestion(sessionId, 'yes') // confirm (use string instead of boolean)
210
-
211
- expect(finalResult.content[0].text).toContain('Migration Complete')
212
- expect(finalResult.content[0].text).toContain('opensaas.config.ts')
213
- expect(finalResult.content[0].text).toContain('Install Dependencies')
214
- expect(finalResult.content[0].text).toContain('Next Steps')
215
- })
216
-
217
- it('should include auth dependencies when auth is enabled', async () => {
218
- const startResult = await wizard.startMigration('prisma')
219
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
220
- const sessionId = sessionIdMatch![1]
221
-
222
- // Answer all questions with auth enabled
223
- await wizard.answerQuestion(sessionId, true)
224
- await wizard.answerQuestion(sessionId, 'postgresql')
225
- await wizard.answerQuestion(sessionId, true) // enable_auth = true
226
- await wizard.answerQuestion(sessionId, ['email-password'])
227
- await wizard.answerQuestion(sessionId, 'authenticated-only')
228
- await wizard.answerQuestion(sessionId, '/admin')
229
- await wizard.answerQuestion(sessionId, [])
230
- const finalResult = await wizard.answerQuestion(sessionId, true)
231
-
232
- expect(finalResult.content[0].text).toContain('@opensaas/stack-auth')
233
- })
234
-
235
- it('should clean up session after completion', async () => {
236
- const startResult = await wizard.startMigration('prisma')
237
- const sessionIdMatch = startResult.content[0].text.match(/sessionId.*?:\s*"([^"]+)"/)
238
- const sessionId = sessionIdMatch![1]
239
-
240
- // Verify session exists
241
- expect(wizard.getSession(sessionId)).toBeDefined()
242
-
243
- // Complete wizard
244
- await wizard.answerQuestion(sessionId, 'yes')
245
- await wizard.answerQuestion(sessionId, 'sqlite')
246
- await wizard.answerQuestion(sessionId, 'no')
247
- await wizard.answerQuestion(sessionId, 'public-read-auth-write')
248
- await wizard.answerQuestion(sessionId, '/admin')
249
- await wizard.answerQuestion(sessionId, [])
250
- await wizard.answerQuestion(sessionId, 'yes')
251
-
252
- // Verify session is cleaned up
253
- expect(wizard.getSession(sessionId)).toBeUndefined()
254
- })
255
- })
256
-
257
- describe('model detection', () => {
258
- it('should ask about auth models when detected', async () => {
259
- const analysis: IntrospectedSchema = {
260
- provider: 'postgresql',
261
- models: [
262
- {
263
- name: 'User',
264
- fields: [],
265
- hasRelations: false,
266
- primaryKey: 'id',
267
- },
268
- {
269
- name: 'Account',
270
- fields: [],
271
- hasRelations: false,
272
- primaryKey: 'id',
273
- },
274
- {
275
- name: 'Post',
276
- fields: [],
277
- hasRelations: false,
278
- primaryKey: 'id',
279
- },
280
- ],
281
- enums: [],
282
- }
283
-
284
- const result = await wizard.startMigration('prisma', analysis)
285
-
286
- // The wizard should include questions about skipping auth models
287
- expect(result.content[0].text).toBeDefined()
288
- })
289
-
290
- it('should ask about owner models when User relationships detected', async () => {
291
- const analysis: IntrospectedSchema = {
292
- provider: 'postgresql',
293
- models: [
294
- {
295
- name: 'User',
296
- fields: [],
297
- hasRelations: false,
298
- primaryKey: 'id',
299
- },
300
- {
301
- name: 'Post',
302
- fields: [
303
- {
304
- name: 'author',
305
- type: 'User',
306
- isRequired: true,
307
- isUnique: false,
308
- isId: false,
309
- isList: false,
310
- relation: {
311
- name: 'PostToUser',
312
- model: 'User',
313
- fields: ['authorId'],
314
- references: ['id'],
315
- },
316
- },
317
- ],
318
- hasRelations: true,
319
- primaryKey: 'id',
320
- },
321
- ],
322
- enums: [],
323
- }
324
-
325
- const result = await wizard.startMigration('prisma', analysis)
326
-
327
- expect(result.content[0].text).toBeDefined()
328
- })
329
- })
330
-
331
- describe('session management', () => {
332
- it('should clear completed sessions', () => {
333
- const wizard = new MigrationWizard()
334
-
335
- // Create a mock completed session
336
- const session = wizard['sessions']
337
- session['test-session'] = {
338
- id: 'test-session',
339
- projectType: 'prisma',
340
- analysis: { projectTypes: ['prisma'], cwd: '.' },
341
- currentQuestionIndex: 0,
342
- answers: {},
343
- isComplete: true,
344
- createdAt: new Date(),
345
- updatedAt: new Date(),
346
- }
347
-
348
- wizard.clearCompletedSessions()
349
-
350
- expect(wizard.getSession('test-session')).toBeUndefined()
351
- })
352
-
353
- it('should not clear incomplete sessions', () => {
354
- const wizard = new MigrationWizard()
355
-
356
- // Create a mock incomplete session
357
- const session = wizard['sessions']
358
- session['test-session'] = {
359
- id: 'test-session',
360
- projectType: 'prisma',
361
- analysis: { projectTypes: ['prisma'], cwd: '.' },
362
- currentQuestionIndex: 0,
363
- answers: {},
364
- isComplete: false,
365
- createdAt: new Date(),
366
- updatedAt: new Date(),
367
- }
368
-
369
- wizard.clearCompletedSessions()
370
-
371
- expect(wizard.getSession('test-session')).toBeDefined()
372
- })
373
-
374
- it('should clear old sessions', () => {
375
- const wizard = new MigrationWizard()
376
-
377
- // Create a mock old session (2 hours ago)
378
- const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000)
379
- const session = wizard['sessions']
380
- session['old-session'] = {
381
- id: 'old-session',
382
- projectType: 'prisma',
383
- analysis: { projectTypes: ['prisma'], cwd: '.' },
384
- currentQuestionIndex: 0,
385
- answers: {},
386
- isComplete: false,
387
- createdAt: twoHoursAgo,
388
- updatedAt: twoHoursAgo,
389
- }
390
-
391
- wizard.clearOldSessions()
392
-
393
- expect(wizard.getSession('old-session')).toBeUndefined()
394
- })
395
-
396
- it('should not clear recent sessions', () => {
397
- const wizard = new MigrationWizard()
398
-
399
- // Create a mock recent session (5 minutes ago)
400
- const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000)
401
- const session = wizard['sessions']
402
- session['recent-session'] = {
403
- id: 'recent-session',
404
- projectType: 'prisma',
405
- analysis: { projectTypes: ['prisma'], cwd: '.' },
406
- currentQuestionIndex: 0,
407
- answers: {},
408
- isComplete: false,
409
- createdAt: fiveMinutesAgo,
410
- updatedAt: fiveMinutesAgo,
411
- }
412
-
413
- wizard.clearOldSessions()
414
-
415
- expect(wizard.getSession('recent-session')).toBeDefined()
416
- })
417
- })
418
-
419
- describe('answer formatting', () => {
420
- it('should format boolean answers', () => {
421
- expect(wizard['formatAnswer'](true)).toBe('Yes')
422
- expect(wizard['formatAnswer'](false)).toBe('No')
423
- })
424
-
425
- it('should format array answers', () => {
426
- expect(wizard['formatAnswer'](['a', 'b', 'c'])).toBe('a, b, c')
427
- expect(wizard['formatAnswer']([])).toBe('(none)')
428
- })
429
-
430
- it('should format string answers', () => {
431
- expect(wizard['formatAnswer']('test')).toBe('test')
432
- })
433
- })
434
-
435
- describe('progress bar', () => {
436
- it('should render progress bar correctly', () => {
437
- expect(wizard['renderProgressBar'](0, 10)).toBe('░░░░░░░░░░')
438
- expect(wizard['renderProgressBar'](5, 10)).toBe('▓▓▓▓▓░░░░░')
439
- expect(wizard['renderProgressBar'](10, 10)).toBe('▓▓▓▓▓▓▓▓▓▓')
440
- })
441
- })
442
- })
package/tsconfig.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "composite": true,
7
- "declaration": true,
8
- "declarationMap": true,
9
- "esModuleInterop": true
10
- },
11
- "include": ["src/**/*"],
12
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
13
- }