@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,215 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
- import * as fs from 'fs'
3
- import * as path from 'path'
4
- import * as os from 'os'
5
-
6
- // Mock chokidar
7
- const mockWatcherOn = vi.fn()
8
- const mockWatcherClose = vi.fn()
9
- const mockWatch = vi.fn(() => ({
10
- on: mockWatcherOn,
11
- close: mockWatcherClose,
12
- }))
13
-
14
- vi.mock('chokidar', () => ({
15
- default: {
16
- watch: mockWatch,
17
- },
18
- }))
19
-
20
- // Mock the generate command
21
- vi.mock('./generate.js', () => ({
22
- generateCommand: vi.fn().mockResolvedValue(undefined),
23
- }))
24
-
25
- // Mock ora
26
- vi.mock('ora', () => ({
27
- default: vi.fn(() => ({
28
- start: vi.fn().mockReturnThis(),
29
- succeed: vi.fn().mockReturnThis(),
30
- fail: vi.fn().mockReturnThis(),
31
- text: '',
32
- })),
33
- }))
34
-
35
- // Mock chalk
36
- vi.mock('chalk', () => ({
37
- default: {
38
- bold: {
39
- cyan: vi.fn((str) => str),
40
- },
41
- cyan: vi.fn((str) => str),
42
- gray: vi.fn((str) => str),
43
- red: vi.fn((str) => str),
44
- yellow: vi.fn((str) => str),
45
- },
46
- }))
47
-
48
- describe('Dev Command', () => {
49
- let tempDir: string
50
- let originalCwd: string
51
- let originalExit: typeof process.exit
52
- let exitCode: number | undefined
53
-
54
- beforeEach(() => {
55
- vi.clearAllMocks()
56
-
57
- // Create temp directory
58
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dev-test-'))
59
- originalCwd = process.cwd()
60
- process.chdir(tempDir)
61
-
62
- // Mock process.exit
63
- originalExit = process.exit
64
- exitCode = undefined
65
- process.exit = vi.fn((code?: number) => {
66
- exitCode = code
67
- throw new Error(`process.exit(${code})`)
68
- }) as never
69
-
70
- // Create opensaas.config.ts file
71
- fs.writeFileSync(
72
- path.join(tempDir, 'opensaas.config.ts'),
73
- `
74
- import { config } from '@opensaas/stack-core'
75
- export default config({
76
- lists: {}
77
- })
78
- `,
79
- )
80
- })
81
-
82
- afterEach(() => {
83
- // Restore
84
- process.chdir(originalCwd)
85
- process.exit = originalExit
86
-
87
- // Clean up
88
- if (fs.existsSync(tempDir)) {
89
- fs.rmSync(tempDir, { recursive: true, force: true })
90
- }
91
- })
92
-
93
- describe('devCommand', () => {
94
- it('should fail if config file does not exist', async () => {
95
- // Remove config file
96
- fs.unlinkSync(path.join(tempDir, 'opensaas.config.ts'))
97
-
98
- const { devCommand } = await import('./dev.js')
99
-
100
- try {
101
- await devCommand()
102
- } catch {
103
- // Expected to throw
104
- }
105
-
106
- expect(exitCode).toBe(1)
107
- })
108
-
109
- it('should call generateCommand initially', async () => {
110
- const { generateCommand } = await import('./generate.js')
111
- const { devCommand } = await import('./dev.js')
112
-
113
- // Run dev command in background (don't await)
114
- devCommand().catch(() => {
115
- // Ignore errors
116
- })
117
-
118
- // Wait a bit for initial generation
119
- await new Promise((resolve) => setTimeout(resolve, 100))
120
-
121
- expect(generateCommand).toHaveBeenCalled()
122
- })
123
-
124
- it('should set up file watcher for config file', async () => {
125
- const { devCommand } = await import('./dev.js')
126
-
127
- // Run dev command
128
- devCommand().catch(() => {
129
- // Ignore errors
130
- })
131
-
132
- await new Promise((resolve) => setTimeout(resolve, 100))
133
-
134
- // Verify watcher was set up
135
- expect(mockWatch).toHaveBeenCalled()
136
- expect(mockWatch.mock.calls.length).toBeGreaterThan(0)
137
-
138
- const watchPath = mockWatch.mock.calls[0]![0]
139
- expect(watchPath).toContain('opensaas.config.ts')
140
-
141
- const watchOptions = mockWatch.mock.calls[0]![1]
142
- expect(watchOptions).toMatchObject({
143
- persistent: true,
144
- ignoreInitial: true,
145
- })
146
- })
147
-
148
- it('should register change event handler', async () => {
149
- const { devCommand } = await import('./dev.js')
150
-
151
- devCommand().catch(() => {
152
- // Ignore errors
153
- })
154
-
155
- await new Promise((resolve) => setTimeout(resolve, 100))
156
-
157
- expect(mockWatcherOn).toHaveBeenCalledWith('change', expect.any(Function))
158
- })
159
-
160
- it('should register error event handler', async () => {
161
- const { devCommand } = await import('./dev.js')
162
-
163
- devCommand().catch(() => {
164
- // Ignore errors
165
- })
166
-
167
- await new Promise((resolve) => setTimeout(resolve, 100))
168
-
169
- expect(mockWatcherOn).toHaveBeenCalledWith('error', expect.any(Function))
170
- })
171
-
172
- it('should regenerate on config file change', async () => {
173
- const { generateCommand } = await import('./generate.js')
174
- const { devCommand } = await import('./dev.js')
175
-
176
- devCommand().catch(() => {
177
- // Ignore errors
178
- })
179
-
180
- await new Promise((resolve) => setTimeout(resolve, 100))
181
-
182
- // Simulate file change
183
- const changeHandler = mockWatcherOn.mock.calls.find((call) => call[0] === 'change')?.[1]
184
- expect(changeHandler).toBeDefined()
185
-
186
- if (changeHandler) {
187
- await changeHandler()
188
- }
189
-
190
- // generateCommand should be called again
191
- expect(vi.mocked(generateCommand).mock.calls.length).toBeGreaterThan(1)
192
- })
193
-
194
- it('should close watcher on SIGINT', async () => {
195
- const { devCommand } = await import('./dev.js')
196
-
197
- devCommand().catch(() => {
198
- // Ignore errors
199
- })
200
-
201
- await new Promise((resolve) => setTimeout(resolve, 100))
202
-
203
- // Simulate SIGINT - catch the error from process.exit
204
- try {
205
- process.emit('SIGINT', 'SIGINT')
206
- } catch {
207
- // Expected to throw from process.exit mock
208
- }
209
-
210
- await new Promise((resolve) => setTimeout(resolve, 100))
211
-
212
- expect(mockWatcherClose).toHaveBeenCalled()
213
- })
214
- })
215
- })
@@ -1,48 +0,0 @@
1
- import * as path from 'path'
2
- import * as fs from 'fs'
3
- import chalk from 'chalk'
4
- import chokidar from 'chokidar'
5
- import { generateCommand } from './generate.js'
6
-
7
- export async function devCommand() {
8
- const cwd = process.cwd()
9
- const configPath = path.join(cwd, 'opensaas.config.ts')
10
-
11
- // Check if config exists
12
- if (!fs.existsSync(configPath)) {
13
- console.error(chalk.red('Error: opensaas.config.ts not found in current directory'))
14
- console.error(chalk.gray(' Please run this command from your project root'))
15
- process.exit(1)
16
- }
17
-
18
- console.log(chalk.bold.cyan('\nOpenSaas Dev Mode\n'))
19
- console.log(chalk.gray('Watching for changes to opensaas.config.ts...\n'))
20
-
21
- // Run initial generation
22
- await generateCommand()
23
-
24
- // Watch for changes
25
- const watcher = chokidar.watch(configPath, {
26
- persistent: true,
27
- ignoreInitial: true,
28
- })
29
-
30
- watcher.on('change', async () => {
31
- console.log(chalk.yellow('\nConfig changed, regenerating...\n'))
32
- await generateCommand()
33
- })
34
-
35
- watcher.on('error', (error) => {
36
- console.error(chalk.red('\nWatcher error:'), error)
37
- })
38
-
39
- // Keep the process running
40
- console.log(chalk.gray('Press Ctrl+C to stop watching\n'))
41
-
42
- // Handle graceful shutdown
43
- process.on('SIGINT', () => {
44
- console.log(chalk.yellow('\n\nStopping dev mode...'))
45
- watcher.close()
46
- process.exit(0)
47
- })
48
- }
@@ -1,282 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
- import * as fs from 'fs'
3
- import * as path from 'path'
4
- import * as os from 'os'
5
- import type { OpenSaasConfig } from '@opensaas/stack-core'
6
- import { text } from '@opensaas/stack-core/fields'
7
- import { writePrismaSchema, writeTypes, writeContext } from '../generator/index.js'
8
-
9
- // Mock ora module
10
- vi.mock('ora', () => ({
11
- default: vi.fn(() => ({
12
- start: vi.fn().mockReturnThis(),
13
- succeed: vi.fn().mockReturnThis(),
14
- fail: vi.fn().mockReturnThis(),
15
- text: '',
16
- })),
17
- }))
18
-
19
- // Mock chalk module
20
- vi.mock('chalk', () => ({
21
- default: {
22
- bold: vi.fn((str) => str),
23
- cyan: vi.fn((str) => str),
24
- gray: vi.fn((str) => str),
25
- red: vi.fn((str) => str),
26
- yellow: vi.fn((str) => str),
27
- green: vi.fn((str) => str),
28
- },
29
- }))
30
-
31
- describe('Generate Command Integration', () => {
32
- let tempDir: string
33
-
34
- beforeEach(() => {
35
- // Create temp directory for testing
36
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'generate-test-'))
37
- })
38
-
39
- afterEach(() => {
40
- // Clean up
41
- if (fs.existsSync(tempDir)) {
42
- fs.rmSync(tempDir, { recursive: true, force: true })
43
- }
44
- })
45
-
46
- describe('Generator Integration', () => {
47
- it('should generate all files for a basic config', () => {
48
- const config: OpenSaasConfig = {
49
- db: {
50
- provider: 'sqlite',
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- prismaClientConstructor: (() => null) as any,
53
- },
54
- lists: {
55
- User: {
56
- fields: {
57
- name: text({ validation: { isRequired: true } }),
58
- email: text({ validation: { isRequired: true } }),
59
- },
60
- },
61
- },
62
- }
63
-
64
- // Generate files
65
- const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
66
- const typesPath = path.join(tempDir, '.opensaas', 'types.ts')
67
- const contextPath = path.join(tempDir, '.opensaas', 'context.ts')
68
-
69
- writePrismaSchema(config, prismaPath)
70
- writeTypes(config, typesPath)
71
- writeContext(config, contextPath)
72
-
73
- // Verify all files exist
74
- expect(fs.existsSync(prismaPath)).toBe(true)
75
- expect(fs.existsSync(typesPath)).toBe(true)
76
- expect(fs.existsSync(contextPath)).toBe(true)
77
-
78
- // Verify file contents with snapshots
79
- const prismaSchema = fs.readFileSync(prismaPath, 'utf-8')
80
- expect(prismaSchema).toMatchSnapshot('prisma-schema')
81
-
82
- const types = fs.readFileSync(typesPath, 'utf-8')
83
- expect(types).toMatchSnapshot('types')
84
-
85
- const context = fs.readFileSync(contextPath, 'utf-8')
86
- expect(context).toMatchSnapshot('context')
87
- })
88
-
89
- it('should create directories if they do not exist', () => {
90
- const config: OpenSaasConfig = {
91
- db: {
92
- provider: 'sqlite',
93
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
- prismaClientConstructor: (() => null) as any,
95
- },
96
- lists: {},
97
- }
98
-
99
- const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
100
-
101
- writePrismaSchema(config, prismaPath)
102
-
103
- expect(fs.existsSync(path.join(tempDir, 'prisma'))).toBe(true)
104
- expect(fs.existsSync(prismaPath)).toBe(true)
105
- })
106
-
107
- it('should overwrite existing files', () => {
108
- const config1: OpenSaasConfig = {
109
- db: {
110
- provider: 'sqlite',
111
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
- prismaClientConstructor: (() => null) as any,
113
- },
114
- lists: {
115
- User: {
116
- fields: {
117
- name: text(),
118
- },
119
- },
120
- },
121
- }
122
-
123
- const config2: OpenSaasConfig = {
124
- db: {
125
- provider: 'sqlite',
126
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
- prismaClientConstructor: (() => null) as any,
128
- },
129
- lists: {
130
- Post: {
131
- fields: {
132
- title: text(),
133
- },
134
- },
135
- },
136
- }
137
-
138
- const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
139
-
140
- // Generate first config
141
- writePrismaSchema(config1, prismaPath)
142
- let schema = fs.readFileSync(prismaPath, 'utf-8')
143
- expect(schema).toMatchSnapshot('overwrite-before')
144
-
145
- // Generate second config (should overwrite)
146
- writePrismaSchema(config2, prismaPath)
147
- schema = fs.readFileSync(prismaPath, 'utf-8')
148
- expect(schema).toMatchSnapshot('overwrite-after')
149
- })
150
-
151
- it('should handle custom opensaasPath', () => {
152
- const config: OpenSaasConfig = {
153
- db: {
154
- provider: 'sqlite',
155
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
- prismaClientConstructor: (() => null) as any,
157
- },
158
- opensaasPath: '.custom',
159
- lists: {},
160
- }
161
-
162
- const typesPath = path.join(tempDir, '.custom', 'types.ts')
163
- const contextPath = path.join(tempDir, '.custom', 'context.ts')
164
-
165
- writeTypes(config, typesPath)
166
- writeContext(config, contextPath)
167
-
168
- expect(fs.existsSync(path.join(tempDir, '.custom'))).toBe(true)
169
- expect(fs.existsSync(typesPath)).toBe(true)
170
- expect(fs.existsSync(contextPath)).toBe(true)
171
- })
172
-
173
- it('should generate consistent output across multiple runs', () => {
174
- const config: OpenSaasConfig = {
175
- db: {
176
- provider: 'sqlite',
177
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
- prismaClientConstructor: (() => null) as any,
179
- },
180
- lists: {
181
- User: {
182
- fields: {
183
- name: text(),
184
- },
185
- },
186
- },
187
- }
188
-
189
- const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
190
-
191
- // Generate twice
192
- writePrismaSchema(config, prismaPath)
193
- const schema1 = fs.readFileSync(prismaPath, 'utf-8')
194
-
195
- writePrismaSchema(config, prismaPath)
196
- const schema2 = fs.readFileSync(prismaPath, 'utf-8')
197
-
198
- // Should be identical
199
- expect(schema1).toBe(schema2)
200
- expect(schema1).toMatchSnapshot('consistent-output')
201
- })
202
-
203
- it('should handle empty lists config', () => {
204
- const config: OpenSaasConfig = {
205
- db: {
206
- provider: 'sqlite',
207
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
- prismaClientConstructor: (() => null) as any,
209
- },
210
- lists: {},
211
- }
212
-
213
- const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
214
- const typesPath = path.join(tempDir, '.opensaas', 'types.ts')
215
-
216
- writePrismaSchema(config, prismaPath)
217
- writeTypes(config, typesPath)
218
-
219
- expect(fs.existsSync(prismaPath)).toBe(true)
220
- expect(fs.existsSync(typesPath)).toBe(true)
221
-
222
- const schema = fs.readFileSync(prismaPath, 'utf-8')
223
- expect(schema).toMatchSnapshot('empty-lists-schema')
224
-
225
- const types = fs.readFileSync(typesPath, 'utf-8')
226
- expect(types).toMatchSnapshot('empty-lists-types')
227
- })
228
-
229
- it('should handle different database providers', () => {
230
- const providers = ['sqlite', 'postgresql', 'mysql'] as const
231
-
232
- providers.forEach((provider) => {
233
- const config: OpenSaasConfig = {
234
- db: {
235
- provider,
236
- url: provider === 'sqlite' ? 'file:./dev.db' : 'postgresql://localhost:5432/db',
237
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
- prismaClientConstructor: (() => null) as any,
239
- },
240
- lists: {},
241
- }
242
-
243
- const prismaPath = path.join(tempDir, `${provider}-schema.prisma`)
244
- writePrismaSchema(config, prismaPath)
245
-
246
- const schema = fs.readFileSync(prismaPath, 'utf-8')
247
- expect(schema).toMatchSnapshot(`${provider}-provider`)
248
- })
249
- })
250
-
251
- it('should generate files in correct locations', () => {
252
- const config: OpenSaasConfig = {
253
- db: {
254
- provider: 'sqlite',
255
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
256
- prismaClientConstructor: (() => null) as any,
257
- },
258
- lists: {
259
- User: {
260
- fields: {
261
- name: text(),
262
- },
263
- },
264
- },
265
- }
266
-
267
- writePrismaSchema(config, path.join(tempDir, 'prisma', 'schema.prisma'))
268
- writeTypes(config, path.join(tempDir, '.opensaas', 'types.ts'))
269
- writeContext(config, path.join(tempDir, '.opensaas', 'context.ts'))
270
-
271
- // Verify directory structure
272
- const prismaDir = path.join(tempDir, 'prisma')
273
- const opensaasDir = path.join(tempDir, '.opensaas')
274
-
275
- expect(fs.existsSync(prismaDir)).toBe(true)
276
- expect(fs.existsSync(opensaasDir)).toBe(true)
277
- expect(fs.readdirSync(prismaDir)).toContain('schema.prisma')
278
- expect(fs.readdirSync(opensaasDir)).toContain('types.ts')
279
- expect(fs.readdirSync(opensaasDir)).toContain('context.ts')
280
- })
281
- })
282
- })