@actuate-media/cli 0.1.4 → 0.2.1

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.
@@ -1,5 +1,4 @@
1
-  WARN  Issue while reading "/home/runner/work/actuatecms/actuatecms/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
2
1
 
3
- > @actuate-media/cli@0.1.4 build /home/runner/work/actuatecms/actuatecms/packages/cli
2
+ > @actuate-media/cli@0.2.1 build /home/runner/work/actuatecms/actuatecms/packages/cli
4
3
  > tsc
5
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # @actuate-media/cli
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [0621037]
8
+ - @actuate-media/cms-core@0.3.1
9
+
10
+ ## 0.2.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 4e3a5eb: RC2-RC4 and hardening release.
15
+
16
+ **cms-core:**
17
+ - Replace isomorphic-dompurify with sanitize-html (eliminates Vercel ESM crash)
18
+ - Remove cms-core from serverExternalPackages, move to transpilePackages
19
+ - Add GET /api/cms/health endpoint with model probing and diagnostics
20
+ - Harden db() Proxy with try/catch on property access and rawDb()
21
+ - Harden safeCount/safeFindMany against Proxy throws
22
+ - Add startup validation for missing CMS_SECRET (503 instead of 500)
23
+ - Bypass auth on /stats when secret is missing (returns zero-data)
24
+ - Loosen NextConfigLike types (experimental/images/webpack as unknown)
25
+ - Implement login anomaly detection (new IP, brute force, unusual hours)
26
+ - Implement webhook DNS resolution with private range checking
27
+ - Implement TOTP reauth verification
28
+ - Add Zod validation for secret in defineConfig (min 32 chars)
29
+ - Read CMS_CORE_VERSION from package.json at runtime instead of hardcoding '0.1.0'
30
+
31
+ **cms-admin:**
32
+ - Ship pre-compiled CSS (dist/actuate-admin.css) with all 48 responsive classes
33
+ - New export: @actuate-media/cms-admin/styles/precompiled.css
34
+ - Add CSS containment (`contain: layout style`) on .actuate-admin
35
+ - Fix ShortcutHelp modal using React state instead of DOM manipulation
36
+ - Add fixed-position layout isolation wrapper to AdminRoot
37
+ - Add useApiData retry limits (maxRetries=3) with exponential backoff
38
+ - Dashboard shows health-aware "Database Setup Required" banner
39
+
40
+ **cli:**
41
+ - Add `actuate db:init` command (injects CMS models into Prisma schema)
42
+ - Add `actuate db:status` command (checks model presence and DB connectivity)
43
+
44
+ **create-actuate-cms:**
45
+ - Embed full CMS schema (9 models + enum) in generated schema.prisma
46
+ - Add @actuate-media/cli to scaffolded devDependencies
47
+ - Use pre-compiled CSS import in scaffolded admin layout
48
+
49
+ ### Patch Changes
50
+
51
+ - Updated dependencies [4e3a5eb]
52
+ - @actuate-media/cms-core@0.3.0
53
+
3
54
  ## 0.1.4
4
55
 
