@actuate-media/cli 0.4.1 → 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 (92) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +69 -12
  3. package/CHANGELOG.md +58 -0
  4. package/dist/__tests__/db-init.test.d.ts +2 -0
  5. package/dist/__tests__/db-init.test.d.ts.map +1 -0
  6. package/dist/__tests__/db-init.test.js +127 -0
  7. package/dist/__tests__/db-init.test.js.map +1 -0
  8. package/dist/__tests__/db-sync.test.d.ts +2 -0
  9. package/dist/__tests__/db-sync.test.d.ts.map +1 -0
  10. package/dist/__tests__/db-sync.test.js +136 -0
  11. package/dist/__tests__/db-sync.test.js.map +1 -0
  12. package/dist/__tests__/deployment-diagnostics.test.js.map +1 -1
  13. package/dist/__tests__/init.test.js.map +1 -1
  14. package/dist/__tests__/schema-fragment.test.js +1 -1
  15. package/dist/__tests__/schema-fragment.test.js.map +1 -1
  16. package/dist/__tests__/seed.test.js.map +1 -1
  17. package/dist/commands/db-init.d.ts +19 -2
  18. package/dist/commands/db-init.d.ts.map +1 -1
  19. package/dist/commands/db-init.js +128 -306
  20. package/dist/commands/db-init.js.map +1 -1
  21. package/dist/commands/db-status.d.ts +1 -1
  22. package/dist/commands/db-status.d.ts.map +1 -1
  23. package/dist/commands/db-status.js +33 -33
  24. package/dist/commands/db-status.js.map +1 -1
  25. package/dist/commands/db-sync.d.ts +31 -0
  26. package/dist/commands/db-sync.d.ts.map +1 -0
  27. package/dist/commands/db-sync.js +195 -0
  28. package/dist/commands/db-sync.js.map +1 -0
  29. package/dist/commands/doctor.d.ts +1 -1
  30. package/dist/commands/doctor.d.ts.map +1 -1
  31. package/dist/commands/doctor.js +48 -41
  32. package/dist/commands/doctor.js.map +1 -1
  33. package/dist/commands/export.d.ts +1 -1
  34. package/dist/commands/export.d.ts.map +1 -1
  35. package/dist/commands/export.js +32 -32
  36. package/dist/commands/export.js.map +1 -1
  37. package/dist/commands/generate.d.ts +1 -1
  38. package/dist/commands/generate.d.ts.map +1 -1
  39. package/dist/commands/generate.js +8 -8
  40. package/dist/commands/generate.js.map +1 -1
  41. package/dist/commands/import.d.ts +1 -1
  42. package/dist/commands/import.d.ts.map +1 -1
  43. package/dist/commands/import.js +55 -58
  44. package/dist/commands/import.js.map +1 -1
  45. package/dist/commands/init.d.ts.map +1 -1
  46. package/dist/commands/init.js.map +1 -1
  47. package/dist/commands/migrate.d.ts +1 -1
  48. package/dist/commands/migrate.d.ts.map +1 -1
  49. package/dist/commands/migrate.js +18 -24
  50. package/dist/commands/migrate.js.map +1 -1
  51. package/dist/commands/seed.d.ts +1 -1
  52. package/dist/commands/seed.d.ts.map +1 -1
  53. package/dist/commands/seed.js +156 -157
  54. package/dist/commands/seed.js.map +1 -1
  55. package/dist/commands/update-check.d.ts +1 -1
  56. package/dist/commands/update-check.d.ts.map +1 -1
  57. package/dist/commands/update-check.js +34 -27
  58. package/dist/commands/update-check.js.map +1 -1
  59. package/dist/commands/upgrade.d.ts +1 -1
  60. package/dist/commands/upgrade.d.ts.map +1 -1
  61. package/dist/commands/upgrade.js +46 -34
  62. package/dist/commands/upgrade.js.map +1 -1
  63. package/dist/deployment/diagnostics.d.ts.map +1 -1
  64. package/dist/deployment/diagnostics.js +7 -2
  65. package/dist/deployment/diagnostics.js.map +1 -1
  66. package/dist/index.js +17 -15
  67. package/dist/index.js.map +1 -1
  68. package/dist/utils/logger.d.ts.map +1 -1
  69. package/dist/utils/logger.js +5 -5
  70. package/dist/utils/logger.js.map +1 -1
  71. package/package.json +3 -3
  72. package/src/__tests__/db-init.test.ts +155 -0
  73. package/src/__tests__/db-sync.test.ts +167 -0
  74. package/src/__tests__/deployment-diagnostics.test.ts +68 -60
  75. package/src/__tests__/init.test.ts +17 -17
  76. package/src/__tests__/schema-fragment.test.ts +29 -25
  77. package/src/__tests__/seed.test.ts +25 -25
  78. package/src/commands/db-init.ts +146 -319
  79. package/src/commands/db-status.ts +70 -68
  80. package/src/commands/db-sync.ts +227 -0
  81. package/src/commands/doctor.ts +102 -88
  82. package/src/commands/export.ts +65 -75
  83. package/src/commands/generate.ts +14 -16
  84. package/src/commands/import.ts +125 -140
  85. package/src/commands/init.ts +14 -14
  86. package/src/commands/migrate.ts +29 -35
  87. package/src/commands/seed.ts +294 -300
  88. package/src/commands/update-check.ts +77 -72
  89. package/src/commands/upgrade.ts +100 -85
  90. package/src/deployment/diagnostics.ts +86 -72
  91. package/src/index.ts +32 -30
  92. package/src/utils/logger.ts +10 -10
