@actuate-media/cli 0.4.0 → 0.4.2

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 (79) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +21 -10
  3. package/CHANGELOG.md +34 -0
  4. package/dist/__tests__/deployment-diagnostics.test.js +40 -0
  5. package/dist/__tests__/deployment-diagnostics.test.js.map +1 -1
  6. package/dist/__tests__/init.test.js.map +1 -1
  7. package/dist/__tests__/schema-fragment.test.js +1 -1
  8. package/dist/__tests__/schema-fragment.test.js.map +1 -1
  9. package/dist/__tests__/seed.test.js.map +1 -1
  10. package/dist/commands/db-init.d.ts +2 -2
  11. package/dist/commands/db-init.d.ts.map +1 -1
  12. package/dist/commands/db-init.js +32 -32
  13. package/dist/commands/db-init.js.map +1 -1
  14. package/dist/commands/db-status.d.ts +1 -1
  15. package/dist/commands/db-status.d.ts.map +1 -1
  16. package/dist/commands/db-status.js +33 -33
  17. package/dist/commands/db-status.js.map +1 -1
  18. package/dist/commands/doctor.d.ts +1 -1
  19. package/dist/commands/doctor.d.ts.map +1 -1
  20. package/dist/commands/doctor.js +55 -38
  21. package/dist/commands/doctor.js.map +1 -1
  22. package/dist/commands/export.d.ts +1 -1
  23. package/dist/commands/export.d.ts.map +1 -1
  24. package/dist/commands/export.js +32 -32
  25. package/dist/commands/export.js.map +1 -1
  26. package/dist/commands/generate.d.ts +1 -1
  27. package/dist/commands/generate.d.ts.map +1 -1
  28. package/dist/commands/generate.js +8 -8
  29. package/dist/commands/generate.js.map +1 -1
  30. package/dist/commands/import.d.ts +1 -1
  31. package/dist/commands/import.d.ts.map +1 -1
  32. package/dist/commands/import.js +55 -58
  33. package/dist/commands/import.js.map +1 -1
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/migrate.d.ts +1 -1
  37. package/dist/commands/migrate.d.ts.map +1 -1
  38. package/dist/commands/migrate.js +18 -24
  39. package/dist/commands/migrate.js.map +1 -1
  40. package/dist/commands/seed.d.ts +1 -1
  41. package/dist/commands/seed.d.ts.map +1 -1
  42. package/dist/commands/seed.js +156 -157
  43. package/dist/commands/seed.js.map +1 -1
  44. package/dist/commands/update-check.d.ts +1 -1
  45. package/dist/commands/update-check.d.ts.map +1 -1
  46. package/dist/commands/update-check.js +34 -27
  47. package/dist/commands/update-check.js.map +1 -1
  48. package/dist/commands/upgrade.d.ts +1 -1
  49. package/dist/commands/upgrade.d.ts.map +1 -1
  50. package/dist/commands/upgrade.js +41 -34
  51. package/dist/commands/upgrade.js.map +1 -1
  52. package/dist/deployment/diagnostics.d.ts +2 -0
  53. package/dist/deployment/diagnostics.d.ts.map +1 -1
  54. package/dist/deployment/diagnostics.js +50 -1
  55. package/dist/deployment/diagnostics.js.map +1 -1
  56. package/dist/index.js +15 -15
  57. package/dist/index.js.map +1 -1
  58. package/dist/utils/logger.d.ts.map +1 -1
  59. package/dist/utils/logger.js +5 -5
  60. package/dist/utils/logger.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/__tests__/deployment-diagnostics.test.ts +100 -50
  63. package/src/__tests__/init.test.ts +17 -17
  64. package/src/__tests__/schema-fragment.test.ts +29 -25
  65. package/src/__tests__/seed.test.ts +25 -25
  66. package/src/commands/db-init.ts +59 -59
  67. package/src/commands/db-status.ts +70 -68
  68. package/src/commands/doctor.ts +110 -86
  69. package/src/commands/export.ts +65 -75
  70. package/src/commands/generate.ts +14 -16
  71. package/src/commands/import.ts +125 -140
  72. package/src/commands/init.ts +14 -14
  73. package/src/commands/migrate.ts +29 -35
  74. package/src/commands/seed.ts +294 -300
  75. package/src/commands/update-check.ts +77 -72
  76. package/src/commands/upgrade.ts +92 -85
  77. package/src/deployment/diagnostics.ts +124 -61
  78. package/src/index.ts +30 -30
  79. package/src/utils/logger.ts +10 -10
@@ -1,414 +1,408 @@
1
- import { Command } from "commander";
2
- import { readFile } from "node:fs/promises";
3
- import { existsSync } from "node:fs";
4
- import { createRequire } from "node:module";
5
- import path from "node:path";
6
- import { createInterface } from "node:readline/promises";
7
- import { pathToFileURL } from "node:url";
8
- import ora from "ora";
9
- import { logger } from "../utils/logger.js";
1
+ import { Command } from 'commander'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import path from 'node:path'
6
+ import { createInterface } from 'node:readline/promises'
7
+ import { pathToFileURL } from 'node:url'
8
+ import ora from 'ora'
9
+ import { logger } from '../utils/logger.js'
10
10
 
