@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,710 +0,0 @@
1
- /**
2
- * Documentation provider - Fetches documentation from the hosted docs site
3
- */
4
-
5
- import fs from 'fs-extra'
6
- import path from 'path'
7
- import { glob } from 'glob'
8
- import type { DocumentationLookup } from './types.js'
9
-
10
- interface SearchResult {
11
- content: string
12
- metadata: {
13
- title?: string
14
- slug?: string
15
- section?: string
16
- }
17
- score: number
18
- }
19
-
20
- interface SearchResponse {
21
- results: SearchResult[]
22
- query: string
23
- count: number
24
- }
25
-
26
- interface LocalDocResult {
27
- content: string
28
- files: string[]
29
- }
30
-
31
- interface ExampleConfig {
32
- description: string
33
- code: string
34
- notes?: string
35
- sourcePath: string
36
- }
37
-
38
- export class OpenSaasDocumentationProvider {
39
- private readonly DOCS_API = 'https://stack.opensaas.au/api/search'
40
- private cache = new Map<string, { data: DocumentationLookup; timestamp: number }>()
41
- private readonly CACHE_TTL = 1000 * 60 * 30 // 30 minutes
42
-
43
- // Topic mappings for user-friendly queries
44
- private topicMappings: Record<string, string> = {
45
- fields: 'field-types',
46
- 'field types': 'field-types',
47
- 'field type': 'field-types',
48
- access: 'access-control',
49
- 'access control': 'access-control',
50
- permissions: 'access-control',
51
- auth: 'authentication',
52
- authentication: 'authentication',
53
- login: 'authentication',
54
- 'sign in': 'authentication',
55
- hooks: 'hooks',
56
- hook: 'hooks',
57
- lifecycle: 'hooks',
58
- plugins: 'plugin-system',
59
- plugin: 'plugin-system',
60
- rag: 'rag',
61
- search: 'semantic-search',
62
- 'semantic search': 'semantic-search',
63
- storage: 'file-storage',
64
- files: 'file-storage',
65
- upload: 'file-storage',
66
- config: 'configuration',
67
- configuration: 'configuration',
68
- prisma: 'prisma-integration',
69
- database: 'database-setup',
70
- deployment: 'deployment',
71
- deploy: 'deployment',
72
- }
73
-
74
- /**
75
- * Search documentation by query
76
- */
77
- async searchDocs(query: string, limit = 5, minScore = 0.7): Promise<DocumentationLookup> {
78
- const cacheKey = `search:${query}:${limit}:${minScore}`
79
-
80
- // Check cache
81
- const cached = this.cache.get(cacheKey)
82
- if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
83
- return cached.data
84
- }
85
-
86
- try {
87
- const response = await fetch(this.DOCS_API, {
88
- method: 'POST',
89
- headers: {
90
- 'Content-Type': 'application/json',
91
- },
92
- body: JSON.stringify({ query, limit, minScore }),
93
- })
94
-
95
- if (!response.ok) {
96
- throw new Error(`Docs API error: ${response.statusText}`)
97
- }
98
-
99
- const data = (await response.json()) as SearchResponse
100
-
101
- const docLookup: DocumentationLookup = {
102
- topic: query,
103
- content: this.formatSearchResults(data.results),
104
- url: 'https://stack.opensaas.au/',
105
- codeExamples: this.extractCodeExamples(data.results),
106
- relatedTopics: this.extractRelatedTopics(data.results),
107
- }
108
-
109
- // Cache the result
110
- this.cache.set(cacheKey, { data: docLookup, timestamp: Date.now() })
111
-
112
- return docLookup
113
- } catch (error) {
114
- console.error('Error fetching documentation:', error)
115
- return this.getFallbackDocs(query)
116
- }
117
- }
118
-
119
- /**
120
- * Get documentation for a specific topic
121
- */
122
- async getTopicDocs(topic: string): Promise<DocumentationLookup> {
123
- // Normalize topic using mappings
124
- const normalizedTopic = this.topicMappings[topic.toLowerCase()] || topic
125
-
126
- return this.searchDocs(normalizedTopic, 3, 0.8)
127
- }
128
-
129
- /**
130
- * Format search results into readable content
131
- */
132
- private formatSearchResults(results: SearchResult[]): string {
133
- if (results.length === 0) {
134
- return 'No documentation found for this query.'
135
- }
136
-
137
- return results
138
- .map((result, index) => {
139
- const title = result.metadata.title || `Section ${index + 1}`
140
- const section = result.metadata.section || ''
141
- const score = (result.score * 100).toFixed(0)
142
-
143
- return `### ${title}${section ? ` (${section})` : ''} [Relevance: ${score}%]\n\n${result.content}\n`
144
- })
145
- .join('\n---\n\n')
146
- }
147
-
148
- /**
149
- * Extract code examples from search results
150
- */
151
- private extractCodeExamples(results: SearchResult[]): string[] {
152
- const codeExamples: string[] = []
153
- const codeBlockRegex = /```[\s\S]*?```/g
154
-
155
- for (const result of results) {
156
- const matches = result.content.match(codeBlockRegex)
157
- if (matches) {
158
- codeExamples.push(...matches)
159
- }
160
- }
161
-
162
- return codeExamples
163
- }
164
-
165
- /**
166
- * Extract related topics from search results
167
- */
168
- private extractRelatedTopics(results: SearchResult[]): string[] {
169
- const topics = new Set<string>()
170
-
171
- for (const result of results) {
172
- if (result.metadata.section) {
173
- topics.add(result.metadata.section)
174
- }
175
- }
176
-
177
- return Array.from(topics)
178
- }
179
-
180
- /**
181
- * Fallback documentation when API is unavailable
182
- */
183
- private getFallbackDocs(query: string): DocumentationLookup {
184
- return {
185
- topic: query,
186
- content: `Unable to fetch documentation from the docs site at this time.
187
-
188
- Please visit the OpenSaaS Stack documentation directly:
189
- https://stack.opensaas.au/
190
-
191
- For ${query}, you can also check:
192
- - GitHub repository: https://github.com/OpenSaasAU/stack
193
- - Example projects in the examples/ directory`,
194
- url: 'https://stack.opensaas.au/',
195
- codeExamples: [],
196
- relatedTopics: [],
197
- }
198
- }
199
-
200
- /**
201
- * Search local CLAUDE.md files in the monorepo
202
- */
203
- async searchLocalDocs(query: string): Promise<LocalDocResult> {
204
- const cwd = process.cwd()
205
-
206
- // Find CLAUDE.md files
207
- const claudeFiles = await glob('**/CLAUDE.md', {
208
- cwd,
209
- ignore: ['node_modules/**', '.next/**', 'dist/**', 'build/**'],
210
- absolute: true,
211
- })
212
-
213
- const results: Array<{ file: string; content: string; score: number }> = []
214
- const queryLower = query.toLowerCase()
215
- const queryTerms = queryLower.split(/\s+/)
216
-
217
- for (const file of claudeFiles) {
218
- try {
219
- const content = await fs.readFile(file, 'utf-8')
220
- const contentLower = content.toLowerCase()
221
-
222
- // Simple relevance scoring
223
- let score = 0
224
- for (const term of queryTerms) {
225
- if (contentLower.includes(term)) {
226
- score += (contentLower.match(new RegExp(term, 'g')) || []).length
227
- }
228
- }
229
-
230
- if (score > 0) {
231
- // Extract relevant section
232
- const lines = content.split('\n')
233
- const relevantLines: string[] = []
234
- let inRelevantSection = false
235
-
236
- for (const line of lines) {
237
- const lineLower = line.toLowerCase()
238
-
239
- // Check if line starts a section
240
- if (line.startsWith('#')) {
241
- // Check if this section title is relevant
242
- const titleRelevant = queryTerms.some((term) => lineLower.includes(term))
243
- if (titleRelevant) {
244
- inRelevantSection = true
245
- relevantLines.push(line)
246
- } else if (inRelevantSection) {
247
- // We've left the relevant section
248
- break
249
- }
250
- } else if (inRelevantSection) {
251
- relevantLines.push(line)
252
- } else if (queryTerms.some((term) => lineLower.includes(term))) {
253
- // Found relevant content outside a section
254
- relevantLines.push(line)
255
- }
256
- }
257
-
258
- if (relevantLines.length > 0) {
259
- results.push({
260
- file: path.relative(cwd, file),
261
- content: relevantLines.slice(0, 50).join('\n'),
262
- score,
263
- })
264
- }
265
- }
266
- } catch {
267
- // Skip files that can't be read
268
- }
269
- }
270
-
271
- // Sort by score
272
- results.sort((a, b) => b.score - a.score)
273
-
274
- if (results.length === 0) {
275
- return { content: '', files: [] }
276
- }
277
-
278
- const content = results
279
- .slice(0, 3)
280
- .map((r) => `### From \`${r.file}\`\n\n${r.content}`)
281
- .join('\n\n---\n\n')
282
-
283
- return {
284
- content,
285
- files: results.map((r) => r.file),
286
- }
287
- }
288
-
289
- /**
290
- * Get example config code for a feature
291
- */
292
- async getExampleConfig(feature: string): Promise<ExampleConfig | null> {
293
- const examples: Record<string, ExampleConfig> = {
294
- 'blog-with-auth': {
295
- description: 'A blog application with user authentication and post management',
296
- code: `import { config, list } from '@opensaas/stack-core'
297
- import { text, relationship, timestamp, select } from '@opensaas/stack-core/fields'
298
- import { withAuth, authConfig } from '@opensaas/stack-auth'
299
-
300
- const isAuthor = ({ session, item }) =>
301
- session?.userId === item?.authorId
302
-
303
- export default withAuth(
304
- config({
305
- db: {
306
- provider: 'sqlite',
307
- url: 'file:./dev.db',
308
- prismaClientConstructor: (PrismaClient) => {
309
- // ... adapter setup
310
- },
311
- },
312
- lists: {
313
- Post: list({
314
- fields: {
315
- title: text({ validation: { isRequired: true } }),
316
- content: text({ ui: { displayMode: 'textarea' } }),
317
- status: select({
318
- options: [
319
- { label: 'Draft', value: 'draft' },
320
- { label: 'Published', value: 'published' },
321
- ],
322
- defaultValue: 'draft',
323
- }),
324
- author: relationship({ ref: 'User.posts' }),
325
- publishedAt: timestamp(),
326
- },
327
- access: {
328
- operation: {
329
- query: () => true,
330
- create: ({ session }) => !!session,
331
- update: isAuthor,
332
- delete: isAuthor,
333
- },
334
- },
335
- }),
336
- },
337
- }),
338
- authConfig({ emailAndPassword: { enabled: true } })
339
- )`,
340
- notes:
341
- 'This example uses the auth plugin for user management. The `isAuthor` helper restricts updates to the post creator.',
342
- sourcePath: 'examples/auth-demo/opensaas.config.ts',
343
- },
344
-
345
- 'access-control': {
346
- description: 'Common access control patterns for different scenarios',
347
- code: `// Public read, authenticated write
348
- access: {
349
- operation: {
350
- query: () => true,
351
- create: ({ session }) => !!session,
352
- update: ({ session }) => !!session,
353
- delete: ({ session }) => !!session,
354
- },
355
- }
356
-
357
- // Only owners can access
358
- const isOwner = ({ session, item }) =>
359
- session?.userId === item?.userId
360
-
361
- access: {
362
- operation: {
363
- query: isOwner,
364
- update: isOwner,
365
- delete: isOwner,
366
- },
367
- filter: {
368
- query: ({ session }) => ({ userId: { equals: session?.userId } }),
369
- },
370
- }
371
-
372
- // Role-based access
373
- const isAdmin = ({ session }) => session?.role === 'admin'
374
- const isOwnerOrAdmin = (args) => isOwner(args) || isAdmin(args)
375
-
376
- access: {
377
- operation: {
378
- query: () => true,
379
- create: ({ session }) => !!session,
380
- update: isOwnerOrAdmin,
381
- delete: isAdmin,
382
- },
383
- }`,
384
- notes:
385
- 'Access control functions receive session and item context. Use filter access to automatically scope queries.',
386
- sourcePath: 'packages/core/CLAUDE.md',
387
- },
388
-
389
- relationships: {
390
- description: 'How to define relationships between models',
391
- code: `lists: {
392
- User: list({
393
- fields: {
394
- name: text(),
395
- email: text({ isIndexed: 'unique' }),
396
- posts: relationship({ ref: 'Post.author', many: true }),
397
- comments: relationship({ ref: 'Comment.author', many: true }),
398
- },
399
- }),
400
-
401
- Post: list({
402
- fields: {
403
- title: text(),
404
- content: text(),
405
- author: relationship({ ref: 'User.posts' }),
406
- comments: relationship({ ref: 'Comment.post', many: true }),
407
- tags: relationship({ ref: 'Tag.posts', many: true }),
408
- },
409
- }),
410
-
411
- Comment: list({
412
- fields: {
413
- content: text(),
414
- author: relationship({ ref: 'User.comments' }),
415
- post: relationship({ ref: 'Post.comments' }),
416
- },
417
- }),
418
-
419
- Tag: list({
420
- fields: {
421
- name: text(),
422
- posts: relationship({ ref: 'Post.tags', many: true }),
423
- },
424
- }),
425
- }`,
426
- notes:
427
- 'Relationships use `ref: "ListName.fieldName"` format. Set `many: true` for one-to-many or many-to-many.',
428
- sourcePath: 'examples/blog/opensaas.config.ts',
429
- },
430
-
431
- hooks: {
432
- description: 'Data transformation and side effect hooks',
433
- code: `Post: list({
434
- fields: {
435
- title: text(),
436
- slug: text(),
437
- status: select({
438
- options: [
439
- { label: 'Draft', value: 'draft' },
440
- { label: 'Published', value: 'published' },
441
- ],
442
- }),
443
- publishedAt: timestamp(),
444
- },
445
- hooks: {
446
- resolveInput: async ({ resolvedData, operation }) => {
447
- // Auto-generate slug from title
448
- if (resolvedData.title && !resolvedData.slug) {
449
- resolvedData.slug = resolvedData.title
450
- .toLowerCase()
451
- .replace(/[^a-z0-9]+/g, '-')
452
- }
453
-
454
- // Set publishedAt when status changes to published
455
- if (operation === 'update' && resolvedData.status === 'published') {
456
- resolvedData.publishedAt = new Date()
457
- }
458
-
459
- return resolvedData
460
- },
461
- afterOperation: async ({ operation, item }) => {
462
- // Send notification after publish
463
- if (operation === 'update' && item?.status === 'published') {
464
- console.log(\`Post published: \${item.title}\`)
465
- // await sendNotification(item)
466
- }
467
- },
468
- },
469
- })`,
470
- notes:
471
- 'resolveInput transforms data before save. afterOperation runs side effects. Use beforeOperation for validation.',
472
- sourcePath: 'packages/core/CLAUDE.md',
473
- },
474
-
475
- 'custom-fields': {
476
- description: 'Creating custom field types',
477
- code: `// In your field definition file
478
- import type { BaseFieldConfig } from '@opensaas/stack-core'
479
- import { z } from 'zod'
480
-
481
- export type SlugField = BaseFieldConfig & {
482
- type: 'slug'
483
- sourceField?: string
484
- }
485
-
486
- export function slug(options?: Omit<SlugField, 'type'>): SlugField {
487
- return {
488
- type: 'slug',
489
- ...options,
490
-
491
- getZodSchema: (fieldName, operation) => {
492
- if (operation === 'create') {
493
- return z.string().regex(/^[a-z0-9-]+$/).optional()
494
- }
495
- return z.string().regex(/^[a-z0-9-]+$/).optional()
496
- },
497
-
498
- getPrismaType: (fieldName) => ({
499
- type: 'String',
500
- modifiers: '?',
501
- attributes: ['@unique'],
502
- }),
503
-
504
- getTypeScriptType: () => ({
505
- type: 'string',
506
- optional: true,
507
- }),
508
- }
509
- }
510
-
511
- // Usage in config
512
- fields: {
513
- title: text({ validation: { isRequired: true } }),
514
- urlSlug: slug({ sourceField: 'title' }),
515
- }`,
516
- notes:
517
- 'Custom fields implement getZodSchema, getPrismaType, and getTypeScriptType methods. See packages/tiptap for a full example.',
518
- sourcePath: 'examples/custom-field',
519
- },
520
- }
521
-
522
- const normalizedFeature = feature.toLowerCase().replace(/\s+/g, '-')
523
- return examples[normalizedFeature] || null
524
- }
525
-
526
- /**
527
- * Get migration-specific documentation for a project type
528
- */
529
- async findMigrationGuide(projectType: string): Promise<string> {
530
- const guides: Record<string, string> = {
531
- prisma: `# Prisma to OpenSaaS Migration
532
-
533
- ## Type Mapping
534
-
535
- | Prisma Type | OpenSaaS Field |
536
- |-------------|----------------|
537
- | String | text() |
538
- | Int | integer() |
539
- | Boolean | checkbox() |
540
- | DateTime | timestamp() |
541
- | Json | text() with custom handling |
542
- | Enum | select() |
543
- | @relation | relationship() |
544
-
545
- ## Key Changes
546
-
547
- 1. **Models → Lists**: Prisma models become OpenSaaS lists
548
- 2. **Access Control**: Add operation-level and field-level access
549
- 3. **Database URL**: Now provided via prismaClientConstructor
550
- 4. **Relationships**: Use \`ref: 'ListName.fieldName'\` format
551
-
552
- ## Common Patterns
553
-
554
- ### Before (Prisma)
555
- \`\`\`prisma
556
- model Post {
557
- id String @id @default(cuid())
558
- title String
559
- author User @relation(fields: [authorId], references: [id])
560
- authorId String
561
- }
562
- \`\`\`
563
-
564
- ### After (OpenSaaS)
565
- \`\`\`typescript
566
- Post: list({
567
- fields: {
568
- title: text({ validation: { isRequired: true } }),
569
- author: relationship({ ref: 'User.posts' }),
570
- },
571
- access: {
572
- operation: {
573
- query: () => true,
574
- create: ({ session }) => !!session,
575
- },
576
- },
577
- })
578
- \`\`\`
579
- `,
580
-
581
- keystone: `# KeystoneJS to OpenSaaS Migration
582
-
583
- ## Good News!
584
-
585
- KeystoneJS and OpenSaaS Stack are very similar. Migration is mostly:
586
- 1. Update import paths
587
- 2. Minor syntax adjustments
588
- 3. Add prismaClientConstructor for Prisma 7
589
-
590
- ## Key Changes
591
-
592
- ### Imports
593
- \`\`\`typescript
594
- // Before (KeystoneJS)
595
- import { config, list } from '@keystone-6/core'
596
- import { text, relationship } from '@keystone-6/core/fields'
597
-
598
- // After (OpenSaaS)
599
- import { config, list } from '@opensaas/stack-core'
600
- import { text, relationship } from '@opensaas/stack-core/fields'
601
- \`\`\`
602
-
603
- ### Database Config
604
- \`\`\`typescript
605
- // Before
606
- db: {
607
- provider: 'sqlite',
608
- url: 'file:./dev.db',
609
- }
610
-
611
- // After (Prisma 7 requires adapters)
612
- db: {
613
- provider: 'sqlite',
614
- url: 'file:./dev.db',
615
- prismaClientConstructor: (PrismaClient) => {
616
- const db = new Database('./dev.db')
617
- const adapter = new PrismaBetterSQLite3(db)
618
- return new PrismaClient({ adapter })
619
- },
620
- }
621
- \`\`\`
622
-
623
- ## Field Types (1:1 mapping)
624
-
625
- Most field types work identically:
626
- - text() → text()
627
- - integer() → integer()
628
- - checkbox() → checkbox()
629
- - timestamp() → timestamp()
630
- - select() → select()
631
- - relationship() → relationship()
632
-
633
- ## Access Control (same patterns)
634
-
635
- Access control functions work the same way. Just copy them over!
636
- `,
637
-
638
- nextjs: `# Next.js to OpenSaaS Migration
639
-
640
- ## Overview
641
-
642
- If you have a Next.js project without Prisma, you'll need to:
643
- 1. Define your data models in opensaas.config.ts
644
- 2. Run the generator to create Prisma schema
645
- 3. Set up your database
646
-
647
- ## Steps
648
-
649
- 1. **Install Dependencies**
650
- \`\`\`bash
651
- pnpm add @opensaas/stack-core @prisma/client prisma
652
- pnpm add -D @prisma/adapter-better-sqlite3 better-sqlite3
653
- \`\`\`
654
-
655
- 2. **Create opensaas.config.ts**
656
- \`\`\`typescript
657
- import { config, list } from '@opensaas/stack-core'
658
- import { text, integer } from '@opensaas/stack-core/fields'
659
-
660
- export default config({
661
- db: {
662
- provider: 'sqlite',
663
- url: 'file:./dev.db',
664
- prismaClientConstructor: (PrismaClient) => {
665
- // See examples for adapter setup
666
- },
667
- },
668
- lists: {
669
- // Define your models here
670
- },
671
- })
672
- \`\`\`
673
-
674
- 3. **Generate and Push**
675
- \`\`\`bash
676
- pnpm opensaas generate
677
- npx prisma generate
678
- npx prisma db push
679
- \`\`\`
680
-
681
- ## If You're Using an Auth Library
682
-
683
- - **next-auth**: Consider migrating to Better-auth via @opensaas/stack-auth
684
- - **clerk**: Can continue using Clerk alongside OpenSaaS
685
- - **better-auth**: Use @opensaas/stack-auth plugin
686
- `,
687
- }
688
-
689
- return guides[projectType.toLowerCase()] || guides.prisma
690
- }
691
-
692
- /**
693
- * Clear expired cache entries
694
- */
695
- clearExpiredCache(): void {
696
- const now = Date.now()
697
- for (const [key, value] of this.cache.entries()) {
698
- if (now - value.timestamp >= this.CACHE_TTL) {
699
- this.cache.delete(key)
700
- }
701
- }
702
- }
703
-
704
- /**
705
- * Clear all cache
706
- */
707
- clearCache(): void {
708
- this.cache.clear()
709
- }
710
- }