@@ -1,17 +1,17 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  vi.mock('@actuate-media/cms-core', () => ({
4
4
  extractPlainText: vi.fn((value: string) => value.replace(/<[^>]+>/g, ' ')),
5
5
  hashContent: vi.fn(async () => 'hash-1'),
6
6
  sanitizeHtml: vi.fn((value: string) => value.replace(/<script[\s\S]*?<\/script>/gi, '')),
7
7
  createInitialAdmin: vi.fn(),
8
- }));
8
+ }))
9
9
 
10
- import { createSeedDocument, ensureSeedAdmin, normalizeSeedPayload } from '../commands/seed.js';
10
+ import { createSeedDocument, ensureSeedAdmin, normalizeSeedPayload } from '../commands/seed.js'
11
11
 
12
12
  afterEach(() => {
13
- vi.unstubAllEnvs();
14
- });
13
+ vi.unstubAllEnvs()
14
+ })
15
15
 
16
16
  describe('seed payload normalization', () => {
17
17
  it('separates globals from collection documents', () => {
@@ -30,38 +30,38 @@ describe('seed payload normalization', () => {
30
30
  },
31
31
  ],
32
32
  },
33
- });
33
+ })
34
34
 
35
35
  expect(normalized.globals).toEqual([
36
36
  {
37
37
  slug: 'site-settings',
38
38
  data: { siteName: 'MaidPro', phone: '555-0100' },
39
39
  },
40
- ]);
40
+ ])
41
41
  expect(normalized.documents).toEqual([
42
42
  {
43
43
  collection: 'pages',
44
44
  data: { title: 'Home', slug: 'home' },
45
45
  status: 'PUBLISHED',
46
46
  },
47
- ]);
48
- });
47
+ ])
48
+ })
49
49
 
50
50
  it('keeps backwards-compatible collection-key seed files', () => {
51
51
  const normalized = normalizeSeedPayload({
52
52
  pages: [{ title: 'Home', slug: 'home' }],
53
- });
53
+ })
54
54
 
55
- expect(normalized.globals).toEqual([]);
55
+ expect(normalized.globals).toEqual([])
56
56
  expect(normalized.documents).toEqual([
57
57
  {
58
58
  collection: 'pages',
59
59
  data: { title: 'Home', slug: 'home' },
60
60
  status: 'DRAFT',
61
61
  },
62
- ]);
63
- });
64
- });
62
+ ])
63
+ })
64
+ })
65
65
 
66
66
  describe('seed admin handling', () => {
67
67
  it('refuses to create a first admin without credentials', async () => {
@@ -69,11 +69,11 @@ describe('seed admin handling', () => {
69
69
  user: {
70
70
  findFirst: vi.fn().mockResolvedValue(null),
71
71
  },
72
- };
72
+ }
73
73
 
74
- await expect(ensureSeedAdmin(db)).rejects.toThrow('CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD');
75
- });
76
- });
74
+ await expect(ensureSeedAdmin(db)).rejects.toThrow('CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD')
75
+ })
76
+ })
77
77
 