11
11
  async function confirm(question: string): Promise<boolean> {
12
- const rl = createInterface({ input: process.stdin, output: process.stdout });
13
- const answer = await rl.question(`${question} (y/N) `);
14
- rl.close();
15
- return answer.trim().toLowerCase() === "y";
12
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
13
+ const answer = await rl.question(`${question} (y/N) `)
14
+ rl.close()
15
+ return answer.trim().toLowerCase() === 'y'
16
16
  }
17
17
 
18
18
  const DEMO_PAGES = [
19
19
  {
20
- title: "Home",
21
- slug: "home",
22
- content: "<h1>Welcome</h1><p>Your homepage content goes here.</p>",
20
+ title: 'Home',
21
+ slug: 'home',
22
+ content: '<h1>Welcome</h1><p>Your homepage content goes here.</p>',
23
23
  },
24
24
  {
25
- title: "About",
26
- slug: "about",
27
- content:
28
- "<h1>About Us</h1><p>Learn more about our team and mission.</p>",
25
+ title: 'About',
26
+ slug: 'about',
27
+ content: '<h1>About Us</h1><p>Learn more about our team and mission.</p>',
29
28
  },
30
29
  {
31
- title: "Contact",
32
- slug: "contact",
33
- content:
34
- "<h1>Contact</h1><p>Get in touch with us via the form below.</p>",
30
+ title: 'Contact',
31
+ slug: 'contact',
32
+ content: '<h1>Contact</h1><p>Get in touch with us via the form below.</p>',
35
33
  },
36
34
  {
37
- title: "Privacy Policy",
38
- slug: "privacy-policy",
39
- content:
40
- "<h1>Privacy Policy</h1><p>Your privacy matters to us. Read our full policy.</p>",
35
+ title: 'Privacy Policy',
36
+ slug: 'privacy-policy',
37
+ content: '<h1>Privacy Policy</h1><p>Your privacy matters to us. Read our full policy.</p>',
41
38
  },
42
39
  {
43
- title: "Terms of Service",
44
- slug: "terms",
45
- content:
46
- "<h1>Terms of Service</h1><p>Please read the following terms carefully.</p>",
40
+ title: 'Terms of Service',
41
+ slug: 'terms',
42
+ content: '<h1>Terms of Service</h1><p>Please read the following terms carefully.</p>',
47
43
  },
48
- ];
44
+ ]
49
45
 
