@actuate-media/cli 0.1.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.
- package/.turbo/turbo-build.log +4 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +99 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +24 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +204 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +57 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/seed.d.ts +3 -0
- package/dist/commands/seed.d.ts.map +1 -0
- package/dist/commands/seed.js +343 -0
- package/dist/commands/seed.js.map +1 -0
- package/dist/commands/update-check.d.ts +3 -0
- package/dist/commands/update-check.d.ts.map +1 -0
- package/dist/commands/update-check.js +125 -0
- package/dist/commands/update-check.js.map +1 -0
- package/dist/commands/upgrade.d.ts +3 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +150 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +33 -0
- package/src/commands/export.ts +131 -0
- package/src/commands/generate.ts +28 -0
- package/src/commands/import.ts +243 -0
- package/src/commands/migrate.ts +62 -0
- package/src/commands/seed.ts +388 -0
- package/src/commands/update-check.ts +147 -0
- package/src/commands/upgrade.ts +173 -0
- package/src/index.ts +26 -0
- package/src/utils/logger.ts +26 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
|
|
8
|
+
async function confirm(question: string): Promise<boolean> {
|
|
9
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
+
const answer = await rl.question(`${question} (y/N) `);
|
|
11
|
+
rl.close();
|
|
12
|
+
return answer.trim().toLowerCase() === "y";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEMO_PAGES = [
|
|
16
|
+
{
|
|
17
|
+
title: "Home",
|
|
18
|
+
slug: "home",
|
|
19
|
+
content: "<h1>Welcome</h1><p>Your homepage content goes here.</p>",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: "About",
|
|
23
|
+
slug: "about",
|
|
24
|
+
content:
|
|
25
|
+
"<h1>About Us</h1><p>Learn more about our team and mission.</p>",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: "Contact",
|
|
29
|
+
slug: "contact",
|
|
30
|
+
content:
|
|
31
|
+
"<h1>Contact</h1><p>Get in touch with us via the form below.</p>",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: "Privacy Policy",
|
|
35
|
+
slug: "privacy-policy",
|
|
36
|
+
content:
|
|
37
|
+
"<h1>Privacy Policy</h1><p>Your privacy matters to us. Read our full policy.</p>",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
title: "Terms of Service",
|
|
41
|
+
slug: "terms",
|
|
42
|
+
content:
|
|
43
|
+
"<h1>Terms of Service</h1><p>Please read the following terms carefully.</p>",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const DEMO_POSTS = [
|
|
48
|
+
{
|
|
49
|
+
title: "Getting Started with Actuate CMS",
|
|
50
|
+
slug: "getting-started",
|
|
51
|
+
excerpt: "Learn how to set up and configure your new CMS.",
|
|
52
|
+
content:
|
|
53
|
+
"<h1>Getting Started</h1><p>Welcome to Actuate CMS. This guide walks you through initial setup.</p>",
|
|
54
|
+
status: "PUBLISHED",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: "Content Modeling Best Practices",
|
|
58
|
+
slug: "content-modeling",
|
|
59
|
+
excerpt: "Design your collections and fields for maximum flexibility.",
|
|
60
|
+
content:
|
|
61
|
+
"<h1>Content Modeling</h1><p>Effective content modeling is the foundation of a great CMS.</p>",
|
|
62
|
+
status: "PUBLISHED",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: "Working with Media",
|
|
66
|
+
slug: "working-with-media",
|
|
67
|
+
excerpt: "Upload, organize, and optimize your media assets.",
|
|
68
|
+
content:
|
|
69
|
+
"<h1>Working with Media</h1><p>Actuate CMS provides powerful media management.</p>",
|
|
70
|
+
status: "PUBLISHED",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: "SEO Optimization Tips",
|
|
74
|
+
slug: "seo-optimization",
|
|
75
|
+
excerpt: "Boost your search rankings with built-in SEO tools.",
|
|
76
|
+
content:
|
|
77
|
+
"<h1>SEO Optimization</h1><p>Follow these tips to improve your site visibility.</p>",
|
|
78
|
+
status: "PUBLISHED",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "Building Custom Plugins",
|
|
82
|
+
slug: "building-plugins",
|
|
83
|
+
excerpt: "Extend Actuate CMS with your own plugins.",
|
|
84
|
+
content:
|
|
85
|
+
"<h1>Building Plugins</h1><p>The plugin system lets you add custom functionality.</p>",
|
|
86
|
+
status: "PUBLISHED",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "API Reference Overview",
|
|
90
|
+
slug: "api-reference",
|
|
91
|
+
excerpt: "A comprehensive guide to the Actuate CMS REST API.",
|
|
92
|
+
content:
|
|
93
|
+
"<h1>API Reference</h1><p>Use the API to integrate your content anywhere.</p>",
|
|
94
|
+
status: "PUBLISHED",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
title: "Deployment Guide",
|
|
98
|
+
slug: "deployment-guide",
|
|
99
|
+
excerpt: "Deploy Actuate CMS to Vercel, AWS, or self-hosted.",
|
|
100
|
+
content:
|
|
101
|
+
"<h1>Deployment Guide</h1><p>Multiple deployment options for every use case.</p>",
|
|
102
|
+
status: "PUBLISHED",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
title: "Multi-language Content",
|
|
106
|
+
slug: "multi-language",
|
|
107
|
+
excerpt: "Set up localization and manage translated content.",
|
|
108
|
+
content:
|
|
109
|
+
"<h1>Multi-language Content</h1><p>Reach a global audience with localized content.</p>",
|
|
110
|
+
status: "PUBLISHED",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: "Webhooks and Integrations",
|
|
114
|
+
slug: "webhooks-integrations",
|
|
115
|
+
excerpt: "Connect Actuate CMS to external services with webhooks.",
|
|
116
|
+
content:
|
|
117
|
+
"<h1>Webhooks</h1><p>Automate workflows by connecting to third-party services.</p>",
|
|
118
|
+
status: "DRAFT",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
title: "Advanced Access Control",
|
|
122
|
+
slug: "access-control",
|
|
123
|
+
excerpt: "Fine-tune permissions with role-based access control.",
|
|
124
|
+
content:
|
|
125
|
+
"<h1>Access Control</h1><p>Protect your content with granular permissions.</p>",
|
|
126
|
+
status: "DRAFT",
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const DEMO_FORMS = [
|
|
131
|
+
{
|
|
132
|
+
title: "Contact Form",
|
|
133
|
+
slug: "contact-form",
|
|
134
|
+
fields: [
|
|
135
|
+
{ name: "name", type: "text", required: true },
|
|
136
|
+
{ name: "email", type: "email", required: true },
|
|
137
|
+
{ name: "message", type: "textarea", required: true },
|
|
138
|
+
],
|
|
139
|
+
submitLabel: "Send Message",
|
|
140
|
+
successMessage: "Thanks for reaching out! We'll get back to you soon.",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
title: "Newsletter Signup",
|
|
144
|
+
slug: "newsletter",
|
|
145
|
+
fields: [
|
|
146
|
+
{ name: "email", type: "email", required: true },
|
|
147
|
+
{ name: "firstName", type: "text", required: false },
|
|
148
|
+
],
|
|
149
|
+
submitLabel: "Subscribe",
|
|
150
|
+
successMessage: "You're subscribed! Check your inbox for confirmation.",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
title: "Feedback Form",
|
|
154
|
+
slug: "feedback",
|
|
155
|
+
fields: [
|
|
156
|
+
{ name: "name", type: "text", required: false },
|
|
157
|
+
{ name: "rating", type: "select", options: ["1", "2", "3", "4", "5"], required: true },
|
|
158
|
+
{ name: "comments", type: "textarea", required: false },
|
|
159
|
+
],
|
|
160
|
+
submitLabel: "Submit Feedback",
|
|
161
|
+
successMessage: "Thank you for your feedback!",
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const DEMO_USERS = [
|
|
166
|
+
{ email: "editor@example.com", name: "Demo Editor", role: "EDITOR" as const },
|
|
167
|
+
{ email: "author@example.com", name: "Demo Author", role: "AUTHOR" as const },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
interface SeedOptions {
|
|
171
|
+
demo?: boolean;
|
|
172
|
+
file?: string;
|
|
173
|
+
reset?: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function runSeed(options: SeedOptions): Promise<void> {
|
|
177
|
+
if (!options.demo && !options.file) {
|
|
178
|
+
logger.error("Specify --demo to seed demo content or --file <path> to seed from a JSON file.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const { getDB } = await import("@actuate-media/cms-core");
|
|
184
|
+
const db = getDB<any>();
|
|
185
|
+
|
|
186
|
+
if (options.reset) {
|
|
187
|
+
const yes = await confirm(
|
|
188
|
+
"This will delete ALL existing documents and versions. Continue?",
|
|
189
|
+
);
|
|
190
|
+
if (!yes) {
|
|
191
|
+
logger.warn("Seed cancelled.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const resetSpinner = ora("Clearing existing data…").start();
|
|
196
|
+
await db.version.deleteMany({});
|
|
197
|
+
await db.mediaUsage.deleteMany({});
|
|
198
|
+
await db.document.deleteMany({});
|
|
199
|
+
resetSpinner.succeed("Existing data cleared.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.demo) {
|
|
203
|
+
await seedDemoData(db);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (options.file) {
|
|
207
|
+
await seedFromFile(db, options.file);
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
211
|
+
logger.error(`Seed failed: ${message}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function seedDemoData(db: any): Promise<void> {
|
|
217
|
+
const spinner = ora("Seeding demo data…").start();
|
|
218
|
+
|
|
219
|
+
let adminUser = await db.user.findFirst({ where: { role: "ADMIN" } });
|
|
220
|
+
if (!adminUser) {
|
|
221
|
+
adminUser = await db.user.create({
|
|
222
|
+
data: {
|
|
223
|
+
email: "admin@actuatecms.dev",
|
|
224
|
+
name: "Admin",
|
|
225
|
+
role: "ADMIN",
|
|
226
|
+
isActive: true,
|
|
227
|
+
isApproved: true,
|
|
228
|
+
emailVerified: true,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const userId = adminUser.id;
|
|
233
|
+
|
|
234
|
+
let pagesCreated = 0;
|
|
235
|
+
for (const page of DEMO_PAGES) {
|
|
236
|
+
await db.document.create({
|
|
237
|
+
data: {
|
|
238
|
+
collection: "pages",
|
|
239
|
+
data: page,
|
|
240
|
+
status: "PUBLISHED",
|
|
241
|
+
publishedAt: new Date(),
|
|
242
|
+
createdById: userId,
|
|
243
|
+
updatedById: userId,
|
|
244
|
+
plainText: `${page.title} ${page.slug}`,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
pagesCreated++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let postsCreated = 0;
|
|
251
|
+
for (const post of DEMO_POSTS) {
|
|
252
|
+
await db.document.create({
|
|
253
|
+
data: {
|
|
254
|
+
collection: "posts",
|
|
255
|
+
data: post,
|
|
256
|
+
status: post.status,
|
|
257
|
+
publishedAt: post.status === "PUBLISHED" ? new Date() : null,
|
|
258
|
+
createdById: userId,
|
|
259
|
+
updatedById: userId,
|
|
260
|
+
plainText: `${post.title} ${post.excerpt}`,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
postsCreated++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let formsCreated = 0;
|
|
267
|
+
for (const form of DEMO_FORMS) {
|
|
268
|
+
await db.document.create({
|
|
269
|
+
data: {
|
|
270
|
+
collection: "forms",
|
|
271
|
+
data: form,
|
|
272
|
+
status: "PUBLISHED",
|
|
273
|
+
publishedAt: new Date(),
|
|
274
|
+
createdById: userId,
|
|
275
|
+
updatedById: userId,
|
|
276
|
+
plainText: `${form.title} ${form.slug}`,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
formsCreated++;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let usersCreated = 0;
|
|
283
|
+
for (const user of DEMO_USERS) {
|
|
284
|
+
const exists = await db.user.findFirst({ where: { email: user.email } });
|
|
285
|
+
if (!exists) {
|
|
286
|
+
await db.user.create({
|
|
287
|
+
data: {
|
|
288
|
+
email: user.email,
|
|
289
|
+
name: user.name,
|
|
290
|
+
role: user.role,
|
|
291
|
+
isActive: true,
|
|
292
|
+
isApproved: true,
|
|
293
|
+
emailVerified: true,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
usersCreated++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
spinner.succeed("Demo data seeded successfully.");
|
|
301
|
+
logger.info(` Pages: ${pagesCreated}`);
|
|
302
|
+
logger.info(` Posts: ${postsCreated}`);
|
|
303
|
+
logger.info(` Forms: ${formsCreated}`);
|
|
304
|
+
logger.info(` Users: ${usersCreated} (+ existing admin)`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function seedFromFile(db: any, filePath: string): Promise<void> {
|
|
308
|
+
if (!existsSync(filePath)) {
|
|
309
|
+
logger.error(`File not found: ${filePath}`);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const spinner = ora(`Seeding from ${filePath}…`).start();
|
|
314
|
+
|
|
315
|
+
const raw = await readFile(filePath, "utf-8");
|
|
316
|
+
let seedData: any;
|
|
317
|
+
try {
|
|
318
|
+
seedData = JSON.parse(raw);
|
|
319
|
+
} catch {
|
|
320
|
+
spinner.fail("Invalid JSON file.");
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!Array.isArray(seedData) && typeof seedData !== "object") {
|
|
325
|
+
spinner.fail("Seed file must contain a JSON array or an object with collection keys.");
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let adminUser = await db.user.findFirst({ where: { role: "ADMIN" } });
|
|
330
|
+
if (!adminUser) {
|
|
331
|
+
adminUser = await db.user.create({
|
|
332
|
+
data: {
|
|
333
|
+
email: "admin@actuatecms.dev",
|
|
334
|
+
name: "Admin",
|
|
335
|
+
role: "ADMIN",
|
|
336
|
+
isActive: true,
|
|
337
|
+
isApproved: true,
|
|
338
|
+
emailVerified: true,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const userId = adminUser.id;
|
|
343
|
+
|
|
344
|
+
let count = 0;
|
|
345
|
+
|
|
346
|
+
if (Array.isArray(seedData)) {
|
|
347
|
+
for (const doc of seedData) {
|
|
348
|
+
await db.document.create({
|
|
349
|
+
data: {
|
|
350
|
+
collection: doc.collection ?? "imported",
|
|
351
|
+
data: doc.data ?? doc,
|
|
352
|
+
status: doc.status ?? "DRAFT",
|
|
353
|
+
createdById: userId,
|
|
354
|
+
updatedById: userId,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
count++;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
for (const [collection, docs] of Object.entries(seedData)) {
|
|
361
|
+
if (!Array.isArray(docs)) continue;
|
|
362
|
+
for (const doc of docs as any[]) {
|
|
363
|
+
await db.document.create({
|
|
364
|
+
data: {
|
|
365
|
+
collection,
|
|
366
|
+
data: doc.data ?? doc,
|
|
367
|
+
status: doc.status ?? "DRAFT",
|
|
368
|
+
createdById: userId,
|
|
369
|
+
updatedById: userId,
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
count++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
spinner.succeed(`Seeded ${count} documents from ${filePath}.`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function registerSeedCommand(program: Command): void {
|
|
381
|
+
program
|
|
382
|
+
.command("seed")
|
|
383
|
+
.description("Seed the database with demo or custom data")
|
|
384
|
+
.option("--demo", "Seed demo content (pages, posts, forms, users)")
|
|
385
|
+
.option("--file <path>", "Seed from a JSON file")
|
|
386
|
+
.option("--reset", "Clear existing data before seeding")
|
|
387
|
+
.action(runSeed);
|
|
388
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
|
|
8
|
+
const UPDATE_SERVER_URL = "https://updates.actuatecms.com/api/versions";
|
|
9
|
+
|
|
10
|
+
function compareVersions(a: string, b: string): number {
|
|
11
|
+
const pa = a.replace(/^[^0-9]*/, "").split(".").map(Number);
|
|
12
|
+
const pb = b.replace(/^[^0-9]*/, "").split(".").map(Number);
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
15
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PackageVersions {
|
|
21
|
+
[pkg: string]: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getLocalVersions(): Promise<PackageVersions> {
|
|
25
|
+
const pkgPath = resolve(process.cwd(), "package.json");
|
|
26
|
+
if (!existsSync(pkgPath)) {
|
|
27
|
+
throw new Error("No package.json found in the current directory.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const raw = await readFile(pkgPath, "utf-8");
|
|
31
|
+
const pkg = JSON.parse(raw);
|
|
32
|
+
const versions: PackageVersions = {};
|
|
33
|
+
|
|
34
|
+
for (const section of ["dependencies", "devDependencies"] as const) {
|
|
35
|
+
const deps = pkg[section];
|
|
36
|
+
if (!deps) continue;
|
|
37
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
38
|
+
if (name.startsWith("@actuate-media/") && typeof version === "string") {
|
|
39
|
+
versions[name] = version.replace(/^[\^~]/, "").replace("workspace:", "");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return versions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchLatestVersions(
|
|
48
|
+
packages: string[],
|
|
49
|
+
): Promise<PackageVersions> {
|
|
50
|
+
const versions: PackageVersions = {};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(UPDATE_SERVER_URL);
|
|
54
|
+
if (res.ok) {
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
for (const pkg of packages) {
|
|
57
|
+
if (data[pkg]) {
|
|
58
|
+
versions[pkg] = data[pkg];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (Object.keys(versions).length === packages.length) {
|
|
62
|
+
return versions;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Fall through to npm registry
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const pkg of packages) {
|
|
70
|
+
if (versions[pkg]) continue;
|
|
71
|
+
try {
|
|
72
|
+
const npmUrl = `https://registry.npmjs.org/${pkg}/latest`;
|
|
73
|
+
const res = await fetch(npmUrl);
|
|
74
|
+
if (res.ok) {
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
versions[pkg] = data.version;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Skip packages we can't look up
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return versions;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function runUpdateCheck(): Promise<void> {
|
|
87
|
+
const spinner = ora("Checking for updates…").start();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const local = await getLocalVersions();
|
|
91
|
+
const packages = Object.keys(local);
|
|
92
|
+
|
|
93
|
+
if (packages.length === 0) {
|
|
94
|
+
spinner.warn("No @actuate-media/* packages found in package.json.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const latest = await fetchLatestVersions(packages);
|
|
99
|
+
spinner.stop();
|
|
100
|
+
|
|
101
|
+
let hasUpdates = false;
|
|
102
|
+
|
|
103
|
+
console.log("\n " + "Package".padEnd(30) + "Current".padEnd(14) + "Latest".padEnd(14) + "Status");
|
|
104
|
+
console.log(" " + "-".repeat(68));
|
|
105
|
+
|
|
106
|
+
for (const pkg of packages) {
|
|
107
|
+
const current = local[pkg];
|
|
108
|
+
const remote = latest[pkg] ?? "unknown";
|
|
109
|
+
let status: string;
|
|
110
|
+
|
|
111
|
+
if (remote === "unknown") {
|
|
112
|
+
status = "?";
|
|
113
|
+
} else if (compareVersions(current!, remote) < 0) {
|
|
114
|
+
status = "Update available";
|
|
115
|
+
hasUpdates = true;
|
|
116
|
+
} else {
|
|
117
|
+
status = "Up to date";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(
|
|
121
|
+
` ${pkg.padEnd(30)}${(current ?? '').padEnd(14)}${remote.padEnd(14)}${status}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log();
|
|
126
|
+
|
|
127
|
+
if (hasUpdates) {
|
|
128
|
+
logger.warn('Updates available! Run "actuate upgrade" to update.');
|
|
129
|
+
} else {
|
|
130
|
+
logger.success("All packages are up to date!");
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
spinner.fail("Update check failed.");
|
|
134
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
135
|
+
logger.error(message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function registerUpdateCheckCommand(program: Command): void {
|
|
141
|
+
program
|
|
142
|
+
.command("update-check")
|
|
143
|
+
.description(
|
|
144
|
+
"Check for available Actuate CMS package updates",
|
|
145
|
+
)
|
|
146
|
+
.action(runUpdateCheck);
|
|
147
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
|
|
9
|
+
const UPDATE_SERVER_URL = "https://updates.actuatecms.com/api/versions";
|
|
10
|
+
|
|
11
|
+
function compareVersions(a: string, b: string): number {
|
|
12
|
+
const pa = a.replace(/^[^0-9]*/, "").split(".").map(Number);
|
|
13
|
+
const pb = b.replace(/^[^0-9]*/, "").split(".").map(Number);
|
|
14
|
+
for (let i = 0; i < 3; i++) {
|
|
15
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
16
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function confirm(question: string): Promise<boolean> {
|
|
22
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
+
try {
|
|
24
|
+
const answer = await rl.question(`${question} (y/N) `);
|
|
25
|
+
return answer.trim().toLowerCase() === "y";
|
|
26
|
+
} finally {
|
|
27
|
+
rl.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchLatestVersions(
|
|
32
|
+
packages: string[],
|
|
33
|
+
): Promise<Record<string, string>> {
|
|
34
|
+
const versions: Record<string, string> = {};
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(UPDATE_SERVER_URL);
|
|
38
|
+
if (res.ok) {
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
for (const pkg of packages) {
|
|
41
|
+
if (data[pkg]) versions[pkg] = data[pkg];
|
|
42
|
+
}
|
|
43
|
+
if (Object.keys(versions).length === packages.length) return versions;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Fall through to npm registry
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const pkg of packages) {
|
|
50
|
+
if (versions[pkg]) continue;
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`);
|
|
53
|
+
if (res.ok) {
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
versions[pkg] = data.version;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Skip
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return versions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface UpgradeOptions {
|
|
66
|
+
latest?: boolean;
|
|
67
|
+
version?: string;
|
|
68
|
+
dryRun?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runUpgrade(options: UpgradeOptions): Promise<void> {
|
|
72
|
+
const pkgPath = resolve(process.cwd(), "package.json");
|
|
73
|
+
if (!existsSync(pkgPath)) {
|
|
74
|
+
logger.error("No package.json found in the current directory.");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const raw = await readFile(pkgPath, "utf-8");
|
|
79
|
+
const pkg = JSON.parse(raw);
|
|
80
|
+
|
|
81
|
+
const actuatePackages: { name: string; current: string; section: string }[] = [];
|
|
82
|
+
|
|
83
|
+
for (const section of ["dependencies", "devDependencies"] as const) {
|
|
84
|
+
const deps = pkg[section];
|
|
85
|
+
if (!deps) continue;
|
|
86
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
87
|
+
if (name.startsWith("@actuate-media/") && typeof version === "string") {
|
|
88
|
+
if (version.startsWith("workspace:")) continue;
|
|
89
|
+
actuatePackages.push({
|
|
90
|
+
name,
|
|
91
|
+
current: version.replace(/^[\^~]/, ""),
|
|
92
|
+
section,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (actuatePackages.length === 0) {
|
|
99
|
+
logger.warn("No @actuate-media/* packages found in package.json (excluding workspace: references).");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const spinner = ora("Resolving target versions…").start();
|
|
104
|
+
|
|
105
|
+
let targetVersions: Record<string, string>;
|
|
106
|
+
|
|
107
|
+
if (options.version) {
|
|
108
|
+
targetVersions = {};
|
|
109
|
+
for (const p of actuatePackages) {
|
|
110
|
+
targetVersions[p.name] = options.version;
|
|
111
|
+
}
|
|
112
|
+
spinner.succeed(`Target version: ${options.version}`);
|
|
113
|
+
} else {
|
|
114
|
+
targetVersions = await fetchLatestVersions(
|
|
115
|
+
actuatePackages.map((p) => p.name),
|
|
116
|
+
);
|
|
117
|
+
spinner.succeed("Resolved latest versions.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const upgrades: { name: string; from: string; to: string; section: string }[] = [];
|
|
121
|
+
|
|
122
|
+
for (const p of actuatePackages) {
|
|
123
|
+
const target = targetVersions[p.name];
|
|
124
|
+
if (!target) continue;
|
|
125
|
+
if (compareVersions(p.current, target) < 0) {
|
|
126
|
+
upgrades.push({ name: p.name, from: p.current, to: target, section: p.section });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (upgrades.length === 0) {
|
|
131
|
+
logger.success("All @actuate-media/* packages are already up to date.");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log("\n Proposed upgrades:\n");
|
|
136
|
+
console.log(" " + "Package".padEnd(30) + "From".padEnd(14) + "To");
|
|
137
|
+
console.log(" " + "-".repeat(54));
|
|
138
|
+
for (const u of upgrades) {
|
|
139
|
+
console.log(` ${u.name.padEnd(30)}${u.from.padEnd(14)}${u.to}`);
|
|
140
|
+
}
|
|
141
|
+
console.log();
|
|
142
|
+
|
|
143
|
+
if (options.dryRun) {
|
|
144
|
+
logger.info("Dry run — no changes were made.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const proceed = await confirm("Apply these upgrades to package.json?");
|
|
149
|
+
if (!proceed) {
|
|
150
|
+
logger.warn("Upgrade cancelled.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const u of upgrades) {
|
|
155
|
+
const prefix = pkg[u.section][u.name]?.match(/^[\^~]/)?.[0] ?? "^";
|
|
156
|
+
pkg[u.section][u.name] = `${prefix}${u.to}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
160
|
+
|
|
161
|
+
logger.success(`Updated ${upgrades.length} package(s) in package.json.`);
|
|
162
|
+
logger.info('Run "pnpm install" to apply the upgrade.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function registerUpgradeCommand(program: Command): void {
|
|
166
|
+
program
|
|
167
|
+
.command("upgrade")
|
|
168
|
+
.description("Upgrade @actuate-media/* packages to the latest or a specific version")
|
|
169
|
+
.option("--latest", "Upgrade to the latest version (default)", true)
|
|
170
|
+
.option("--version <ver>", "Upgrade to a specific version")
|
|
171
|
+
.option("--dry-run", "Show proposed changes without modifying files")
|
|
172
|
+
.action(runUpgrade);
|
|
173
|
+
}
|