78
78
  describe('seed document creation', () => {
79
79
  it('creates documents with versions, content hashes, and sanitized HTML', async () => {
@@ -84,10 +84,10 @@ describe('seed document creation', () => {
84
84
  version: {
85
85
  create: vi.fn(),
86
86
  },
87
- };
87
+ }
88
88
  const db = {
89
89
  $transaction: vi.fn(async (fn) => fn(tx)),
90
- };
90
+ }
91
91
 
92
92
  await createSeedDocument(db, 'admin-1', {
93
93
  collection: 'pages',
@@ -97,7 +97,7 @@ describe('seed document creation', () => {
97
97
  slug: 'home',
98
98
  content: '<p>Hello</p><script>alert(1)</script>',
99
99
  },
100
- });
100
+ })
101
101
 
102
102
  expect(tx.document.create).toHaveBeenCalledWith({
103
103
  data: expect.objectContaining({
@@ -114,13 +114,13 @@ describe('seed document creation', () => {
114
114
  content: '<p>Hello</p>',
115
115
  }),
116
116
  }),
117
- });
117
+ })
118
118
  expect(tx.version.create).toHaveBeenCalledWith({
119
119
  data: expect.objectContaining({
120
120
  documentId: 'doc-1',
121
121
  changedById: 'admin-1',
122
122
  changeType: 'CREATE',
123
123
  }),
124
- });
125
- });
126
- });
124
+ })
125
+ })
126
+ })
@@ -1,384 +1,211 @@
1
- import { Command } from "commander";
2
- import { readFile, writeFile, access } from "node:fs/promises";
3
- import { resolve, join } from "node:path";
4
- import { execSync, type ExecSyncOptions } from "node:child_process";
5
- import ora from "ora";
6
- import { logger } from "../utils/logger.js";
1
+ import { Command } from 'commander'
2
+ import { readFile, writeFile, access } from 'node:fs/promises'
3
+ import { resolve, join, dirname } from 'node:path'
4
+ import { createRequire } from 'node:module'
5
+ import { execSync, type ExecSyncOptions } from 'node:child_process'
6
+ import ora from 'ora'
7
+ import { logger } from '../utils/logger.js'
7
8
 
8
- const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
9
+ const CMS_SCHEMA_MARKER = '// ── Actuate CMS models'
10
+
11
+ const require = createRequire(import.meta.url)
9
12
 
10
13
  export let runDbInitCommand = (command: string, options: ExecSyncOptions): void => {
11
- execSync(command, options);
12
- };
14
+ execSync(command, options)
15
+ }
13
16
 
14
- const defaultDbInitCommandRunner = runDbInitCommand;
17
+ const defaultDbInitCommandRunner = runDbInitCommand
15
18
 
16
19
  export function setDbInitCommandRunner(runner: typeof runDbInitCommand): void {
17
- runDbInitCommand = runner;
20
+ runDbInitCommand = runner
18
21
  }
19
22
 