50
46
  const DEMO_POSTS = [
51
47
  {
52
- title: "Getting Started with Actuate CMS",
53
- slug: "getting-started",
54
- excerpt: "Learn how to set up and configure your new CMS.",
48
+ title: 'Getting Started with Actuate CMS',
49
+ slug: 'getting-started',
50
+ excerpt: 'Learn how to set up and configure your new CMS.',
55
51
  content:
56
- "<h1>Getting Started</h1><p>Welcome to Actuate CMS. This guide walks you through initial setup.</p>",
57
- status: "PUBLISHED",
52
+ '<h1>Getting Started</h1><p>Welcome to Actuate CMS. This guide walks you through initial setup.</p>',
53
+ status: 'PUBLISHED',
58
54
  },
59
55
  {
60
- title: "Content Modeling Best Practices",
61
- slug: "content-modeling",
62
- excerpt: "Design your collections and fields for maximum flexibility.",
56
+ title: 'Content Modeling Best Practices',
57
+ slug: 'content-modeling',
58
+ excerpt: 'Design your collections and fields for maximum flexibility.',
63
59
  content:
64
- "<h1>Content Modeling</h1><p>Effective content modeling is the foundation of a great CMS.</p>",
65
- status: "PUBLISHED",
60
+ '<h1>Content Modeling</h1><p>Effective content modeling is the foundation of a great CMS.</p>',
61
+ status: 'PUBLISHED',
66
62
  },
67
63
  {
68
- title: "Working with Media",
69
- slug: "working-with-media",
70
- excerpt: "Upload, organize, and optimize your media assets.",
71
- content:
72
- "<h1>Working with Media</h1><p>Actuate CMS provides powerful media management.</p>",
73
- status: "PUBLISHED",
64
+ title: 'Working with Media',
65
+ slug: 'working-with-media',
66
+ excerpt: 'Upload, organize, and optimize your media assets.',
67
+ content: '<h1>Working with Media</h1><p>Actuate CMS provides powerful media management.</p>',
68
+ status: 'PUBLISHED',
74
69
  },
75
70
  {
76
- title: "SEO Optimization Tips",
77
- slug: "seo-optimization",
78
- excerpt: "Boost your search rankings with built-in SEO tools.",
79
- content:
80
- "<h1>SEO Optimization</h1><p>Follow these tips to improve your site visibility.</p>",
81
- status: "PUBLISHED",
71
+ title: 'SEO Optimization Tips',
72
+ slug: 'seo-optimization',
73
+ excerpt: 'Boost your search rankings with built-in SEO tools.',
74
+ content: '<h1>SEO Optimization</h1><p>Follow these tips to improve your site visibility.</p>',
75
+ status: 'PUBLISHED',
82
76
  },
83
77
  {
84
- title: "Building Custom Plugins",
85
- slug: "building-plugins",
86
- excerpt: "Extend Actuate CMS with your own plugins.",
87
- content:
88
- "<h1>Building Plugins</h1><p>The plugin system lets you add custom functionality.</p>",
89
- status: "PUBLISHED",
78
+ title: 'Building Custom Plugins',
79
+ slug: 'building-plugins',
80
+ excerpt: 'Extend Actuate CMS with your own plugins.',
81
+ content: '<h1>Building Plugins</h1><p>The plugin system lets you add custom functionality.</p>',
82
+ status: 'PUBLISHED',
90
83
  },
91
84
  {
92
- title: "API Reference Overview",
93
- slug: "api-reference",
94
- excerpt: "A comprehensive guide to the Actuate CMS REST API.",
95
- content:
96
- "<h1>API Reference</h1><p>Use the API to integrate your content anywhere.</p>",
97
- status: "PUBLISHED",
85
+ title: 'API Reference Overview',
86
+ slug: 'api-reference',
87
+ excerpt: 'A comprehensive guide to the Actuate CMS REST API.',
88
+ content: '<h1>API Reference</h1><p>Use the API to integrate your content anywhere.</p>',
89
+ status: 'PUBLISHED',
98
90
  },
99
91
  {
100
- title: "Deployment Guide",
101
- slug: "deployment-guide",
102
- excerpt: "Deploy Actuate CMS to Vercel, AWS, or self-hosted.",
103
- content:
104
- "<h1>Deployment Guide</h1><p>Multiple deployment options for every use case.</p>",
105
- status: "PUBLISHED",
92
+ title: 'Deployment Guide',
93
+ slug: 'deployment-guide',
94
+ excerpt: 'Deploy Actuate CMS to Vercel, AWS, or self-hosted.',
95
+ content: '<h1>Deployment Guide</h1><p>Multiple deployment options for every use case.</p>',
96
+ status: 'PUBLISHED',
106
97
  },
107
98
  {
108
- title: "Multi-language Content",
109
- slug: "multi-language",
110
- excerpt: "Set up localization and manage translated content.",
99
+ title: 'Multi-language Content',
100
+ slug: 'multi-language',
101
+ excerpt: 'Set up localization and manage translated content.',
111
102
  content:
112
- "<h1>Multi-language Content</h1><p>Reach a global audience with localized content.</p>",
113
- status: "PUBLISHED",
103
+ '<h1>Multi-language Content</h1><p>Reach a global audience with localized content.</p>',
104
+ status: 'PUBLISHED',
114
105
  },
115
106
  {
116
- title: "Webhooks and Integrations",
117
- slug: "webhooks-integrations",
118
- excerpt: "Connect Actuate CMS to external services with webhooks.",
119
- content:
120
- "<h1>Webhooks</h1><p>Automate workflows by connecting to third-party services.</p>",
121
- status: "DRAFT",
107
+ title: 'Webhooks and Integrations',
108
+ slug: 'webhooks-integrations',
109
+ excerpt: 'Connect Actuate CMS to external services with webhooks.',
110
+ content: '<h1>Webhooks</h1><p>Automate workflows by connecting to third-party services.</p>',
111
+ status: 'DRAFT',
122
112
  },
123
113
  {
124
- title: "Advanced Access Control",
125
- slug: "access-control",
126
- excerpt: "Fine-tune permissions with role-based access control.",
127
- content:
128
- "<h1>Access Control</h1><p>Protect your content with granular permissions.</p>",
129
- status: "DRAFT",
114
+ title: 'Advanced Access Control',
115
+ slug: 'access-control',
116
+ excerpt: 'Fine-tune permissions with role-based access control.',
117
+ content: '<h1>Access Control</h1><p>Protect your content with granular permissions.</p>',
118
+ status: 'DRAFT',
130
119
  },
131
- ];
120
+ ]
132
121
 
133
122
  const DEMO_FORMS = [
134
123
  {
135
- title: "Contact Form",
136
- slug: "contact-form",
124
+ title: 'Contact Form',
125
+ slug: 'contact-form',
137
126
  fields: [
138
- { name: "name", type: "text", required: true },
139
- { name: "email", type: "email", required: true },
140
- { name: "message", type: "textarea", required: true },
127
+ { name: 'name', type: 'text', required: true },
128
+ { name: 'email', type: 'email', required: true },
129
+ { name: 'message', type: 'textarea', required: true },
141
130
  ],
142
- submitLabel: "Send Message",
131
+ submitLabel: 'Send Message',
143
132
  successMessage: "Thanks for reaching out! We'll get back to you soon.",
144
133
  },
145
134
  {
146
- title: "Newsletter Signup",
147
- slug: "newsletter",
135
+ title: 'Newsletter Signup',
136
+ slug: 'newsletter',
148
137
  fields: [
149
- { name: "email", type: "email", required: true },
150
- { name: "firstName", type: "text", required: false },
138
+ { name: 'email', type: 'email', required: true },
139
+ { name: 'firstName', type: 'text', required: false },
151
140
  ],
152
- submitLabel: "Subscribe",
141
+ submitLabel: 'Subscribe',
153
142
  successMessage: "You're subscribed! Check your inbox for confirmation.",
154
143
  },
155
144
  {
156
- title: "Feedback Form",
157
- slug: "feedback",
145
+ title: 'Feedback Form',
146
+ slug: 'feedback',
158
147
  fields: [
159
- { name: "name", type: "text", required: false },
160
- { name: "rating", type: "select", options: ["1", "2", "3", "4", "5"], required: true },
161
- { name: "comments", type: "textarea", required: false },
148
+ { name: 'name', type: 'text', required: false },
149
+ { name: 'rating', type: 'select', options: ['1', '2', '3', '4', '5'], required: true },
150
+ { name: 'comments', type: 'textarea', required: false },
162
151
  ],
163
- submitLabel: "Submit Feedback",
164
- successMessage: "Thank you for your feedback!",
152
+ submitLabel: 'Submit Feedback',
153
+ successMessage: 'Thank you for your feedback!',
165
154
  },
166
- ];
155
+ ]
167
156
 