5
56
  ### Patch Changes
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerDbInitCommand(program: Command): void;
3
+ //# sourceMappingURL=db-init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-init.d.ts","sourceRoot":"","sources":["../../src/commands/db-init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgOpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4E5D"}
@@ -0,0 +1,294 @@
1
+ import { readFile, writeFile, access } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import ora from "ora";
5
+ import { logger } from "../utils/logger.js";
6
+ const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
7
+ async function fileExists(filePath) {
8
+ try {
9
+ await access(filePath);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ function getCmsSchemaFragment() {
17
+ return `
18
+ // ── Actuate CMS models ─────────────────────────────────────────────────────
19
+ // Auto-injected by \`actuate db:init\`. Do not remove this marker comment.
20
+ // Schema version: 1
21
+
22
+ enum DocumentStatus {
23
+ DRAFT
24
+ PUBLISHED
25
+ ARCHIVED
26
+ SCHEDULED
27
+ }
28
+
29
+ model User {
30
+ id String @id @default(cuid())
31
+ email String @unique
32
+ name String @default("")
33
+ role String @default("EDITOR")
34
+ passwordHash String?
35
+ isActive Boolean @default(true)
36
+ isApproved Boolean @default(false)
37
+ emailVerified Boolean @default(false)
38
+ totpEnabled Boolean @default(false)
39
+ totpSecret String?
40
+ backupCodes Json?
41
+ oauthProvider String?
42
+ oauthId String?
43
+ createdAt DateTime @default(now())
44
+ updatedAt DateTime @updatedAt
45
+
46
+ sessions Session[]
47
+ documentsCreated Document[] @relation("DocumentCreatedBy")
48
+ documentsUpdated Document[] @relation("DocumentUpdatedBy")
49
+ documentsReviewed Document[] @relation("DocumentReviewer")
50
+ versions Version[]
51
+ mediaUploaded Media[] @relation("MediaUploadedBy")
52
+ auditLogs AuditLog[]
53
+
54
+ @@index([role])
55
+ @@index([isActive])
56
+ }
57
+
58
+ model Session {
59
+ id String @id @default(cuid())
60
+ userId String
61
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
62
+ token String? @unique
63
+ expiresAt DateTime
64
+ revokedAt DateTime?
65
+ ipAddress String?
66
+ userAgent String?
67
+ createdAt DateTime @default(now())
68
+
69
+ @@index([userId])
70
+ @@index([expiresAt])
71
+ }
72
+
73
+ model Document {
74
+ id String @id @default(cuid())
75
+ collection String
76
+ slug String?
77
+ title String?
78
+ data Json
79
+ status DocumentStatus @default(DRAFT)
80
+ plainText String? @db.Text
81
+ locale String?
82
+ folderId String?
83
+ structuredData Json?
84
+ workflowStage String?
85
+ reviewerId String?
86
+ reviewNote String? @db.Text
87
+ publishedAt DateTime?
88
+ scheduledAt DateTime?
89
+ scheduledUnpublishAt DateTime?
90
+ deletedAt DateTime?
91
+ contentHash String?
92
+ siteId String?
93
+ templateId String?
94
+ createdById String
95
+ updatedById String
96
+ createdAt DateTime @default(now())
97
+ updatedAt DateTime @updatedAt
98
+
99
+ createdBy User @relation("DocumentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict)
100
+ updatedBy User @relation("DocumentUpdatedBy", fields: [updatedById], references: [id], onDelete: Restrict)
101
+ reviewer User? @relation("DocumentReviewer", fields: [reviewerId], references: [id], onDelete: SetNull)
102
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
103
+ versions Version[]
104
+ formSubmissions FormSubmission[]
105
+
106
+ @@unique([collection, slug], name: "collection_slug")
107
+ @@index([collection])
108
+ @@index([status])
109
+ @@index([deletedAt])
110
+ @@index([publishedAt])
111
+ @@index([folderId])
112
+ @@index([locale])
113
+ @@index([scheduledAt])
114
+ @@index([scheduledUnpublishAt])
115
+ @@index([createdById])
116
+ @@index([updatedById])
117
+ }
118
+
119
+ model Media {
120
+ id String @id @default(cuid())
121
+ filename String
122
+ storageKey String @unique
123
+ mimeType String
124
+ fileSize Int
125
+ width Int?
126
+ height Int?
127
+ altText String?
128
+ title String?
129
+ blurHash String?
130
+ focalPointX Float?
131
+ focalPointY Float?
132
+ folderId String?
133
+ uploadedById String
134
+ createdAt DateTime @default(now())
135
+ updatedAt DateTime @updatedAt
136
+
137
+ uploadedBy User @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: Restrict)
138
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
139
+
140
+ @@index([folderId])
141
+ @@index([mimeType])
142
+ @@index([createdAt])
143
+ }
144
+
145
+ model Version {
146
+ id String @id @default(cuid())
147
+ documentId String
148
+ document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
149
+ data Json
150
+ changedById String
151
+ changedBy User @relation(fields: [changedById], references: [id], onDelete: Restrict)
152
+ changeType String @default("UPDATE")
153
+ createdAt DateTime @default(now())
154
+
155
+ @@index([documentId])
156
+ @@index([createdAt])
157
+ }
158
+
159
+ model Folder {
160
+ id String @id @default(cuid())
161
+ name String
162
+ scope String
163
+ parentId String?
164
+ position Int @default(0)
165
+ createdAt DateTime @default(now())
166
+
167
+ parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Cascade)
168
+ children Folder[] @relation("FolderTree")
169
+ documents Document[]
170
+ media Media[]
171
+
172
+ @@index([scope])
173
+ @@index([parentId])
174
+ @@index([scope, parentId, position])
175
+ }
176
+
177
+ model Redirect {
178
+ id String @id @default(cuid())
179
+ source String
180
+ destination String
181
+ statusCode Int @default(301)
182
+ isRegex Boolean @default(false)
183
+ notes String?
184
+ createdAt DateTime @default(now())
185
+ updatedAt DateTime @updatedAt
186
+
187
+ @@unique([source])
188
+ @@index([createdAt])
189
+ }
190
+
191
+ model FormSubmission {
192
+ id String @id @default(cuid())
193
+ formId String
194
+ form Document @relation(fields: [formId], references: [id], onDelete: Cascade)
195
+ data Json
196
+ attribution Json?
197
+ submittedAt DateTime @default(now())
198
+ createdAt DateTime @default(now())
199
+
200
+ @@index([formId])
201
+ @@index([submittedAt])
202
+ @@index([createdAt])
203
+ }
204
+
205
+ model AuditLog {
206
+ id String @id @default(cuid())
207
+ event String
208
+ userId String?
209
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
210
+ details Json?
211
+ ipAddress String?
212
+ userAgent String?
213
+ createdAt DateTime @default(now())
214
+
215
+ @@index([event])
216
+ @@index([userId])
217
+ @@index([createdAt])
218
+ }
219
+ `;
220
+ }
221
+ export function registerDbInitCommand(program) {
222
+ program
223
+ .command("db:init")
224
+ .description("Add Actuate CMS models to your Prisma schema and generate the client")
225
+ .option("--schema <path>", "Path to schema.prisma", "prisma/schema.prisma")
226
+ .option("--migrate", "Run prisma migrate dev after adding models")
227
+ .option("--force", "Overwrite existing CMS models if present")
228
+ .action(async (opts) => {
229
+ const schemaPath = resolve(process.cwd(), opts.schema);
230
+ if (!(await fileExists(schemaPath))) {
231
+ logger.error(`Schema file not found at ${schemaPath}`);
232
+ logger.info("Run `npx prisma init` first, or specify --schema <path>.");
233
+ process.exitCode = 1;
234
+ return;
235
+ }
236
+ const spinner = ora("Reading Prisma schema...").start();
237
+ let content;
238
+ try {
239
+ content = await readFile(schemaPath, "utf-8");
240
+ }
241
+ catch (err) {
242
+ spinner.fail("Failed to read schema file.");
243
+ logger.error(err instanceof Error ? err.message : String(err));
244
+ process.exitCode = 1;
245
+ return;
246
+ }
247
+ if (content.includes(CMS_SCHEMA_MARKER)) {
248
+ if (!opts.force) {
249
+ spinner.info("Actuate CMS models already present in schema. Use --force to overwrite.");
250
+ return;
251
+ }
252
+ spinner.text = "Removing existing CMS models...";
253
+ const markerIndex = content.indexOf(CMS_SCHEMA_MARKER);
254
+ content = content.substring(0, markerIndex).trimEnd() + "\n";
255
+ }
256
+ spinner.text = "Adding Actuate CMS models...";
257
+ const updatedContent = content.trimEnd() + "\n" + getCmsSchemaFragment();
258
+ try {
259
+ await writeFile(schemaPath, updatedContent);
260
+ spinner.succeed("Actuate CMS models added to schema.");
261
+ }
262
+ catch (err) {
263
+ spinner.fail("Failed to write schema file.");
264
+ logger.error(err instanceof Error ? err.message : String(err));
265
+ process.exitCode = 1;
266
+ return;
267
+ }
268
+ const execOpts = { stdio: "inherit", cwd: process.cwd() };
269
+ const genSpinner = ora("Running prisma generate...").start();
270
+ try {
271
+ genSpinner.stop();
272
+ execSync("npx prisma generate", execOpts);
273
+ logger.success("Prisma client generated.");
274
+ }
275
+ catch {
276
+ logger.warn("prisma generate failed. You may need to set DATABASE_URL first.");
277
+ }
278
+ if (opts.migrate) {
279
+ const migSpinner = ora("Running prisma migrate dev...").start();
280
+ try {
281
+ migSpinner.stop();
282
+ execSync("npx prisma migrate dev --name actuate-cms-init", execOpts);
283
+ logger.success("Migration created and applied.");
284
+ }
285
+ catch {
286
+ logger.warn("prisma migrate dev failed. Run it manually after setting DATABASE_URL.");
287
+ }
288
+ }
289
+ else {
290
+ logger.info("Run `npx prisma migrate dev --name actuate-cms` to create the migration.");
291
+ }
292
+ });
293
+ }
294
+ //# sourceMappingURL=db-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-init.js","sourceRoot":"","sources":["../../src/commands/db-init.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAQ,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAwB,MAAM,oBAAoB,CAAC;AACpE,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAErD,KAAK,UAAU,UAAU,CAAC,QAAgB;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB;IAC3B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0MR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,sEAAsE,CAAC;SACnF,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,EAAE,sBAAsB,CAAC;SAC1E,MAAM,CAAC,WAAW,EAAE,4CAA4C,CAAC;SACjE,MAAM,CAAC,SAAS,EAAE,0CAA0C,CAAC;SAC7D,MAAM,CAAC,KAAK,EAAE,IAA4D,EAAE,EAAE;QAC7E,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAEvD,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,KAAK,CAAC,4BAA4B,UAAU,EAAE,CAAC,CAAC;YACvD,MAAM,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;YACxE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,GAAG,CAAC,0BAA0B,CAAC,CAAC,KAAK,EAAE,CAAC;QAExD,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;YAC5C,MAAM,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;gBAChB,OAAO,CAAC,IAAI,CAAC,yEAAyE,CAAC,CAAC;gBACxF,OAAO;YACT,CAAC;YACD,OAAO,CAAC,IAAI,GAAG,iCAAiC,CAAC;YACjD,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;YACvD,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;QAC/D,CAAC;QAED,OAAO,CAAC,IAAI,GAAG,8BAA8B,CAAC;QAC9C,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,GAAG,oBAAoB,EAAE,CAAC;QAEzE,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;YAC5C,OAAO,CAAC,OAAO,CAAC,qCAAqC,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;YAC7C,MAAM,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAoB,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;QAE3E,MAAM,UAAU,GAAG,GAAG,CAAC,4BAA4B,CAAC,CAAC,KAAK,EAAE,CAAC;QAC7D,IAAI,CAAC;YACH,UAAU,CAAC,IAAI,EAAE,CAAC;YAClB,QAAQ,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;YAC1C,MAAM,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;QACjF,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,UAAU,GAAG,GAAG,CAAC,+BAA+B,CAAC,CAAC,KAAK,EAAE,CAAC;YAChE,IAAI,CAAC;gBACH,UAAU,CAAC,IAAI,EAAE,CAAC;gBAClB,QAAQ,CAAC,gDAAgD,EAAE,QAAQ,CAAC,CAAC;gBACrE,MAAM,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,CAAC,IAAI,CAAC,wEAAwE,CAAC,CAAC;YACxF,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerDbStatusCommand(program: Command): void;
3
+ //# sourceMappingURL=db-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-status.d.ts","sourceRoot":"","sources":["../../src/commands/db-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+BpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmG9D"}
@@ -0,0 +1,118 @@
1
+ import { execSync } from "node:child_process";
2
+ import { readFile, access } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import ora from "ora";
5
+ import chalk from "chalk";
6
+ import { logger } from "../utils/logger.js";
7
+ const CMS_EXPECTED_MODELS = [
8
+ "User",
9
+ "Session",
10
+ "Document",
11
+ "Media",
12
+ "Version",
13
+ "Folder",
14
+ "Redirect",
15
+ "FormSubmission",
16
+ "AuditLog",
17
+ ];
18
+ const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
19
+ async function fileExists(filePath) {
20
+ try {
21
+ await access(filePath);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ export function registerDbStatusCommand(program) {
29
+ program
30
+ .command("db:status")
31
+ .description("Check which Actuate CMS models are present in your Prisma schema")
32
+ .option("--schema <path>", "Path to schema.prisma", "prisma/schema.prisma")
33
+ .action(async (opts) => {
34
+ const schemaPath = resolve(process.cwd(), opts.schema);
35
+ if (!(await fileExists(schemaPath))) {
36
+ logger.error(`Schema file not found at ${schemaPath}`);
37
+ logger.info("Run `npx prisma init` first, or specify --schema <path>.");
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ const spinner = ora("Checking schema...").start();
42
+ let content;
43
+ try {
44
+ content = await readFile(schemaPath, "utf-8");
45
+ }
46
+ catch (err) {
47
+ spinner.fail("Failed to read schema file.");
48
+ logger.error(err instanceof Error ? err.message : String(err));
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ spinner.stop();
53
+ const hasMarker = content.includes(CMS_SCHEMA_MARKER);
54
+ const modelRegex = /^model\s+(\w+)\s*\{/gm;
55
+ const schemaModels = new Set();
56
+ let match;
57
+ while ((match = modelRegex.exec(content)) !== null) {
58
+ schemaModels.add(match[1]);
59
+ }
60
+ console.log();
61
+ console.log(chalk.bold(" Actuate CMS Model Status"));
62
+ console.log(chalk.dim(" ─────────────────────────────────"));
63
+ console.log();
64
+ let present = 0;
65
+ let missing = 0;
66
+ for (const model of CMS_EXPECTED_MODELS) {
67
+ if (schemaModels.has(model)) {
68
+ console.log(` ${chalk.green("✓")} ${model}`);
69
+ present++;
70
+ }
71
+ else {
72
+ console.log(` ${chalk.red("✗")} ${model} ${chalk.dim("— missing")}`);
73
+ missing++;
74
+ }
75
+ }
76
+ console.log();
77
+ console.log(chalk.dim(" ─────────────────────────────────"));
78
+ console.log(` ${chalk.green(present)} present, ${missing > 0 ? chalk.red(missing) : chalk.green(missing)} missing`);
79
+ if (!hasMarker) {
80
+ console.log();
81
+ logger.warn("CMS schema marker not found. Models may have been added manually.");
82
+ logger.info("Run `actuate db:init` to add CMS models with proper markers.");
83
+ }
84
+ if (missing > 0) {
85
+ console.log();
86
+ logger.info("Run `actuate db:init` to add missing models.");
87
+ }
88
+ let dbConnected = false;
89
+ try {
90
+ spinner.start("Checking database connection...");
91
+ execSync("npx prisma db execute --stdin --schema " + JSON.stringify(schemaPath), {
92
+ input: "SELECT 1;",
93
+ stdio: ["pipe", "pipe", "pipe"],
94
+ cwd: process.cwd(),
95
+ });
96
+ spinner.succeed("Database connection OK.");
97
+ dbConnected = true;
98
+ }
99
+ catch {
100
+ spinner.warn("Could not connect to database. Check DATABASE_URL.");
101
+ }
102
+ if (dbConnected && missing === 0) {
103
+ try {
104
+ spinner.start("Checking migration status...");
105
+ spinner.stop();
106
+ execSync("npx prisma migrate status", {
107
+ stdio: "inherit",
108
+ cwd: process.cwd(),
109
+ });
110
+ }
111
+ catch {
112
+ logger.warn("Could not check migration status.");
113
+ }
114
+ }
115
+ console.log();
116
+ });
117
+ }
118
+ //# sourceMappingURL=db-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-status.js","sourceRoot":"","sources":["../../src/commands/db-status.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import { registerImportCommand } from "./commands/import.js";
7
7
  import { registerExportCommand } from "./commands/export.js";
8
8
  import { registerUpgradeCommand } from "./commands/upgrade.js";
9
9
  import { registerUpdateCheckCommand } from "./commands/update-check.js";
10
+ import { registerDbInitCommand } from "./commands/db-init.js";
11
+ import { registerDbStatusCommand } from "./commands/db-status.js";
10
12
  const program = new Command();
11
13
  program
12
14
  .name("actuate")
@@ -19,5 +21,7 @@ registerImportCommand(program);
19
21
  registerExportCommand(program);
20
22
  registerUpgradeCommand(program);
21
23
  registerUpdateCheckCommand(program);
24
+ registerDbInitCommand(program);
25
+ registerDbStatusCommand(program);
22
26
  program.parse();
23
27
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAExE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,uBAAuB,CAAC,OAAO,CAAC,CAAC;AACjC,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,0BAA0B,CAAC,OAAO,CAAC,CAAC;AAEpC,OAAO,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAElE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,uBAAuB,CAAC,OAAO,CAAC,CAAC;AACjC,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,0BAA0B,CAAC,OAAO,CAAC,CAAC;AACpC,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAEjC,OAAO,CAAC,KAAK,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for Actuate CMS — migrations, codegen, imports, exports, and upgrades",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  "commander": "^13.1.0",
20
20
  "chalk": "^5.4.1",
21
21
  "ora": "^8.2.0",
22
- "@actuate-media/cms-core": "0.2.3"
22
+ "@actuate-media/cms-core": "0.3.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "typescript": "^5.7.0"
@@ -0,0 +1,301 @@
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";
7
+
8
+ const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
9
+
10
+ async function fileExists(filePath: string): Promise<boolean> {
11
+ try {
12
+ await access(filePath);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function getCmsSchemaFragment(): string {
20
+ return `
21
+ // ── Actuate CMS models ─────────────────────────────────────────────────────
22
+ // Auto-injected by \`actuate db:init\`. Do not remove this marker comment.
23
+ // Schema version: 1
24
+
25
+ enum DocumentStatus {
26
+ DRAFT
27
+ PUBLISHED
28
+ ARCHIVED
29
+ SCHEDULED
30
+ }
31
+
32
+ model User {
33
+ id String @id @default(cuid())
34
+ email String @unique
35
+ name String @default("")
36
+ role String @default("EDITOR")
37
+ passwordHash String?
38
+ isActive Boolean @default(true)
39
+ isApproved Boolean @default(false)
40
+ emailVerified Boolean @default(false)
41
+ totpEnabled Boolean @default(false)
42
+ totpSecret String?
43
+ backupCodes Json?
44
+ oauthProvider String?
45
+ oauthId String?
46
+ createdAt DateTime @default(now())
47
+ updatedAt DateTime @updatedAt
48
+
49
+ sessions Session[]
50
+ documentsCreated Document[] @relation("DocumentCreatedBy")
51
+ documentsUpdated Document[] @relation("DocumentUpdatedBy")
52
+ documentsReviewed Document[] @relation("DocumentReviewer")
53
+ versions Version[]
54
+ mediaUploaded Media[] @relation("MediaUploadedBy")
55
+ auditLogs AuditLog[]
56
+
57
+ @@index([role])
58
+ @@index([isActive])
59
+ }
60
+
61
+ model Session {
62
+ id String @id @default(cuid())
63
+ userId String
64
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
65
+ token String? @unique
66
+ expiresAt DateTime
67
+ revokedAt DateTime?
68
+ ipAddress String?
69
+ userAgent String?
70
+ createdAt DateTime @default(now())
71
+
72
+ @@index([userId])
73
+ @@index([expiresAt])
74
+ }
75
+
76
+ model Document {
77
+ id String @id @default(cuid())
78
+ collection String
79
+ slug String?
80
+ title String?
81
+ data Json
82
+ status DocumentStatus @default(DRAFT)
83
+ plainText String? @db.Text
84
+ locale String?
85
+ folderId String?
86
+ structuredData Json?
87
+ workflowStage String?
88
+ reviewerId String?
89
+ reviewNote String? @db.Text
90
+ publishedAt DateTime?
91
+ scheduledAt DateTime?
92
+ scheduledUnpublishAt DateTime?
93
+ deletedAt DateTime?
94
+ contentHash String?
95
+ siteId String?
96
+ templateId String?
97
+ createdById String
98
+ updatedById String
99
+ createdAt DateTime @default(now())
100
+ updatedAt DateTime @updatedAt
101
+
102
+ createdBy User @relation("DocumentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict)
103
+ updatedBy User @relation("DocumentUpdatedBy", fields: [updatedById], references: [id], onDelete: Restrict)
104
+ reviewer User? @relation("DocumentReviewer", fields: [reviewerId], references: [id], onDelete: SetNull)
105
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
106
+ versions Version[]
107
+ formSubmissions FormSubmission[]
108
+
109
+ @@unique([collection, slug], name: "collection_slug")
110
+ @@index([collection])
111
+ @@index([status])
112
+ @@index([deletedAt])
113
+ @@index([publishedAt])
114
+ @@index([folderId])
115
+ @@index([locale])
116
+ @@index([scheduledAt])
117
+ @@index([scheduledUnpublishAt])
118
+ @@index([createdById])
119
+ @@index([updatedById])
120
+ }
121
+
122
+ model Media {
123
+ id String @id @default(cuid())
124
+ filename String
125
+ storageKey String @unique
126
+ mimeType String
127
+ fileSize Int
128
+ width Int?
129
+ height Int?
130
+ altText String?
131
+ title String?
132
+ blurHash String?
133
+ focalPointX Float?
134
+ focalPointY Float?
135
+ folderId String?
136
+ uploadedById String
137
+ createdAt DateTime @default(now())
138
+ updatedAt DateTime @updatedAt
139
+
140
+ uploadedBy User @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: Restrict)
141
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
142
+
143
+ @@index([folderId])
144
+ @@index([mimeType])
145
+ @@index([createdAt])
146
+ }
147
+
148
+ model Version {
149
+ id String @id @default(cuid())
150
+ documentId String
151
+ document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
152
+ data Json
153
+ changedById String
154
+ changedBy User @relation(fields: [changedById], references: [id], onDelete: Restrict)
155
+ changeType String @default("UPDATE")
156
+ createdAt DateTime @default(now())
157
+
158
+ @@index([documentId])
159
+ @@index([createdAt])
160
+ }
161
+
162
+ model Folder {
163
+ id String @id @default(cuid())
164
+ name String
165
+ scope String
166
+ parentId String?
167
+ position Int @default(0)
168
+ createdAt DateTime @default(now())
169
+
170
+ parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Cascade)
171
+ children Folder[] @relation("FolderTree")
172
+ documents Document[]
173
+ media Media[]
174
+
175
+ @@index([scope])
176
+ @@index([parentId])
177
+ @@index([scope, parentId, position])
178
+ }
179
+
180
+ model Redirect {
181
+ id String @id @default(cuid())
182
+ source String
183
+ destination String
184
+ statusCode Int @default(301)
185
+ isRegex Boolean @default(false)
186
+ notes String?
187
+ createdAt DateTime @default(now())
188
+ updatedAt DateTime @updatedAt
189
+
190
+ @@unique([source])
191
+ @@index([createdAt])
192
+ }
193
+
194
+ model FormSubmission {
195
+ id String @id @default(cuid())
196
+ formId String
197
+ form Document @relation(fields: [formId], references: [id], onDelete: Cascade)
198
+ data Json
199
+ attribution Json?
200
+ submittedAt DateTime @default(now())
201
+ createdAt DateTime @default(now())
202
+
203
+ @@index([formId])
204
+ @@index([submittedAt])
205
+ @@index([createdAt])
206
+ }
207
+
208
+ model AuditLog {
209
+ id String @id @default(cuid())
210
+ event String
211
+ userId String?
212
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
213
+ details Json?
214
+ ipAddress String?
215
+ userAgent String?
216
+ createdAt DateTime @default(now())
217
+
218
+ @@index([event])
219
+ @@index([userId])
220
+ @@index([createdAt])
221
+ }
222
+ `;
223
+ }
224
+
225
+ export function registerDbInitCommand(program: Command): void {
226
+ program
227
+ .command("db:init")
228
+ .description("Add Actuate CMS models to your Prisma schema and generate the client")
229
+ .option("--schema <path>", "Path to schema.prisma", "prisma/schema.prisma")
230
+ .option("--migrate", "Run prisma migrate dev after adding models")
231
+ .option("--force", "Overwrite existing CMS models if present")
232
+ .action(async (opts: { schema: string; migrate?: boolean; force?: boolean }) => {
233
+ const schemaPath = resolve(process.cwd(), opts.schema);
234
+
235
+ if (!(await fileExists(schemaPath))) {
236
+ logger.error(`Schema file not found at ${schemaPath}`);
237
+ logger.info("Run `npx prisma init` first, or specify --schema <path>.");
238
+ process.exitCode = 1;
239
+ return;
240
+ }
241
+
242
+ const spinner = ora("Reading Prisma schema...").start();
243
+
244
+ let content: string;
245
+ try {
246
+ content = await readFile(schemaPath, "utf-8");
247
+ } catch (err) {
248
+ spinner.fail("Failed to read schema file.");
249
+ logger.error(err instanceof Error ? err.message : String(err));
250
+ process.exitCode = 1;
251
+ return;
252
+ }
253
+
254
+ if (content.includes(CMS_SCHEMA_MARKER)) {
255
+ if (!opts.force) {
256
+ spinner.info("Actuate CMS models already present in schema. Use --force to overwrite.");
257
+ return;
258
+ }
259
+ spinner.text = "Removing existing CMS models...";
260
+ const markerIndex = content.indexOf(CMS_SCHEMA_MARKER);
261
+ content = content.substring(0, markerIndex).trimEnd() + "\n";
262
+ }
263
+
264
+ spinner.text = "Adding Actuate CMS models...";
265
+ const updatedContent = content.trimEnd() + "\n" + getCmsSchemaFragment();
266
+
267
+ try {
268
+ await writeFile(schemaPath, updatedContent);
269
+ spinner.succeed("Actuate CMS models added to schema.");
270
+ } catch (err) {
271
+ spinner.fail("Failed to write schema file.");
272
+ logger.error(err instanceof Error ? err.message : String(err));
273
+ process.exitCode = 1;
274
+ return;
275
+ }
276
+
277
+ const execOpts: ExecSyncOptions = { stdio: "inherit", cwd: process.cwd() };
278
+
279
+ const genSpinner = ora("Running prisma generate...").start();
280
+ try {
281
+ genSpinner.stop();
282
+ execSync("npx prisma generate", execOpts);
283
+ logger.success("Prisma client generated.");
284
+ } catch {
285
+ logger.warn("prisma generate failed. You may need to set DATABASE_URL first.");
286
+ }
287
+
288
+ if (opts.migrate) {
289
+ const migSpinner = ora("Running prisma migrate dev...").start();
290
+ try {
291
+ migSpinner.stop();
292
+ execSync("npx prisma migrate dev --name actuate-cms-init", execOpts);
293
+ logger.success("Migration created and applied.");
294
+ } catch {
295
+ logger.warn("prisma migrate dev failed. Run it manually after setting DATABASE_URL.");
296
+ }
297
+ } else {
298
+ logger.info("Run `npx prisma migrate dev --name actuate-cms` to create the migration.");
299
+ }
300
+ });
301
+ }
@@ -0,0 +1,131 @@
1
+ import { Command } from "commander";
2
+ import { execSync } from "node:child_process";
3
+ import { readFile, access } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import ora from "ora";
6
+ import chalk from "chalk";
7
+ import { logger } from "../utils/logger.js";
8
+
9
+ const CMS_EXPECTED_MODELS = [
10
+ "User",
11
+ "Session",
12
+ "Document",
13
+ "Media",
14
+ "Version",
15
+ "Folder",
16
+ "Redirect",
17
+ "FormSubmission",
18
+ "AuditLog",
19
+ ];
20
+
21
+ const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
22
+
23
+ async function fileExists(filePath: string): Promise<boolean> {
24
+ try {
25
+ await access(filePath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export function registerDbStatusCommand(program: Command): void {
33
+ program
34
+ .command("db:status")
35
+ .description("Check which Actuate CMS models are present in your Prisma schema")
36
+ .option("--schema <path>", "Path to schema.prisma", "prisma/schema.prisma")
37
+ .action(async (opts: { schema: string }) => {
38
+ const schemaPath = resolve(process.cwd(), opts.schema);
39
+
40
+ if (!(await fileExists(schemaPath))) {
41
+ logger.error(`Schema file not found at ${schemaPath}`);
42
+ logger.info("Run `npx prisma init` first, or specify --schema <path>.");
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ const spinner = ora("Checking schema...").start();
48
+
49
+ let content: string;
50
+ try {
51
+ content = await readFile(schemaPath, "utf-8");
52
+ } catch (err) {
53
+ spinner.fail("Failed to read schema file.");
54
+ logger.error(err instanceof Error ? err.message : String(err));
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+
59
+ spinner.stop();
60
+
61
+ const hasMarker = content.includes(CMS_SCHEMA_MARKER);
62
+ const modelRegex = /^model\s+(\w+)\s*\{/gm;
63
+ const schemaModels = new Set<string>();
64
+ let match;
65
+ while ((match = modelRegex.exec(content)) !== null) {
66
+ schemaModels.add(match[1]!);
67
+ }
68
+
69
+ console.log();
70
+ console.log(chalk.bold(" Actuate CMS Model Status"));
71
+ console.log(chalk.dim(" ─────────────────────────────────"));
72
+ console.log();
73
+
74
+ let present = 0;
75
+ let missing = 0;
76
+
77
+ for (const model of CMS_EXPECTED_MODELS) {
78
+ if (schemaModels.has(model)) {
79
+ console.log(` ${chalk.green("✓")} ${model}`);
80
+ present++;
81
+ } else {
82
+ console.log(` ${chalk.red("✗")} ${model} ${chalk.dim("— missing")}`);
83
+ missing++;
84
+ }
85
+ }
86
+
87
+ console.log();
88
+ console.log(chalk.dim(" ─────────────────────────────────"));
89
+ console.log(` ${chalk.green(present)} present, ${missing > 0 ? chalk.red(missing) : chalk.green(missing)} missing`);
90
+
91
+ if (!hasMarker) {
92
+ console.log();
93
+ logger.warn("CMS schema marker not found. Models may have been added manually.");
94
+ logger.info("Run `actuate db:init` to add CMS models with proper markers.");
95
+ }
96
+
97
+ if (missing > 0) {
98
+ console.log();
99
+ logger.info("Run `actuate db:init` to add missing models.");
100
+ }
101
+
102
+ let dbConnected = false;
103
+ try {
104
+ spinner.start("Checking database connection...");
105
+ execSync("npx prisma db execute --stdin --schema " + JSON.stringify(schemaPath), {
106
+ input: "SELECT 1;",
107
+ stdio: ["pipe", "pipe", "pipe"],
108
+ cwd: process.cwd(),
109
+ });
110
+ spinner.succeed("Database connection OK.");
111
+ dbConnected = true;
112
+ } catch {
113
+ spinner.warn("Could not connect to database. Check DATABASE_URL.");
114
+ }
115
+
116
+ if (dbConnected && missing === 0) {
117
+ try {
118
+ spinner.start("Checking migration status...");
119
+ spinner.stop();
120
+ execSync("npx prisma migrate status", {
121
+ stdio: "inherit",
122
+ cwd: process.cwd(),
123
+ });
124
+ } catch {
125
+ logger.warn("Could not check migration status.");
126
+ }
127
+ }
128
+
129
+ console.log();
130
+ });
131
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ import { registerImportCommand } from "./commands/import.js";
7
7
  import { registerExportCommand } from "./commands/export.js";
8
8
  import { registerUpgradeCommand } from "./commands/upgrade.js";
9
9
  import { registerUpdateCheckCommand } from "./commands/update-check.js";
10
+ import { registerDbInitCommand } from "./commands/db-init.js";
11
+ import { registerDbStatusCommand } from "./commands/db-status.js";
10
12
 
11
13
  const program = new Command();
12
14
 
@@ -22,5 +24,7 @@ registerImportCommand(program);
22
24
  registerExportCommand(program);
23
25
  registerUpgradeCommand(program);
24
26
  registerUpdateCheckCommand(program);
27
+ registerDbInitCommand(program);
28
+ registerDbStatusCommand(program);
25
29
 
26
30
  program.parse();