20
23
  export function resetDbInitCommandRunner(): void {
21
- runDbInitCommand = defaultDbInitCommandRunner;
22
- }
23
-
24
- async function fileExists(filePath: string): Promise<boolean> {
24
+ runDbInitCommand = defaultDbInitCommandRunner
25
+ }
26
+
27
+ /**
28
+ * Reads the canonical Prisma schema shipped by the installed
29
+ * `@actuate-media/cms-core` package. This is the single source of truth — it has
30
+ * the correct `@@map("actuate_*")` table names and every model the API handlers
31
+ * expect. Previously `db:init` injected a hand-maintained copy that drifted
32
+ * (missing `@@map`, missing models), producing a database the runtime couldn't
33
+ * query. Overridable for tests.
34
+ */
35
+ export let readCanonicalCmsSchema = async (): Promise<string | null> => {
36
+ const candidates: string[] = []
25
37
  try {
26
- await access(filePath);
27
- return true;
38
+ // `./prisma/schema` is an exported subpath, so this resolves inside the
39
+ // package's `prisma/` directory without depending on the build output.
40
+ const exported = require.resolve('@actuate-media/cms-core/prisma/schema')
41
+ candidates.push(join(dirname(exported), 'schema.prisma'))
28
42
  } catch {
29
- return false;
43
+ /* exports map missing the subpath — fall through to the main entry */
44
+ }
45
+ try {
46
+ const mainEntry = require.resolve('@actuate-media/cms-core')
47
+ candidates.push(join(dirname(mainEntry), '..', 'prisma', 'schema.prisma'))
48
+ } catch {
49
+ /* package not installed */
30
50
  }
31
- }
32
-
33
- function getCmsSchemaFragment(): string {
34
- return `
35
- // ── Actuate CMS models ─────────────────────────────────────────────────────
36
- // Auto-injected by \`actuate db:init\`. Do not remove this marker comment.
37
- // Schema version: 1
38
-
39
- enum DocumentStatus {
40
- DRAFT
41
- PUBLISHED
42
- ARCHIVED
43
- SCHEDULED
44
- }
45
-
46
- model User {
47
- id String @id @default(cuid())
48
- email String @unique
49
- name String @default("")
50
- role String @default("EDITOR")
51
- passwordHash String?
52
- isActive Boolean @default(true)
53
- isApproved Boolean @default(false)
54
- emailVerified Boolean @default(false)
55
- totpEnabled Boolean @default(false)
56
- totpSecret String?
57
- backupCodes Json?
58
- oauthProvider String?
59
- oauthId String?
60
- createdAt DateTime @default(now())
61
- updatedAt DateTime @updatedAt
62
-
63
- sessions Session[]
64
- documentsCreated Document[] @relation("DocumentCreatedBy")
65
- documentsUpdated Document[] @relation("DocumentUpdatedBy")
66
- documentsReviewed Document[] @relation("DocumentReviewer")
67
- versions Version[]
68
- mediaUploaded Media[] @relation("MediaUploadedBy")
69
- auditLogs AuditLog[]
70
-
71
- @@index([role])
72
- @@index([isActive])
73
- }
74
-
75
- model Session {
76
- id String @id @default(cuid())
77
- userId String
78
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
79
- token String? @unique
80
- expiresAt DateTime
81
- revokedAt DateTime?
82
- ipAddress String?
83
- userAgent String?
84
- createdAt DateTime @default(now())
85
-
86
- @@index([userId])
87
- @@index([expiresAt])
88
- }
89
-
90
- model Document {
91
- id String @id @default(cuid())
92
- collection String
93
- slug String?
94
- title String?
95
- data Json
96
- status DocumentStatus @default(DRAFT)
97
- plainText String? @db.Text
98
- locale String?
99
- folderId String?
100
- structuredData Json?
101
- workflowStage String?
102
- reviewerId String?
103
- reviewNote String? @db.Text
104
- publishedAt DateTime?
105
- scheduledAt DateTime?
106
- scheduledUnpublishAt DateTime?
107
- deletedAt DateTime?
108
- contentHash String?
109
- siteId String?
110
- templateId String?
111
- createdById String
112
- updatedById String
113
- createdAt DateTime @default(now())
114
- updatedAt DateTime @updatedAt
115
-
116
- createdBy User @relation("DocumentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict)
117
- updatedBy User @relation("DocumentUpdatedBy", fields: [updatedById], references: [id], onDelete: Restrict)
118
- reviewer User? @relation("DocumentReviewer", fields: [reviewerId], references: [id], onDelete: SetNull)
119
- folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
120
- versions Version[]
121
- formSubmissions FormSubmission[]
122
-
123
- @@unique([collection, slug], name: "collection_slug")
124
- @@index([collection])
125
- @@index([status])
126
- @@index([deletedAt])
127
- @@index([publishedAt])
128
- @@index([folderId])
129
- @@index([locale])
130
- @@index([scheduledAt])
131
- @@index([scheduledUnpublishAt])
132
- @@index([createdById])
133
- @@index([updatedById])
134
- }
135
-
136
- model Media {
137
- id String @id @default(cuid())
138
- filename String
139
- storageKey String @unique
140
- mimeType String
141
- fileSize Int
142
- width Int?
143
- height Int?
144
- altText String?
145
- title String?
146
- blurHash String?
147
- focalPointX Float?
148
- focalPointY Float?
149
- folderId String?
150
- uploadedById String
151
- createdAt DateTime @default(now())
152
- updatedAt DateTime @updatedAt
153
-
154
- uploadedBy User @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: Restrict)
155
- folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
156
-
157
- @@index([folderId])
158
- @@index([mimeType])
159
- @@index([createdAt])
160
- }
161
-
162
- model Version {
163
- id String @id @default(cuid())
164
- documentId String
165
- document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
166
- data Json
167
- changedById String
168
- changedBy User @relation(fields: [changedById], references: [id], onDelete: Restrict)
169
- changeType String @default("UPDATE")
170
- createdAt DateTime @default(now())
171
-
172
- @@index([documentId])
173
- @@index([createdAt])
174
- }
175
-
176
- model Folder {
177
- id String @id @default(cuid())
178
- name String
179
- scope String
180
- parentId String?
181
- position Int @default(0)
182
- createdAt DateTime @default(now())
183
-
184
- parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Cascade)
185
- children Folder[] @relation("FolderTree")
186
- documents Document[]
187
- media Media[]
188
-
189
- @@index([scope])
190
- @@index([parentId])
191
- @@index([scope, parentId, position])
192
- }
193
-
194
- model Redirect {
195
- id String @id @default(cuid())
196
- source String
197
- destination String
198
- statusCode Int @default(301)
199
- isRegex Boolean @default(false)
200
- notes String?
201
- createdAt DateTime @default(now())
202
- updatedAt DateTime @updatedAt
203
51
 
204
- @@unique([source])
205
- @@index([createdAt])
52
+ for (const candidate of candidates) {
53
+ try {
54
+ return await readFile(candidate, 'utf-8')
55
+ } catch {
56
+ /* try the next candidate */
57
+ }
58
+ }
59
+ return null
206
60
  }
207
61
 
208
- model FormSubmission {
209
- id String @id @default(cuid())
210
- formId String
211
- form Document @relation(fields: [formId], references: [id], onDelete: Cascade)
212
- data Json
213
- attribution Json?
214
- submittedAt DateTime @default(now())
215
- createdAt DateTime @default(now())
62
+ const defaultSchemaReader = readCanonicalCmsSchema
216
63
 
217
- @@index([formId])
218
- @@index([submittedAt])
219
- @@index([createdAt])
64
+ export function setCanonicalSchemaReader(reader: typeof readCanonicalCmsSchema): void {
65
+ readCanonicalCmsSchema = reader
220
66
  }
221
67
 
222
- model AuditLog {
223
- id String @id @default(cuid())
224
- event String
225
- userId String?
226
- user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
227
- details Json?
228
- ipAddress String?
229
- userAgent String?
230
- createdAt DateTime @default(now())
231
-
232
- @@index([event])
233
- @@index([userId])
234
- @@index([createdAt])
68
+ export function resetCanonicalSchemaReader(): void {
69
+ readCanonicalCmsSchema = defaultSchemaReader
235
70
  }
236
71
 
237
- model PasswordResetToken {
238
- id String @id @default(cuid())
239
- userId String
240
- tokenHash String
241
- expiresAt DateTime
242
- usedAt DateTime?
243
- createdAt DateTime @default(now())
244
-
245
- @@index([tokenHash])
246
- @@index([userId])
72
+ function netBraces(line: string): number {
73
+ return (line.match(/\{/g)?.length ?? 0) - (line.match(/\}/g)?.length ?? 0)
247
74
  }
248
75
 
249
- model MediaUsage {
250
- id String @id @default(cuid())
251
- mediaId String
252
- documentId String
253
- fieldPath String?
254
- createdAt DateTime @default(now())
76
+ /**
77
+ * Strips the top-level `generator` and `datasource` blocks from a full Prisma
78
+ * schema, leaving only the `enum` and `model` definitions. The consumer keeps
79
+ * their own datasource/generator; we only contribute models.
80
+ */
81
+ export function extractModelsFragment(fullSchema: string): string {
82
+ const lines = fullSchema.split(/\r?\n/)
83
+ const kept: string[] = []
84
+ let skipping = false
85
+ let depth = 0
255
86
 
256
- @@unique([mediaId, documentId, fieldPath])
257
- @@index([mediaId])
258
- @@index([documentId])
259
- }
260
-
261
- model ScriptTag {
262
- id String @id @default(cuid())
263
- name String
264
- code String @db.Text
265
- placement String
266
- scope String
267
- targetPaths String[]
268
- priority Int @default(100)
269
- enabled Boolean @default(true)
270
- createdAt DateTime @default(now())
271
- updatedAt DateTime @updatedAt
87
+ for (const line of lines) {
88
+ if (!skipping) {
89
+ if (/^\s*(generator|datasource)\s+[A-Za-z0-9_]+\s*\{/.test(line)) {
90
+ skipping = true
91
+ depth = netBraces(line)
92
+ if (depth <= 0) skipping = false
93
+ continue
94
+ }
95
+ kept.push(line)
96
+ } else {
97
+ depth += netBraces(line)
98
+ if (depth <= 0) skipping = false
99
+ }
100
+ }
272
101
 
273
- @@index([enabled])
274
- @@index([placement])
102
+ return kept.join('\n').trim()
275
103
  }
276
104
 
277
- model PageTemplate {
278
- id String @id @default(cuid())
279
- name String
280
- description String?
281
- category String @default("content")
282
- tree Json
283
- thumbnail String?
284
- builtIn Boolean @default(false)
285
- createdAt DateTime @default(now())
286
- updatedAt DateTime @updatedAt
105
+ function wrapFragment(models: string): string {
106
+ return `
107
+ ${CMS_SCHEMA_MARKER} ─────────────────────────────────────────────────────────
108
+ // Auto-injected by \`actuate db:init\` from the installed @actuate-media/cms-core
109
+ // package's canonical prisma/schema.prisma (the single source of truth). Do not
110
+ // edit by hand — re-run \`actuate db:init --force\` after upgrading cms-core.
287
111
 
288
- @@index([category])
289
- @@index([builtIn])
112
+ ${models}
113
+ `
290
114
  }
291
115
 
292
- model SavedSection {
293
- id String @id @default(cuid())
294
- name String
295
- description String?
296
- category String @default("content")
297
- tree Json
298
- thumbnail String?
299
- usageCount Int @default(0)
300
- createdAt DateTime @default(now())
301
- updatedAt DateTime @updatedAt
302
-
303
- @@index([category])
304
- }
305
- `;
116
+ async function fileExists(filePath: string): Promise<boolean> {
117
+ try {
118
+ await access(filePath)
119
+ return true
120
+ } catch {
121
+ return false
122
+ }
306
123
  }
307
124
 
308
125
  export function registerDbInitCommand(program: Command): void {
309
126
  program
310
- .command("db:init")
311
- .description("Add Actuate CMS models to your Prisma schema and generate the client")
312
- .option("--schema <path>", "Path to schema.prisma", "prisma/schema.prisma")
313
- .option("--migrate", "Run prisma migrate dev after adding models")
314
- .option("--force", "Overwrite existing CMS models if present")
127
+ .command('db:init')
128
+ .description('Add Actuate CMS models (from the installed cms-core) to your Prisma schema')
129
+ .option('--schema <path>', 'Path to schema.prisma', 'prisma/schema.prisma')
130
+ .option('--migrate', 'Run prisma migrate dev after adding models')
131
+ .option('--force', 'Overwrite existing CMS models if present')
315
132
  .action(async (opts: { schema: string; migrate?: boolean; force?: boolean }) => {
316
- const schemaPath = resolve(process.cwd(), opts.schema);
133
+ const schemaPath = resolve(process.cwd(), opts.schema)
317
134
 
318
135
  if (!(await fileExists(schemaPath))) {
319
- logger.error(`Schema file not found at ${schemaPath}`);
320
- logger.info("Run `npx prisma init` first, or specify --schema <path>.");
321
- process.exitCode = 1;
322
- return;
136
+ logger.error(`Schema file not found at ${schemaPath}`)
137
+ logger.info('Run `npx prisma init` first, or specify --schema <path>.')
138
+ process.exitCode = 1
139
+ return
323
140
  }
324
141
 
325
- const spinner = ora("Reading Prisma schema...").start();
142
+ const spinner = ora('Reading canonical Actuate schema...').start()
143
+
144
+ const canonical = await readCanonicalCmsSchema()
145
+ if (!canonical) {
146
+ spinner.fail('Could not locate @actuate-media/cms-core.')
147
+ logger.info('Install it first: `npm install @actuate-media/cms-core`.')
148
+ process.exitCode = 1
149
+ return
150
+ }
151
+ const fragment = wrapFragment(extractModelsFragment(canonical))
326
152
 
327
- let content: string;
153
+ spinner.text = 'Reading Prisma schema...'
154
+ let content: string
328
155
  try {
329
- content = await readFile(schemaPath, "utf-8");
156
+ content = await readFile(schemaPath, 'utf-8')
330
157
  } catch (err) {
331
- spinner.fail("Failed to read schema file.");
332
- logger.error(err instanceof Error ? err.message : String(err));
333
- process.exitCode = 1;
334
- return;
158
+ spinner.fail('Failed to read schema file.')
159
+ logger.error(err instanceof Error ? err.message : String(err))
160
+ process.exitCode = 1
161
+ return
335
162
  }
336
163
 
337
164
  if (content.includes(CMS_SCHEMA_MARKER)) {
338
165
  if (!opts.force) {
339
- spinner.info("Actuate CMS models already present in schema. Use --force to overwrite.");
340
- return;
166
+ spinner.info('Actuate CMS models already present in schema. Use --force to overwrite.')
167
+ return
341
168
  }
342
- spinner.text = "Removing existing CMS models...";
343
- const markerIndex = content.indexOf(CMS_SCHEMA_MARKER);
344
- content = content.substring(0, markerIndex).trimEnd() + "\n";
169
+ spinner.text = 'Removing existing CMS models...'
170
+ const markerIndex = content.indexOf(CMS_SCHEMA_MARKER)
171
+ content = content.substring(0, markerIndex).trimEnd() + '\n'
345
172
  }
346
173
 
347
- spinner.text = "Adding Actuate CMS models...";
348
- const updatedContent = content.trimEnd() + "\n" + getCmsSchemaFragment();
174
+ spinner.text = 'Adding Actuate CMS models...'
175
+ const updatedContent = content.trimEnd() + '\n' + fragment
349
176
 
350
177
  try {
351
- await writeFile(schemaPath, updatedContent);
352
- spinner.succeed("Actuate CMS models added to schema.");
178
+ await writeFile(schemaPath, updatedContent)
179
+ spinner.succeed('Actuate CMS models added to schema.')
353
180
  } catch (err) {
354
- spinner.fail("Failed to write schema file.");
355
- logger.error(err instanceof Error ? err.message : String(err));
356
- process.exitCode = 1;
357
- return;
181
+ spinner.fail('Failed to write schema file.')
182
+ logger.error(err instanceof Error ? err.message : String(err))
183
+ process.exitCode = 1
184
+ return
358
185
  }
359
186
 
360
- const execOpts: ExecSyncOptions = { stdio: "inherit", cwd: process.cwd() };
187
+ const execOpts: ExecSyncOptions = { stdio: 'inherit', cwd: process.cwd() }
361
188
 
362
- const genSpinner = ora("Running prisma generate...").start();
189
+ const genSpinner = ora('Running prisma generate...').start()
363
190
  try {
364
- genSpinner.stop();
365
- runDbInitCommand("npx prisma generate", execOpts);
366
- logger.success("Prisma client generated.");
191
+ genSpinner.stop()
192
+ runDbInitCommand('npx prisma generate', execOpts)
193
+ logger.success('Prisma client generated.')
367
194
  } catch {
368
- logger.warn("prisma generate failed. You may need to set DATABASE_URL first.");
195
+ logger.warn('prisma generate failed. You may need to set DATABASE_URL first.')
369
196
  }
370
197
 
371
198
  if (opts.migrate) {
372
- const migSpinner = ora("Running prisma migrate dev...").start();
199
+ const migSpinner = ora('Running prisma migrate dev...').start()
373
200
  try {
374
- migSpinner.stop();
375
- runDbInitCommand("npx prisma migrate dev --name actuate-cms-init", execOpts);
376
- logger.success("Migration created and applied.");
201
+ migSpinner.stop()
202
+ runDbInitCommand('npx prisma migrate dev --name actuate-cms-init', execOpts)
203
+ logger.success('Migration created and applied.')
377
204
  } catch {
378
- logger.warn("prisma migrate dev failed. Run it manually after setting DATABASE_URL.");
205
+ logger.warn('prisma migrate dev failed. Run it manually after setting DATABASE_URL.')
379
206
  }
380
207
  } else {
381
- logger.info("Run `npx prisma migrate dev --name actuate-cms` to create the migration.");
208
+ logger.info('Run `npx prisma migrate dev --name actuate-cms` to create the migration.')
382
209
  }
383
- });
210
+ })
384
211
  }