168
157
  const DEMO_USERS = [
169
- { email: "editor@example.com", name: "Demo Editor", role: "EDITOR" as const },
170
- { email: "author@example.com", name: "Demo Author", role: "AUTHOR" as const },
171
- ];
158
+ { email: 'editor@example.com', name: 'Demo Editor', role: 'EDITOR' as const },
159
+ { email: 'author@example.com', name: 'Demo Author', role: 'AUTHOR' as const },
160
+ ]
172
161
 
173
162
  interface SeedOptions {
174
- demo?: boolean;
175
- file?: string;
176
- reset?: boolean;
163
+ demo?: boolean
164
+ file?: string
165
+ reset?: boolean
177
166
  }
178
167
 
179
168
  export interface NormalizedSeedDocument {
180
- collection: string;
181
- data: Record<string, unknown>;
182
- status: string;
169
+ collection: string
170
+ data: Record<string, unknown>
171
+ status: string
183
172
  }
184
173
 
185
174
  export interface NormalizedSeedGlobal {
186
- slug: string;
187
- data: Record<string, unknown>;
175
+ slug: string
176
+ data: Record<string, unknown>
188
177
  }
189
178
 
190
179
  export interface NormalizedSeedPayload {
191
- documents: NormalizedSeedDocument[];
192
- globals: NormalizedSeedGlobal[];
180
+ documents: NormalizedSeedDocument[]
181
+ globals: NormalizedSeedGlobal[]
193
182
  }
194
183
 
195
184
  const SEED_FILE_CANDIDATES = [
196
- "actuate.seed.json",
197
- "actuate.seed.ts",
198
- "actuate.seed.js",
199
- "actuate.seed.mjs",
200
- "cms.seed.json",
201
- ];
185
+ 'actuate.seed.json',
186
+ 'actuate.seed.ts',
187
+ 'actuate.seed.js',
188
+ 'actuate.seed.mjs',
189
+ 'cms.seed.json',
190
+ ]
202
191
 
203
192
  function asRecord(value: unknown): Record<string, unknown> {
204
- return value && typeof value === "object" && !Array.isArray(value)
205
- ? value as Record<string, unknown>
206
- : {};
193
+ return value && typeof value === 'object' && !Array.isArray(value)
194
+ ? (value as Record<string, unknown>)
195
+ : {}
207
196
  }
208
197
 
209
198
  function normalizeDocument(collection: string, doc: unknown): NormalizedSeedDocument {
210
- const record = asRecord(doc);
199
+ const record = asRecord(doc)
211
200
  return {
212
201
  collection,
213
202
  data: asRecord(record.data ?? record),
214
- status: typeof record.status === "string" ? record.status : "DRAFT",
215
- };
203
+ status: typeof record.status === 'string' ? record.status : 'DRAFT',
204
+ }
216
205
  }
217
206
 
218
207
  export function normalizeSeedPayload(seedData: unknown): NormalizedSeedPayload {
219
- const documents: NormalizedSeedDocument[] = [];
220
- const globals: NormalizedSeedGlobal[] = [];
208
+ const documents: NormalizedSeedDocument[] = []
209
+ const globals: NormalizedSeedGlobal[] = []
221
210
 
222
211
  if (Array.isArray(seedData)) {
223
212
  for (const doc of seedData) {
224
- const record = asRecord(doc);
225
- documents.push(normalizeDocument(
226
- typeof record.collection === "string" ? record.collection : "imported",
227
- record.data ? record : { data: record },
228
- ));
213
+ const record = asRecord(doc)
214
+ documents.push(
215
+ normalizeDocument(
216
+ typeof record.collection === 'string' ? record.collection : 'imported',
217
+ record.data ? record : { data: record },
218
+ ),
219
+ )
229
220
  }
230
- return { documents, globals };
221
+ return { documents, globals }
231
222
  }
232
223
 
233
- const root = asRecord(seedData);
224
+ const root = asRecord(seedData)
234
225
 
235
- const globalEntries = asRecord(root.globals);
226
+ const globalEntries = asRecord(root.globals)
236
227
  for (const [slug, value] of Object.entries(globalEntries)) {
237
- globals.push({ slug, data: asRecord(value) });
228
+ globals.push({ slug, data: asRecord(value) })
238
229
  }
239
230
 
240
- const collections = root.collections ? asRecord(root.collections) : root;
231
+ const collections = root.collections ? asRecord(root.collections) : root
241
232
  for (const [collection, docs] of Object.entries(collections)) {
242
- if (collection === "globals" || collection === "collections") continue;
243
- if (!Array.isArray(docs)) continue;
233
+ if (collection === 'globals' || collection === 'collections') continue
234
+ if (!Array.isArray(docs)) continue
244
235
  for (const doc of docs) {
245
- documents.push(normalizeDocument(collection, doc));
236
+ documents.push(normalizeDocument(collection, doc))
246
237
  }
247
238
  }
248
239
 
249
- return { documents, globals };
240
+ return { documents, globals }
250
241
  }
251
242
 
252
243
  function findConventionSeedFile(): string | null {
253
244
  for (const candidate of SEED_FILE_CANDIDATES) {
254
- if (existsSync(candidate)) return candidate;
245
+ if (existsSync(candidate)) return candidate
255
246
  }
256
- return null;
247
+ return null
257
248
  }
258
249
 
259
250
  async function loadSeedFile(filePath: string): Promise<unknown> {
260
- const extension = path.extname(filePath);
261
- if (extension === ".json" || extension === "") {
262
- const raw = await readFile(filePath, "utf-8");
263
- return JSON.parse(raw);
251
+ const extension = path.extname(filePath)
252
+ if (extension === '.json' || extension === '') {
253
+ const raw = await readFile(filePath, 'utf-8')
254
+ return JSON.parse(raw)
264
255
  }
265
256
 
266
- const fileUrl = pathToFileURL(path.resolve(filePath)).href;
267
- const mod = extension === ".ts"
268
- ? await import("tsx/esm/api").then(({ tsImport }) => tsImport(fileUrl, import.meta.url))
269
- : await import(fileUrl);
257
+ const fileUrl = pathToFileURL(path.resolve(filePath)).href
258
+ const mod =
259
+ extension === '.ts'
260
+ ? await import('tsx/esm/api').then(({ tsImport }) => tsImport(fileUrl, import.meta.url))
261
+ : await import(fileUrl)
270
262
 
271
- return (mod as { default?: unknown; seed?: unknown }).default
272
- ?? (mod as { seed?: unknown }).seed;
263
+ return (mod as { default?: unknown; seed?: unknown }).default ?? (mod as { seed?: unknown }).seed
273
264
  }
274
265
 
275
266
  async function runSeed(options: SeedOptions): Promise<void> {
276
- const conventionFile = !options.demo && !options.file ? findConventionSeedFile() : null;
277
- const file = options.file ?? conventionFile ?? undefined;
267
+ const conventionFile = !options.demo && !options.file ? findConventionSeedFile() : null
268
+ const file = options.file ?? conventionFile ?? undefined
278
269
 
279
270
  if (!options.demo && !file) {
280
- logger.error("Specify --demo, --file <path>, or add actuate.seed.json in the project root.");
281
- process.exit(1);
271
+ logger.error('Specify --demo, --file <path>, or add actuate.seed.json in the project root.')
272
+ process.exit(1)
282
273
  }
283
274
 
284
- let seededDb: { db: any; disconnect: () => Promise<void> } | null = null;
275
+ let seededDb: { db: any; disconnect: () => Promise<void> } | null = null
285
276
 
286
277
  try {
287
- seededDb = await getSeedDatabase();
288
- const db = seededDb.db;
278
+ seededDb = await getSeedDatabase()
279
+ const db = seededDb.db
289
280
 
290
281
  if (options.reset) {
291
- const yes = await confirm(
292
- "This will delete ALL existing documents and versions. Continue?",
293
- );
282
+ const yes = await confirm('This will delete ALL existing documents and versions. Continue?')
294
283
  if (!yes) {
295
- logger.warn("Seed cancelled.");
296
- return;
284
+ logger.warn('Seed cancelled.')
285
+ return
297
286
  }
298
287
 
299
- const resetSpinner = ora("Clearing existing data…").start();
300
- await db.version.deleteMany({});
288
+ const resetSpinner = ora('Clearing existing data…').start()
289
+ await db.version.deleteMany({})
301
290
  if (db.mediaUsage?.deleteMany) {
302
- await db.mediaUsage.deleteMany({});
291
+ await db.mediaUsage.deleteMany({})
303
292
  }
304
- await db.document.deleteMany({});
305
- resetSpinner.succeed("Existing data cleared.");
293
+ await db.document.deleteMany({})
294
+ resetSpinner.succeed('Existing data cleared.')
306
295
  }
307
296
 
308
297
  if (options.demo) {
309
- await seedDemoData(db);
298
+ await seedDemoData(db)
310
299
  }
311
300
 
312
301
  if (file) {
313
- await seedFromFile(db, file);
302
+ await seedFromFile(db, file)
314
303
  }
315
304
  } catch (err) {
316
- const message = err instanceof Error ? err.message : String(err);
317
- logger.error(`Seed failed: ${message}`);
318
- process.exit(1);
305
+ const message = err instanceof Error ? err.message : String(err)
306
+ logger.error(`Seed failed: ${message}`)
307
+ process.exit(1)
319
308
  } finally {
320
- await seededDb?.disconnect();
309
+ await seededDb?.disconnect()
321
310
  }
322
311
  }
323
312
 
324
313
  async function getSeedDatabase(): Promise<{ db: any; disconnect: () => Promise<void> }> {
325
- const { getDB, initDB, isDBInitialized } = await import("@actuate-media/cms-core");
314
+ const { getDB, initDB, isDBInitialized } = await import('@actuate-media/cms-core')
326
315
 
327
316
  if (isDBInitialized()) {
328
- return { db: getDB<any>(), disconnect: async () => {} };
317
+ return { db: getDB<any>(), disconnect: async () => {} }
329
318
  }
330
319
 
331
- const db = await createProjectPrismaClient();
332
- initDB(db);
320
+ const db = await createProjectPrismaClient()
321
+ initDB(db)
333
322
  return {
334
323
  db,
335
324
  disconnect: async () => {
336
- if (typeof db.$disconnect === "function") {
337
- await db.$disconnect();
325
+ if (typeof db.$disconnect === 'function') {
326
+ await db.$disconnect()
338
327
  }
339
328
  },
340
- };
329
+ }
341
330
  }
342
331
 
343
332
  async function createProjectPrismaClient(): Promise<any> {
344
333
  if (!process.env.DATABASE_URL) {
345
- throw new Error("DATABASE_URL is required to run seed/populate.");
334
+ throw new Error('DATABASE_URL is required to run seed/populate.')
346
335
  }
347
336
 
348
- const requireFromProject = createRequire(path.join(process.cwd(), "package.json"));
349
- const generatedClient = path.resolve("generated", "prisma", "client.ts");
337
+ const requireFromProject = createRequire(path.join(process.cwd(), 'package.json'))
338
+ const generatedClient = path.resolve('generated', 'prisma', 'client.ts')
350
339
 
351
340
  if (existsSync(generatedClient)) {
352
341
  const [{ tsImport }, adapterModule, pgModule] = await Promise.all([
353
- import("tsx/esm/api"),
354
- import(pathToFileURL(requireFromProject.resolve("@prisma/adapter-pg")).href),
355
- import(pathToFileURL(requireFromProject.resolve("pg")).href),
356
- ]);
357
- const { PrismaClient } = await tsImport(pathToFileURL(generatedClient).href, import.meta.url) as {
358
- PrismaClient: new (options?: unknown) => any;
359
- };
360
- const { PrismaPg } = adapterModule as { PrismaPg: new (pool: unknown) => unknown };
361
- const pg = (pgModule as { default?: typeof pgModule }).default ?? pgModule;
362
- const pool = new (pg as any).Pool({ connectionString: process.env.DATABASE_URL });
363
- const adapter = new PrismaPg(pool);
364
- return new PrismaClient({ adapter } as any);
342
+ import('tsx/esm/api'),
343
+ import(pathToFileURL(requireFromProject.resolve('@prisma/adapter-pg')).href),
344
+ import(pathToFileURL(requireFromProject.resolve('pg')).href),
345
+ ])
346
+ const { PrismaClient } = (await tsImport(
347
+ pathToFileURL(generatedClient).href,
348
+ import.meta.url,
349
+ )) as {
350
+ PrismaClient: new (options?: unknown) => any
351
+ }
352
+ const { PrismaPg } = adapterModule as { PrismaPg: new (pool: unknown) => unknown }
353
+ const pg = (pgModule as { default?: typeof pgModule }).default ?? pgModule
354
+ const pool = new (pg as any).Pool({ connectionString: process.env.DATABASE_URL })
355
+ const adapter = new PrismaPg(pool)
356
+ return new PrismaClient({ adapter } as any)
365
357
  }
366
358
 
367
- const clientModule = await import(pathToFileURL(requireFromProject.resolve("@prisma/client")).href) as {
368
- PrismaClient: new () => any;
369
- };
370
- return new clientModule.PrismaClient();
359
+ const clientModule = (await import(
360
+ pathToFileURL(requireFromProject.resolve('@prisma/client')).href
361
+ )) as {
362
+ PrismaClient: new () => any
363
+ }
364
+ return new clientModule.PrismaClient()
371
365
  }
372
366
 
373
367
  export async function ensureSeedAdmin(db: any): Promise<{ id: string }> {
374
- const existing = await db.user.findFirst({ where: { role: "ADMIN" } });
375
- if (existing) return existing;
368
+ const existing = await db.user.findFirst({ where: { role: 'ADMIN' } })
369
+ if (existing) return existing
376
370
 
377
- const email = process.env.CMS_ADMIN_EMAIL;
378
- const password = process.env.CMS_ADMIN_PASSWORD;
379
- const name = process.env.CMS_ADMIN_NAME ?? "Admin";
371
+ const email = process.env.CMS_ADMIN_EMAIL
372
+ const password = process.env.CMS_ADMIN_PASSWORD
373
+ const name = process.env.CMS_ADMIN_NAME ?? 'Admin'
380
374
 
381
375
  if (!email || !password) {
382
376
  throw new Error(
383
- "No admin user exists. Set CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD before running seed, or complete the setup wizard first.",
384
- );
377
+ 'No admin user exists. Set CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD before running seed, or complete the setup wizard first.',
378
+ )
385
379
  }
386
380
 
387
- const { createInitialAdmin } = await import("@actuate-media/cms-core");
388
- const result = await createInitialAdmin(db, { email, password, name });
381
+ const { createInitialAdmin } = await import('@actuate-media/cms-core')
382
+ const result = await createInitialAdmin(db, { email, password, name })
389
383
  if (!result.success || !result.userId) {
390
- throw new Error(result.error ?? "Failed to create initial admin user");
384
+ throw new Error(result.error ?? 'Failed to create initial admin user')
391
385
  }
392
386
 
393
- return { id: result.userId };
387
+ return { id: result.userId }
394
388
  }
395
389
 
396
390
  function sanitizeSeedData(value: unknown, sanitizeHtml: (html: string) => string): unknown {
397
391
  if (Array.isArray(value)) {
398
- return value.map((item) => sanitizeSeedData(item, sanitizeHtml));
392
+ return value.map((item) => sanitizeSeedData(item, sanitizeHtml))
399
393
  }
400
- if (value && typeof value === "object") {
394
+ if (value && typeof value === 'object') {
401
395
  return Object.fromEntries(
402
396
  Object.entries(value as Record<string, unknown>).map(([key, item]) => [
403
397
  key,
404
398
  sanitizeSeedData(item, sanitizeHtml),
405
399
  ]),
406
- );
400
+ )
407
401
  }
408
- if (typeof value === "string" && /<[a-z][\s\S]*>/i.test(value)) {
409
- return sanitizeHtml(value);
402
+ if (typeof value === 'string' && /<[a-z][\s\S]*>/i.test(value)) {
403
+ return sanitizeHtml(value)
410
404
  }
411
- return value;
405
+ return value
412
406
  }
413
407
 
414
408
  export async function createSeedDocument(
@@ -416,78 +410,78 @@ export async function createSeedDocument(
416
410
  userId: string,
417
411
  doc: NormalizedSeedDocument,
418
412
  ): Promise<void> {
419
- const { extractPlainText, hashContent, sanitizeHtml } = await import("@actuate-media/cms-core");
420
- const data = sanitizeSeedData(doc.data, sanitizeHtml) as Record<string, unknown>;
421
- const serialized = JSON.stringify(data);
422
- const plainText = extractPlainText(serialized);
423
- const contentHash = await hashContent(serialized);
413
+ const { extractPlainText, hashContent, sanitizeHtml } = await import('@actuate-media/cms-core')
414
+ const data = sanitizeSeedData(doc.data, sanitizeHtml) as Record<string, unknown>
415
+ const serialized = JSON.stringify(data)
416
+ const plainText = extractPlainText(serialized)
417
+ const contentHash = await hashContent(serialized)
424
418
 
425
419
  await db.$transaction(async (tx: any) => {
426
420
  const created = await tx.document.create({
427
421
  data: {
428
422
  collection: doc.collection,
429
- title: typeof data.title === "string" ? data.title : null,
430
- slug: typeof data.slug === "string" ? data.slug : null,
423
+ title: typeof data.title === 'string' ? data.title : null,
424
+ slug: typeof data.slug === 'string' ? data.slug : null,
431
425
  data,
432
426
  status: doc.status,
433
- publishedAt: doc.status === "PUBLISHED" ? new Date() : null,
427
+ publishedAt: doc.status === 'PUBLISHED' ? new Date() : null,
434
428
  createdById: userId,
435
429
  updatedById: userId,
436
430
  plainText,
437
431
  contentHash,
438
432
  },
439
- });
433
+ })
440
434
 
441
435
  await tx.version.create({
442
436
  data: {
443
437
  documentId: created.id,
444
438
  data,
445
439
  changedById: userId,
446
- changeType: "CREATE",
440
+ changeType: 'CREATE',
447
441
  },
448
- });
449
- });
442
+ })
443
+ })
450
444
  }
451
445
 
452
446
  async function seedDemoData(db: any): Promise<void> {
453
- const spinner = ora("Seeding demo data…").start();
447
+ const spinner = ora('Seeding demo data…').start()
454
448
 
455
- const adminUser = await ensureSeedAdmin(db);
456
- const userId = adminUser.id;
449
+ const adminUser = await ensureSeedAdmin(db)
450
+ const userId = adminUser.id
457
451
 
458
- let pagesCreated = 0;
452
+ let pagesCreated = 0
459
453
  for (const page of DEMO_PAGES) {
460
454
  await createSeedDocument(db, userId, {
461
- collection: "pages",
455
+ collection: 'pages',
462
456
  data: page,
463
- status: "PUBLISHED",
464
- });
465
- pagesCreated++;
457
+ status: 'PUBLISHED',
458
+ })
459
+ pagesCreated++
466
460
  }
467
461
 
468
- let postsCreated = 0;
462
+ let postsCreated = 0
469
463
  for (const post of DEMO_POSTS) {
470
464
  await createSeedDocument(db, userId, {
471
- collection: "posts",
465
+ collection: 'posts',
472
466
  data: post,
473
467
  status: post.status,
474
- });
475
- postsCreated++;
468
+ })
469
+ postsCreated++
476
470
  }
477
471
 
478
- let formsCreated = 0;
472
+ let formsCreated = 0
479
473
  for (const form of DEMO_FORMS) {
480
474
  await createSeedDocument(db, userId, {
481
- collection: "forms",
475
+ collection: 'forms',
482
476
  data: form,
483
- status: "PUBLISHED",
484
- });
485
- formsCreated++;
477
+ status: 'PUBLISHED',
478
+ })
479
+ formsCreated++
486
480
  }
487
481
 
488
- let usersCreated = 0;
482
+ let usersCreated = 0
489
483
  for (const user of DEMO_USERS) {
490
- const exists = await db.user.findFirst({ where: { email: user.email } });
484
+ const exists = await db.user.findFirst({ where: { email: user.email } })
491
485
  if (!exists) {
492
486
  await db.user.create({
493
487
  data: {
@@ -498,74 +492,74 @@ async function seedDemoData(db: any): Promise<void> {
498
492
  isApproved: true,
499
493
  emailVerified: true,
500
494
  },
501
- });
502
- usersCreated++;
495
+ })
496
+ usersCreated++
503
497
  }
504
498
  }
505
499
 
506
- spinner.succeed("Demo data seeded successfully.");
507
- logger.info(` Pages: ${pagesCreated}`);
508
- logger.info(` Posts: ${postsCreated}`);
509
- logger.info(` Forms: ${formsCreated}`);
510
- logger.info(` Users: ${usersCreated} (+ existing admin)`);
500
+ spinner.succeed('Demo data seeded successfully.')
501
+ logger.info(` Pages: ${pagesCreated}`)
502
+ logger.info(` Posts: ${postsCreated}`)
503
+ logger.info(` Forms: ${formsCreated}`)
504
+ logger.info(` Users: ${usersCreated} (+ existing admin)`)
511
505
  }
512
506
 
513
507
  async function seedFromFile(db: any, filePath: string): Promise<void> {
514
508
  if (!existsSync(filePath)) {
515
- logger.error(`File not found: ${filePath}`);
516
- process.exit(1);
509
+ logger.error(`File not found: ${filePath}`)
510
+ process.exit(1)
517
511
  }
518
512
 
519
- const spinner = ora(`Seeding from ${filePath}…`).start();
513
+ const spinner = ora(`Seeding from ${filePath}…`).start()
520
514
 
521
- let seedData: any;
515
+ let seedData: any
522
516
  try {
523
- seedData = await loadSeedFile(filePath);
517
+ seedData = await loadSeedFile(filePath)
524
518
  } catch {
525
- spinner.fail("Invalid seed file.");
526
- process.exit(1);
519
+ spinner.fail('Invalid seed file.')
520
+ process.exit(1)
527
521
  }
528
522
 
529
- if (!Array.isArray(seedData) && typeof seedData !== "object") {
530
- spinner.fail("Seed file must contain a JSON array or an object with collection keys.");
531
- process.exit(1);
523
+ if (!Array.isArray(seedData) && typeof seedData !== 'object') {
524
+ spinner.fail('Seed file must contain a JSON array or an object with collection keys.')
525
+ process.exit(1)
532
526
  }
533
527
 
534
- const adminUser = await ensureSeedAdmin(db);
535
- const userId = adminUser.id;
528
+ const adminUser = await ensureSeedAdmin(db)
529
+ const userId = adminUser.id
536
530
 
537
- const normalized = normalizeSeedPayload(seedData);
538
- const { updateGlobal } = await import("@actuate-media/cms-core");
539
- const ctx = { userId, role: "ADMIN", db };
531
+ const normalized = normalizeSeedPayload(seedData)
532
+ const { updateGlobal } = await import('@actuate-media/cms-core')
533
+ const ctx = { userId, role: 'ADMIN', db }
540
534
 
541
- let documentCount = 0;
535
+ let documentCount = 0
542
536
  for (const doc of normalized.documents) {
543
- await createSeedDocument(db, userId, doc);
544
- documentCount++;
537
+ await createSeedDocument(db, userId, doc)
538
+ documentCount++
545
539
  }
546
540
 
547
- let globalCount = 0;
541
+ let globalCount = 0
548
542
  for (const global of normalized.globals) {
549
- await updateGlobal(global.slug, global.data, ctx);
550
- globalCount++;
543
+ await updateGlobal(global.slug, global.data, ctx)
544
+ globalCount++
551
545
  }
552
546
 
553
- spinner.succeed(`Seeded ${documentCount} documents and ${globalCount} globals from ${filePath}.`);
547
+ spinner.succeed(`Seeded ${documentCount} documents and ${globalCount} globals from ${filePath}.`)
554
548
  }
555
549
 
556
550
  export function registerSeedCommand(program: Command): void {
557
551
  program
558
- .command("seed")
559
- .description("Seed the database with demo or custom data")
560
- .option("--demo", "Seed demo content (pages, posts, forms, users)")
561
- .option("--file <path>", "Seed from a JSON, JavaScript, or TypeScript file")
562
- .option("--reset", "Clear existing data before seeding")
563
- .action(runSeed);
552
+ .command('seed')
553
+ .description('Seed the database with demo or custom data')
554
+ .option('--demo', 'Seed demo content (pages, posts, forms, users)')
555
+ .option('--file <path>', 'Seed from a JSON, JavaScript, or TypeScript file')
556
+ .option('--reset', 'Clear existing data before seeding')
557
+ .action(runSeed)
564
558
 
565
559
  program
566
- .command("populate")
567
- .description("Populate the database from actuate.seed.json or a custom seed file")
568
- .option("--file <path>", "Seed from a JSON, JavaScript, or TypeScript file")
569
- .option("--reset", "Clear existing data before seeding")
570
- .action(runSeed);
560
+ .command('populate')
561
+ .description('Populate the database from actuate.seed.json or a custom seed file')
562
+ .option('--file <path>', 'Seed from a JSON, JavaScript, or TypeScript file')
563
+ .option('--reset', 'Clear existing data before seeding')
564
+ .action(runSeed)
571
565
  }