@vexblocks/cli 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1215 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import pc6 from "picocolors";
6
+
7
+ // src/commands/init.ts
8
+ import path4 from "path";
9
+ import fs4 from "fs-extra";
10
+ import pc2 from "picocolors";
11
+ import ora from "ora";
12
+ import { confirm, input, select } from "@inquirer/prompts";
13
+
14
+ // src/utils/logger.ts
15
+ import pc from "picocolors";
16
+ var logger = {
17
+ info: (message) => {
18
+ console.log(pc.blue("\u2139"), message);
19
+ },
20
+ success: (message) => {
21
+ console.log(pc.green("\u2714"), message);
22
+ },
23
+ warn: (message) => {
24
+ console.log(pc.yellow("\u26A0"), message);
25
+ },
26
+ error: (message) => {
27
+ console.log(pc.red("\u2716"), message);
28
+ },
29
+ log: (message) => {
30
+ console.log(message);
31
+ },
32
+ break: () => {
33
+ console.log();
34
+ },
35
+ title: (message) => {
36
+ console.log();
37
+ console.log(pc.bold(pc.cyan(message)));
38
+ console.log();
39
+ },
40
+ step: (step, total, message) => {
41
+ console.log(pc.dim(`[${step}/${total}]`), message);
42
+ },
43
+ list: (items) => {
44
+ for (const item of items) {
45
+ console.log(pc.dim(" \u2022"), item);
46
+ }
47
+ },
48
+ box: (title, content) => {
49
+ const maxLength = Math.max(title.length, ...content.map((c) => c.length));
50
+ const border = "\u2500".repeat(maxLength + 4);
51
+ console.log();
52
+ console.log(pc.dim(`\u250C${border}\u2510`));
53
+ console.log(pc.dim("\u2502"), pc.bold(title.padEnd(maxLength + 2)), pc.dim("\u2502"));
54
+ console.log(pc.dim(`\u251C${border}\u2524`));
55
+ for (const line of content) {
56
+ console.log(pc.dim("\u2502"), line.padEnd(maxLength + 2), pc.dim("\u2502"));
57
+ }
58
+ console.log(pc.dim(`\u2514${border}\u2518`));
59
+ console.log();
60
+ },
61
+ code: (code, language) => {
62
+ console.log();
63
+ console.log(pc.dim(`\`\`\`${language || ""}`));
64
+ console.log(code);
65
+ console.log(pc.dim("```"));
66
+ console.log();
67
+ }
68
+ };
69
+
70
+ // src/utils/fs.ts
71
+ import fs from "fs-extra";
72
+ import path from "path";
73
+ import crypto from "crypto";
74
+ async function isTurborepoProject(cwd) {
75
+ const turboJsonPath = path.join(cwd, "turbo.json");
76
+ return fs.pathExists(turboJsonPath);
77
+ }
78
+ async function getPackageManager(cwd) {
79
+ if (await fs.pathExists(path.join(cwd, "pnpm-lock.yaml"))) {
80
+ return "pnpm";
81
+ }
82
+ if (await fs.pathExists(path.join(cwd, "yarn.lock"))) {
83
+ return "yarn";
84
+ }
85
+ if (await fs.pathExists(path.join(cwd, "bun.lockb"))) {
86
+ return "bun";
87
+ }
88
+ return "npm";
89
+ }
90
+ async function createBackup(filePath) {
91
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
92
+ const backupPath = `${filePath}.backup-${timestamp}`;
93
+ if (await fs.pathExists(filePath)) {
94
+ await fs.copy(filePath, backupPath);
95
+ }
96
+ return backupPath;
97
+ }
98
+
99
+ // src/utils/manifest.ts
100
+ import fs2 from "fs-extra";
101
+ import path2 from "path";
102
+
103
+ // src/utils/constants.ts
104
+ var GITHUB_REPO = "vexblocks/vexblocks";
105
+ var GITHUB_BRANCH = "main";
106
+ var GITHUB_RAW_URL = `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}`;
107
+ var MANIFEST_FILE = "vexblocks.json";
108
+ var PACKAGE_PATHS = {
109
+ cms: "apps/cms",
110
+ backend: "packages/backend",
111
+ shared: "packages/cms-shared",
112
+ types: "packages/type-generator"
113
+ };
114
+ var PACKAGE_NAMES = {
115
+ cms: "CMS Dashboard",
116
+ backend: "Convex Backend",
117
+ shared: "Shared Utilities",
118
+ types: "Type Generator"
119
+ };
120
+ var MANAGED_PACKAGES = ["cms", "shared", "types"];
121
+ var PACKAGE_DEPENDENCIES = {
122
+ cms: ["backend", "shared"],
123
+ backend: [],
124
+ shared: [],
125
+ types: ["backend"]
126
+ };
127
+ var REQUIRED_ENV_VARS = {
128
+ cms: ["CONVEX_DEPLOYMENT", "NEXT_PUBLIC_CONVEX_URL", "SITE_URL"],
129
+ backend: ["CONVEX_DEPLOYMENT", "NEXT_PUBLIC_CONVEX_URL", "SITE_URL"],
130
+ shared: [],
131
+ types: ["CONVEX_URL"]
132
+ };
133
+ var OPTIONAL_ENV_VARS = {
134
+ backend: [
135
+ {
136
+ name: "CLOUDFLARE_ACCOUNT_ID",
137
+ description: "Required for media library (Cloudflare Images)"
138
+ },
139
+ {
140
+ name: "CLOUDFLARE_SECRET_TOKEN",
141
+ description: "Required for media library (Cloudflare Images)"
142
+ },
143
+ {
144
+ name: "REVALIDATE_SECRET",
145
+ description: "Required for ISR revalidation webhooks"
146
+ },
147
+ {
148
+ name: "FRONTEND_URL",
149
+ description: "Your frontend URL for revalidation"
150
+ }
151
+ ]
152
+ };
153
+
154
+ // src/utils/manifest.ts
155
+ async function readManifest(cwd) {
156
+ const manifestPath = path2.join(cwd, MANIFEST_FILE);
157
+ try {
158
+ if (await fs2.pathExists(manifestPath)) {
159
+ return await fs2.readJson(manifestPath);
160
+ }
161
+ return null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+ async function writeManifest(cwd, manifest) {
167
+ const manifestPath = path2.join(cwd, MANIFEST_FILE);
168
+ await fs2.writeJson(manifestPath, manifest, { spaces: 2 });
169
+ }
170
+ function createManifest(version) {
171
+ return {
172
+ $schema: "https://vexblocks.dev/schema/vexblocks.json",
173
+ version,
174
+ packages: {}
175
+ };
176
+ }
177
+
178
+ // src/utils/github.ts
179
+ import fs3 from "fs-extra";
180
+ import path3 from "path";
181
+ async function fetchRemoteManifest() {
182
+ const url = `${GITHUB_RAW_URL}/templates/manifest.json`;
183
+ const response = await fetch(url);
184
+ if (!response.ok) {
185
+ throw new Error(`Failed to fetch manifest: ${response.statusText}`);
186
+ }
187
+ return response.json();
188
+ }
189
+ async function fetchFile(filePath) {
190
+ const url = `${GITHUB_RAW_URL}/${filePath}`;
191
+ const response = await fetch(url);
192
+ if (!response.ok) {
193
+ throw new Error(`Failed to fetch file ${filePath}: ${response.statusText}`);
194
+ }
195
+ return response.text();
196
+ }
197
+ async function downloadFile(remotePath, localPath) {
198
+ const url = `${GITHUB_RAW_URL}/${remotePath}`;
199
+ const response = await fetch(url);
200
+ if (!response.ok) {
201
+ throw new Error(`Failed to download ${remotePath}: ${response.statusText}`);
202
+ }
203
+ await fs3.ensureDir(path3.dirname(localPath));
204
+ const content = await response.text();
205
+ await fs3.writeFile(localPath, content, "utf-8");
206
+ }
207
+ async function downloadAndExtractPackage(packagePath, targetDir, onProgress) {
208
+ const treeUrl = `https://api.github.com/repos/${GITHUB_REPO}/git/trees/${GITHUB_BRANCH}?recursive=1`;
209
+ const response = await fetch(treeUrl, {
210
+ headers: {
211
+ Accept: "application/vnd.github.v3+json",
212
+ "User-Agent": "vexblocks-cli"
213
+ }
214
+ });
215
+ if (!response.ok) {
216
+ throw new Error(`Failed to fetch repository tree: ${response.statusText}`);
217
+ }
218
+ const data = await response.json();
219
+ const files = data.tree.filter(
220
+ (item) => item.type === "blob" && item.path.startsWith(`${packagePath}/`)
221
+ );
222
+ for (const file of files) {
223
+ const relativePath = file.path.slice(packagePath.length + 1);
224
+ const localPath = path3.join(targetDir, relativePath);
225
+ onProgress?.(relativePath);
226
+ await downloadFile(file.path, localPath);
227
+ }
228
+ }
229
+ async function getPackageFiles(packagePath) {
230
+ const treeUrl = `https://api.github.com/repos/${GITHUB_REPO}/git/trees/${GITHUB_BRANCH}?recursive=1`;
231
+ const response = await fetch(treeUrl, {
232
+ headers: {
233
+ Accept: "application/vnd.github.v3+json",
234
+ "User-Agent": "vexblocks-cli"
235
+ }
236
+ });
237
+ if (!response.ok) {
238
+ throw new Error(`Failed to fetch repository tree: ${response.statusText}`);
239
+ }
240
+ const data = await response.json();
241
+ return data.tree.filter(
242
+ (item) => item.type === "blob" && item.path.startsWith(`${packagePath}/`)
243
+ ).map((item) => item.path.slice(packagePath.length + 1));
244
+ }
245
+ async function getLatestVersion() {
246
+ const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
247
+ try {
248
+ const response = await fetch(url, {
249
+ headers: {
250
+ Accept: "application/vnd.github.v3+json",
251
+ "User-Agent": "vexblocks-cli"
252
+ }
253
+ });
254
+ if (response.ok) {
255
+ const data = await response.json();
256
+ return data.tag_name.replace(/^v/, "");
257
+ }
258
+ } catch {
259
+ }
260
+ try {
261
+ const manifest = await fetchRemoteManifest();
262
+ return manifest.version;
263
+ } catch {
264
+ return "1.0.0";
265
+ }
266
+ }
267
+ async function getChangelog(fromVersion, toVersion) {
268
+ const manifest = await fetchRemoteManifest();
269
+ const changelog = {};
270
+ const versions = Object.keys(manifest.changelog).sort((a, b) => {
271
+ const [aMajor, aMinor, aPatch] = a.split(".").map(Number);
272
+ const [bMajor, bMinor, bPatch] = b.split(".").map(Number);
273
+ if (aMajor !== bMajor) return aMajor - bMajor;
274
+ if (aMinor !== bMinor) return aMinor - bMinor;
275
+ return aPatch - bPatch;
276
+ });
277
+ const fromIndex = versions.indexOf(fromVersion);
278
+ const toIndex = versions.indexOf(toVersion);
279
+ if (fromIndex === -1 || toIndex === -1) {
280
+ return changelog;
281
+ }
282
+ for (let i = fromIndex + 1; i <= toIndex; i++) {
283
+ const version = versions[i];
284
+ if (manifest.changelog[version]) {
285
+ changelog[version] = manifest.changelog[version];
286
+ }
287
+ }
288
+ return changelog;
289
+ }
290
+ function compareVersions(a, b) {
291
+ const [aMajor, aMinor, aPatch] = a.split(".").map(Number);
292
+ const [bMajor, bMinor, bPatch] = b.split(".").map(Number);
293
+ if (aMajor !== bMajor) return aMajor < bMajor ? -1 : 1;
294
+ if (aMinor !== bMinor) return aMinor < bMinor ? -1 : 1;
295
+ if (aPatch !== bPatch) return aPatch < bPatch ? -1 : 1;
296
+ return 0;
297
+ }
298
+
299
+ // src/commands/init.ts
300
+ async function initCommand(options) {
301
+ const cwd = options.cwd ? path4.resolve(options.cwd) : process.cwd();
302
+ logger.title("\u{1F680} VexBlocks CMS Setup");
303
+ const manifestPath = path4.join(cwd, MANIFEST_FILE);
304
+ if (await fs4.pathExists(manifestPath)) {
305
+ logger.warn("VexBlocks is already initialized in this directory.");
306
+ const continueAnyway = options.yes || await confirm({
307
+ message: "Do you want to reinitialize?",
308
+ default: false
309
+ });
310
+ if (!continueAnyway) {
311
+ logger.info("Run `vexblocks add <package>` to add packages.");
312
+ return;
313
+ }
314
+ }
315
+ const isTurbo = await isTurborepoProject(cwd);
316
+ let projectType;
317
+ if (isTurbo) {
318
+ logger.success("Detected existing Turborepo project");
319
+ projectType = "existing";
320
+ } else {
321
+ projectType = await select({
322
+ message: "What would you like to do?",
323
+ choices: [
324
+ {
325
+ name: "Create a new Turborepo project with VexBlocks",
326
+ value: "new"
327
+ },
328
+ {
329
+ name: "Add VexBlocks to this directory (I'll set up Turborepo manually)",
330
+ value: "existing"
331
+ }
332
+ ]
333
+ });
334
+ }
335
+ if (projectType === "new") {
336
+ await createNewProject(cwd, options);
337
+ } else {
338
+ await initializeExisting(cwd, options);
339
+ }
340
+ }
341
+ async function createNewProject(cwd, options) {
342
+ const projectName = await input({
343
+ message: "Project name:",
344
+ default: path4.basename(cwd),
345
+ validate: (value) => {
346
+ if (!value) return "Project name is required";
347
+ if (!/^[a-z0-9-_]+$/.test(value)) {
348
+ return "Project name can only contain lowercase letters, numbers, hyphens, and underscores";
349
+ }
350
+ return true;
351
+ }
352
+ });
353
+ const projectDir = path4.join(cwd, projectName);
354
+ if (await fs4.pathExists(projectDir)) {
355
+ const overwrite = await confirm({
356
+ message: `Directory ${projectName} already exists. Overwrite?`,
357
+ default: false
358
+ });
359
+ if (!overwrite) {
360
+ logger.info("Aborted.");
361
+ return;
362
+ }
363
+ await fs4.remove(projectDir);
364
+ }
365
+ const spinner = ora("Creating project structure...").start();
366
+ try {
367
+ await fs4.ensureDir(projectDir);
368
+ await fs4.ensureDir(path4.join(projectDir, "apps"));
369
+ await fs4.ensureDir(path4.join(projectDir, "packages"));
370
+ const packageJson = {
371
+ name: projectName,
372
+ version: "0.1.0",
373
+ private: true,
374
+ packageManager: "pnpm@9.15.0",
375
+ workspaces: ["apps/*", "packages/*"],
376
+ scripts: {
377
+ dev: "turbo run dev",
378
+ build: "turbo run build",
379
+ lint: "turbo run lint",
380
+ "generate-types": "pnpm --filter @repo/type-generator generate"
381
+ },
382
+ devDependencies: {
383
+ turbo: "^2.6.1"
384
+ }
385
+ };
386
+ await fs4.writeJson(path4.join(projectDir, "package.json"), packageJson, {
387
+ spaces: 2
388
+ });
389
+ const turboJson = {
390
+ $schema: "https://turbo.build/schema.json",
391
+ globalDependencies: ["**/.env.*local"],
392
+ globalEnv: [
393
+ "CONVEX_DEPLOYMENT",
394
+ "NEXT_PUBLIC_CONVEX_URL",
395
+ "NEXT_PUBLIC_CONVEX_SITE_URL",
396
+ "SITE_URL",
397
+ "CLOUDFLARE_ACCOUNT_ID",
398
+ "CLOUDFLARE_SECRET_TOKEN"
399
+ ],
400
+ ui: "tui",
401
+ tasks: {
402
+ dev: {
403
+ cache: false,
404
+ persistent: true
405
+ },
406
+ build: {
407
+ dependsOn: ["^build"],
408
+ inputs: ["$TURBO_DEFAULT$", ".env*"],
409
+ outputs: [".next/**", "dist/**"]
410
+ },
411
+ lint: {
412
+ outputs: []
413
+ }
414
+ }
415
+ };
416
+ await fs4.writeJson(path4.join(projectDir, "turbo.json"), turboJson, {
417
+ spaces: 2
418
+ });
419
+ const pnpmWorkspace = `packages:
420
+ - "apps/*"
421
+ - "packages/*"
422
+ `;
423
+ await fs4.writeFile(
424
+ path4.join(projectDir, "pnpm-workspace.yaml"),
425
+ pnpmWorkspace
426
+ );
427
+ const gitignore = `# Dependencies
428
+ node_modules
429
+ .pnpm-store
430
+
431
+ # Build outputs
432
+ dist
433
+ .next
434
+ .turbo
435
+
436
+ # Environment files
437
+ .env
438
+ .env.local
439
+ .env.*.local
440
+
441
+ # IDE
442
+ .idea
443
+ .vscode
444
+ *.swp
445
+ *.swo
446
+
447
+ # OS
448
+ .DS_Store
449
+ Thumbs.db
450
+
451
+ # Logs
452
+ *.log
453
+ npm-debug.log*
454
+ pnpm-debug.log*
455
+
456
+ # Convex
457
+ .convex
458
+ `;
459
+ await fs4.writeFile(path4.join(projectDir, ".gitignore"), gitignore);
460
+ const envExample = `# Convex
461
+ CONVEX_DEPLOYMENT=
462
+ NEXT_PUBLIC_CONVEX_URL=
463
+
464
+ # Better Auth
465
+ SITE_URL=http://localhost:3001
466
+
467
+ # Cloudflare Images (optional, for media library)
468
+ CLOUDFLARE_ACCOUNT_ID=
469
+ CLOUDFLARE_SECRET_TOKEN=
470
+
471
+ # Revalidation (optional, for ISR)
472
+ REVALIDATE_SECRET=
473
+ FRONTEND_URL=http://localhost:3000
474
+ `;
475
+ await fs4.writeFile(path4.join(projectDir, ".env.example"), envExample);
476
+ const version = await getLatestVersion();
477
+ const manifest = createManifest(version);
478
+ await writeManifest(projectDir, manifest);
479
+ spinner.succeed("Project structure created");
480
+ logger.break();
481
+ logger.box("\u2705 Project created successfully!", [
482
+ `Directory: ${pc2.cyan(projectDir)}`,
483
+ "",
484
+ pc2.bold("Next steps:"),
485
+ `1. ${pc2.cyan(`cd ${projectName}`)}`,
486
+ `2. ${pc2.cyan("pnpm install")}`,
487
+ `3. ${pc2.cyan("npx vexblocks add all")}`,
488
+ "4. Set up your Convex project",
489
+ `5. ${pc2.cyan("pnpm dev")}`
490
+ ]);
491
+ const addPackages = options.yes || await confirm({
492
+ message: "Would you like to add VexBlocks packages now?",
493
+ default: true
494
+ });
495
+ if (addPackages) {
496
+ logger.break();
497
+ logger.info(`Run: ${pc2.cyan("npx vexblocks add all")}`);
498
+ logger.info("After navigating to your project directory.");
499
+ }
500
+ } catch (error) {
501
+ spinner.fail("Failed to create project");
502
+ throw error;
503
+ }
504
+ }
505
+ async function initializeExisting(cwd, _options) {
506
+ const spinner = ora("Initializing VexBlocks...").start();
507
+ try {
508
+ const hasTurbo = await isTurborepoProject(cwd);
509
+ if (!hasTurbo) {
510
+ spinner.warn("No turbo.json found");
511
+ logger.warn(
512
+ "VexBlocks requires Turborepo. Please set up Turborepo first."
513
+ );
514
+ logger.info(`Run: ${pc2.cyan("npx create-turbo@latest")}`);
515
+ return;
516
+ }
517
+ const version = await getLatestVersion();
518
+ const manifest = createManifest(version);
519
+ await writeManifest(cwd, manifest);
520
+ spinner.succeed("VexBlocks initialized");
521
+ const packageManager = await getPackageManager(cwd);
522
+ logger.break();
523
+ logger.box("\u2705 VexBlocks initialized!", [
524
+ `Manifest: ${pc2.cyan(MANIFEST_FILE)}`,
525
+ `Version: ${pc2.cyan(version)}`,
526
+ "",
527
+ pc2.bold("Next steps:"),
528
+ `1. ${pc2.cyan("npx vexblocks add all")} - Add all CMS packages`,
529
+ "2. Configure your environment variables",
530
+ `3. ${pc2.cyan(`${packageManager} install`)}`,
531
+ `4. ${pc2.cyan(`${packageManager === "npm" ? "npm run" : packageManager} dev`)}`
532
+ ]);
533
+ } catch (error) {
534
+ spinner.fail("Failed to initialize");
535
+ throw error;
536
+ }
537
+ }
538
+
539
+ // src/commands/add.ts
540
+ import path5 from "path";
541
+ import fs5 from "fs-extra";
542
+ import pc3 from "picocolors";
543
+ import ora2 from "ora";
544
+ import { confirm as confirm2, checkbox } from "@inquirer/prompts";
545
+ var ALL_PACKAGES = ["backend", "shared", "types", "cms"];
546
+ async function addCommand(packages, options) {
547
+ const cwd = options.cwd ? path5.resolve(options.cwd) : process.cwd();
548
+ logger.title("\u{1F4E6} Add VexBlocks Packages");
549
+ const isTurbo = await isTurborepoProject(cwd);
550
+ if (!isTurbo) {
551
+ logger.error("This is not a Turborepo project.");
552
+ logger.info(
553
+ `Run ${pc3.cyan("npx vexblocks init")} first to set up your project.`
554
+ );
555
+ process.exit(1);
556
+ }
557
+ let manifest = await readManifest(cwd);
558
+ if (!manifest) {
559
+ const version2 = await getLatestVersion();
560
+ manifest = createManifest(version2);
561
+ await writeManifest(cwd, manifest);
562
+ logger.info(`Created ${MANIFEST_FILE}`);
563
+ }
564
+ let packagesToAdd = [];
565
+ if (packages.length === 0 || packages.includes("all")) {
566
+ if (packages.includes("all")) {
567
+ packagesToAdd = ALL_PACKAGES;
568
+ } else {
569
+ const selectedPackages = await checkbox({
570
+ message: "Which packages would you like to add?",
571
+ choices: ALL_PACKAGES.map((pkg) => ({
572
+ name: `${PACKAGE_NAMES[pkg]} (${PACKAGE_PATHS[pkg]})`,
573
+ value: pkg,
574
+ checked: true
575
+ }))
576
+ });
577
+ packagesToAdd = selectedPackages;
578
+ }
579
+ } else {
580
+ for (const pkg of packages) {
581
+ if (pkg !== "all" && !ALL_PACKAGES.includes(pkg)) {
582
+ logger.error(`Unknown package: ${pkg}`);
583
+ logger.info(`Available packages: ${ALL_PACKAGES.join(", ")}, all`);
584
+ process.exit(1);
585
+ }
586
+ }
587
+ packagesToAdd = packages.filter((p) => p !== "all");
588
+ }
589
+ if (packagesToAdd.length === 0) {
590
+ logger.info("No packages selected.");
591
+ return;
592
+ }
593
+ const resolvedPackages = resolveDependencies(packagesToAdd);
594
+ if (resolvedPackages.length !== packagesToAdd.length) {
595
+ const added = resolvedPackages.filter((p) => !packagesToAdd.includes(p));
596
+ logger.info(
597
+ `Adding dependencies: ${added.map((p) => PACKAGE_NAMES[p]).join(", ")}`
598
+ );
599
+ }
600
+ const existingPackages = [];
601
+ for (const pkg of resolvedPackages) {
602
+ const targetPath = getPackageTargetPath(cwd, pkg);
603
+ if (await fs5.pathExists(targetPath)) {
604
+ existingPackages.push(pkg);
605
+ }
606
+ }
607
+ if (existingPackages.length > 0 && !options.overwrite) {
608
+ logger.warn(`These packages already exist: ${existingPackages.join(", ")}`);
609
+ const overwrite = options.yes || await confirm2({
610
+ message: "Overwrite existing packages?",
611
+ default: false
612
+ });
613
+ if (!overwrite) {
614
+ const newPackages = resolvedPackages.filter(
615
+ (p) => !existingPackages.includes(p)
616
+ );
617
+ if (newPackages.length === 0) {
618
+ logger.info("No new packages to add.");
619
+ return;
620
+ }
621
+ resolvedPackages.length = 0;
622
+ resolvedPackages.push(...newPackages);
623
+ }
624
+ }
625
+ logger.break();
626
+ logger.log(pc3.bold("Packages to install:"));
627
+ for (const pkg of resolvedPackages) {
628
+ const targetPath = getPackageTargetPath(cwd, pkg);
629
+ const relativePath = path5.relative(cwd, targetPath);
630
+ logger.log(
631
+ ` ${pc3.green("+")} ${PACKAGE_NAMES[pkg]} \u2192 ${pc3.dim(relativePath)}`
632
+ );
633
+ }
634
+ logger.break();
635
+ const proceed = options.yes || await confirm2({
636
+ message: "Proceed with installation?",
637
+ default: true
638
+ });
639
+ if (!proceed) {
640
+ logger.info("Aborted.");
641
+ return;
642
+ }
643
+ const version = await getLatestVersion();
644
+ for (const pkg of resolvedPackages) {
645
+ await installPackage(cwd, pkg, version, manifest);
646
+ }
647
+ await writeManifest(cwd, manifest);
648
+ showNextSteps(resolvedPackages);
649
+ }
650
+ function resolveDependencies(packages) {
651
+ const resolved = /* @__PURE__ */ new Set();
652
+ const toProcess = [...packages];
653
+ while (toProcess.length > 0) {
654
+ const pkg = toProcess.pop();
655
+ if (resolved.has(pkg)) continue;
656
+ const deps = PACKAGE_DEPENDENCIES[pkg] || [];
657
+ for (const dep of deps) {
658
+ if (!resolved.has(dep)) {
659
+ toProcess.push(dep);
660
+ }
661
+ }
662
+ resolved.add(pkg);
663
+ }
664
+ const orderedPackages = [];
665
+ const order = ["backend", "shared", "types", "cms"];
666
+ for (const pkg of order) {
667
+ if (resolved.has(pkg)) {
668
+ orderedPackages.push(pkg);
669
+ }
670
+ }
671
+ return orderedPackages;
672
+ }
673
+ function getPackageTargetPath(cwd, pkg) {
674
+ switch (pkg) {
675
+ case "cms":
676
+ return path5.join(cwd, "apps", "cms");
677
+ case "backend":
678
+ return path5.join(cwd, "packages", "backend");
679
+ case "shared":
680
+ return path5.join(cwd, "packages", "cms-shared");
681
+ case "types":
682
+ return path5.join(cwd, "packages", "type-generator");
683
+ default:
684
+ return path5.join(cwd, PACKAGE_PATHS[pkg]);
685
+ }
686
+ }
687
+ async function installPackage(cwd, pkg, version, manifest) {
688
+ const spinner = ora2(`Installing ${PACKAGE_NAMES[pkg]}...`).start();
689
+ try {
690
+ const targetPath = getPackageTargetPath(cwd, pkg);
691
+ const sourcePath = PACKAGE_PATHS[pkg];
692
+ if (pkg === "backend") {
693
+ await installBackendPackage(targetPath, sourcePath, spinner);
694
+ } else {
695
+ await fs5.ensureDir(targetPath);
696
+ await downloadAndExtractPackage(sourcePath, targetPath, (file) => {
697
+ spinner.text = `Installing ${PACKAGE_NAMES[pkg]}... ${pc3.dim(file)}`;
698
+ });
699
+ }
700
+ const config = {
701
+ version,
702
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
703
+ path: path5.relative(cwd, targetPath)
704
+ };
705
+ manifest.packages[pkg] = config;
706
+ spinner.succeed(`Installed ${PACKAGE_NAMES[pkg]}`);
707
+ } catch (error) {
708
+ spinner.fail(`Failed to install ${PACKAGE_NAMES[pkg]}`);
709
+ throw error;
710
+ }
711
+ }
712
+ async function installBackendPackage(targetPath, sourcePath, spinner) {
713
+ const convexPath = path5.join(targetPath, "convex");
714
+ const existingSchemaPath = path5.join(convexPath, "schema.ts");
715
+ const hasSchema = await fs5.pathExists(existingSchemaPath);
716
+ if (hasSchema) {
717
+ spinner.text = "Detected existing Convex schema, merging CMS tables...";
718
+ const backupPath = `${existingSchemaPath}.backup-${Date.now()}`;
719
+ await fs5.copy(existingSchemaPath, backupPath);
720
+ logger.info(`Backed up existing schema to ${path5.basename(backupPath)}`);
721
+ const cmsFiles = [
722
+ "convex/cms",
723
+ "convex/schema.cms.ts",
724
+ "convex/auth.ts",
725
+ "convex/auth.config.ts",
726
+ "convex/http.ts",
727
+ "convex/settings.ts",
728
+ "better-auth",
729
+ "emails"
730
+ ];
731
+ for (const file of cmsFiles) {
732
+ const fullSourcePath = `${sourcePath}/${file}`;
733
+ const fullTargetPath = path5.join(targetPath, file);
734
+ spinner.text = `Downloading ${file}...`;
735
+ try {
736
+ await downloadAndExtractPackage(fullSourcePath, fullTargetPath);
737
+ } catch {
738
+ try {
739
+ const content = await fetchFile(fullSourcePath);
740
+ await fs5.ensureDir(path5.dirname(fullTargetPath));
741
+ await fs5.writeFile(fullTargetPath, content);
742
+ } catch {
743
+ }
744
+ }
745
+ }
746
+ const schemaInstructions = `
747
+ // === VEXBLOCKS CMS ===
748
+ // Add the following imports and tables to your schema:
749
+ //
750
+ // import { cmsSchemaExports } from "./schema.cms"
751
+ //
752
+ // Then add to your defineSchema:
753
+ // export default defineSchema({
754
+ // ...yourExistingTables,
755
+ // ...cmsSchemaExports,
756
+ // })
757
+ // === END VEXBLOCKS CMS ===
758
+ `;
759
+ const existingContent = await fs5.readFile(existingSchemaPath, "utf-8");
760
+ if (!existingContent.includes("VEXBLOCKS CMS")) {
761
+ await fs5.appendFile(existingSchemaPath, schemaInstructions);
762
+ }
763
+ } else {
764
+ await fs5.ensureDir(targetPath);
765
+ await downloadAndExtractPackage(sourcePath, targetPath, (file) => {
766
+ spinner.text = `Installing backend... ${pc3.dim(file)}`;
767
+ });
768
+ }
769
+ }
770
+ function showNextSteps(packages) {
771
+ const packageManager = "pnpm";
772
+ logger.break();
773
+ logger.box("\u2705 Packages installed successfully!", [
774
+ "",
775
+ pc3.bold("Next steps:"),
776
+ "",
777
+ `1. Install dependencies: ${pc3.cyan(`${packageManager} install`)}`,
778
+ "",
779
+ "2. Set up environment variables:",
780
+ ...getRequiredEnvVars(packages).map((v) => ` ${pc3.dim(v)}`),
781
+ "",
782
+ "3. Set up Convex (if not already):",
783
+ ` ${pc3.cyan("npx convex dev")} in packages/backend`,
784
+ "",
785
+ "4. Start development:",
786
+ ` ${pc3.cyan(`${packageManager} dev`)}`,
787
+ "",
788
+ pc3.dim("CMS will be available at http://localhost:3001")
789
+ ]);
790
+ const optionalVars = getOptionalEnvVars(packages);
791
+ if (optionalVars.length > 0) {
792
+ logger.break();
793
+ logger.log(pc3.bold("Optional environment variables:"));
794
+ for (const { name, description } of optionalVars) {
795
+ logger.log(` ${pc3.yellow(name)}`);
796
+ logger.log(` ${pc3.dim(description)}`);
797
+ }
798
+ }
799
+ }
800
+ function getRequiredEnvVars(packages) {
801
+ const vars = /* @__PURE__ */ new Set();
802
+ for (const pkg of packages) {
803
+ const required = REQUIRED_ENV_VARS[pkg] || [];
804
+ for (const v of required) {
805
+ vars.add(v);
806
+ }
807
+ }
808
+ return Array.from(vars);
809
+ }
810
+ function getOptionalEnvVars(packages) {
811
+ const vars = [];
812
+ const seen = /* @__PURE__ */ new Set();
813
+ for (const pkg of packages) {
814
+ const optional = OPTIONAL_ENV_VARS[pkg] || [];
815
+ for (const v of optional) {
816
+ if (!seen.has(v.name)) {
817
+ seen.add(v.name);
818
+ vars.push(v);
819
+ }
820
+ }
821
+ }
822
+ return vars;
823
+ }
824
+
825
+ // src/commands/upgrade.ts
826
+ import path6 from "path";
827
+ import fs6 from "fs-extra";
828
+ import pc4 from "picocolors";
829
+ import ora3 from "ora";
830
+ import { confirm as confirm3 } from "@inquirer/prompts";
831
+ async function upgradeCommand(packages, options) {
832
+ const cwd = options.cwd ? path6.resolve(options.cwd) : process.cwd();
833
+ logger.title("\u2B06\uFE0F Upgrade VexBlocks Packages");
834
+ const manifest = await readManifest(cwd);
835
+ if (!manifest) {
836
+ logger.error("No vexblocks.json found. Run `vexblocks init` first.");
837
+ process.exit(1);
838
+ }
839
+ const spinner = ora3("Checking for updates...").start();
840
+ const latestVersion = await getLatestVersion();
841
+ spinner.stop();
842
+ const installedPackages = Object.keys(manifest.packages);
843
+ if (installedPackages.length === 0) {
844
+ logger.info("No packages installed. Run `vexblocks add <package>` first.");
845
+ return;
846
+ }
847
+ let packagesToUpgrade;
848
+ if (packages.length === 0) {
849
+ packagesToUpgrade = installedPackages;
850
+ } else {
851
+ packagesToUpgrade = packages.filter(
852
+ (p) => installedPackages.includes(p)
853
+ );
854
+ if (packagesToUpgrade.length === 0) {
855
+ logger.error("None of the specified packages are installed.");
856
+ return;
857
+ }
858
+ }
859
+ const updates = [];
860
+ for (const pkg of packagesToUpgrade) {
861
+ const config = manifest.packages[pkg];
862
+ if (!config) continue;
863
+ const comparison = compareVersions(config.version, latestVersion);
864
+ if (comparison < 0) {
865
+ updates.push({
866
+ pkg,
867
+ from: config.version,
868
+ to: latestVersion
869
+ });
870
+ }
871
+ }
872
+ if (updates.length === 0) {
873
+ logger.success("All packages are up to date!");
874
+ logger.info(`Current version: ${pc4.cyan(latestVersion)}`);
875
+ return;
876
+ }
877
+ logger.log(pc4.bold("\nAvailable updates:"));
878
+ logger.break();
879
+ for (const update of updates) {
880
+ const isManaged = MANAGED_PACKAGES.includes(update.pkg);
881
+ logger.log(
882
+ ` ${PACKAGE_NAMES[update.pkg]}`,
883
+ pc4.dim(`${update.from} \u2192 `),
884
+ pc4.green(update.to),
885
+ isManaged ? pc4.yellow(" (managed)") : ""
886
+ );
887
+ }
888
+ const changelog = await getChangelog(updates[0].from, latestVersion);
889
+ if (Object.keys(changelog).length > 0) {
890
+ logger.break();
891
+ logger.log(pc4.bold("Changelog:"));
892
+ for (const [version, changes] of Object.entries(changelog)) {
893
+ logger.log(`
894
+ ${pc4.cyan(version)}`);
895
+ for (const change of changes) {
896
+ logger.log(` ${pc4.dim("\u2022")} ${change}`);
897
+ }
898
+ }
899
+ }
900
+ if (options.check) {
901
+ logger.break();
902
+ logger.info(
903
+ `Run ${pc4.cyan("npx vexblocks upgrade")} to apply these updates.`
904
+ );
905
+ return;
906
+ }
907
+ logger.break();
908
+ const proceed = options.yes || await confirm3({
909
+ message: `Upgrade ${updates.length} package(s)?`,
910
+ default: true
911
+ });
912
+ if (!proceed) {
913
+ logger.info("Aborted.");
914
+ return;
915
+ }
916
+ for (const update of updates) {
917
+ await upgradePackage(cwd, update.pkg, update.to, manifest, options);
918
+ }
919
+ manifest.version = latestVersion;
920
+ await writeManifest(cwd, manifest);
921
+ logger.break();
922
+ logger.success(`Upgraded to version ${pc4.cyan(latestVersion)}`);
923
+ logger.info(
924
+ "Run your package manager's install command to update dependencies."
925
+ );
926
+ }
927
+ async function upgradePackage(cwd, pkg, version, manifest, options) {
928
+ const spinner = ora3(`Upgrading ${PACKAGE_NAMES[pkg]}...`).start();
929
+ try {
930
+ const config = manifest.packages[pkg];
931
+ if (!config) {
932
+ spinner.skip(`${PACKAGE_NAMES[pkg]} not installed`);
933
+ return;
934
+ }
935
+ const targetPath = path6.join(cwd, config.path);
936
+ if (!await fs6.pathExists(targetPath)) {
937
+ spinner.warn(
938
+ `${PACKAGE_NAMES[pkg]} directory not found at ${config.path}`
939
+ );
940
+ return;
941
+ }
942
+ const isManaged = MANAGED_PACKAGES.includes(pkg);
943
+ if (isManaged || options.force) {
944
+ const backupPath = await createBackup(targetPath);
945
+ spinner.text = `Created backup at ${path6.basename(backupPath)}`;
946
+ await fs6.remove(targetPath);
947
+ const sourcePath = getSourcePath(pkg);
948
+ await downloadAndExtractPackage(sourcePath, targetPath, (file) => {
949
+ spinner.text = `Upgrading ${PACKAGE_NAMES[pkg]}... ${pc4.dim(file)}`;
950
+ });
951
+ } else {
952
+ spinner.text = `Upgrading CMS files in ${PACKAGE_NAMES[pkg]}...`;
953
+ const cmsFiles = ["convex/cms", "convex/schema.cms.ts"];
954
+ for (const file of cmsFiles) {
955
+ const targetFilePath = path6.join(targetPath, file);
956
+ const sourceFilePath = `packages/backend/${file}`;
957
+ if (await fs6.pathExists(targetFilePath)) {
958
+ await createBackup(targetFilePath);
959
+ }
960
+ try {
961
+ await fs6.remove(targetFilePath);
962
+ await downloadAndExtractPackage(sourceFilePath, targetFilePath);
963
+ } catch {
964
+ }
965
+ }
966
+ }
967
+ const newConfig = {
968
+ ...config,
969
+ version,
970
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
971
+ };
972
+ manifest.packages[pkg] = newConfig;
973
+ spinner.succeed(`Upgraded ${PACKAGE_NAMES[pkg]} to ${version}`);
974
+ } catch (error) {
975
+ spinner.fail(`Failed to upgrade ${PACKAGE_NAMES[pkg]}`);
976
+ throw error;
977
+ }
978
+ }
979
+ function getSourcePath(pkg) {
980
+ switch (pkg) {
981
+ case "cms":
982
+ return "apps/cms";
983
+ case "backend":
984
+ return "packages/backend";
985
+ case "shared":
986
+ return "packages/cms-shared";
987
+ case "types":
988
+ return "packages/type-generator";
989
+ default:
990
+ throw new Error(`Unknown package: ${pkg}`);
991
+ }
992
+ }
993
+
994
+ // src/commands/diff.ts
995
+ import path7 from "path";
996
+ import fs7 from "fs-extra";
997
+ import pc5 from "picocolors";
998
+ import ora4 from "ora";
999
+ import { createTwoFilesPatch } from "diff";
1000
+ async function diffCommand(packageArg, options) {
1001
+ const cwd = options.cwd ? path7.resolve(options.cwd) : process.cwd();
1002
+ logger.title("\u{1F50D} VexBlocks Diff");
1003
+ const manifest = await readManifest(cwd);
1004
+ if (!manifest) {
1005
+ logger.error("No vexblocks.json found. Run `vexblocks init` first.");
1006
+ process.exit(1);
1007
+ }
1008
+ const installedPackages = Object.keys(manifest.packages);
1009
+ if (installedPackages.length === 0) {
1010
+ logger.info("No packages installed.");
1011
+ return;
1012
+ }
1013
+ let packageToDiff;
1014
+ if (packageArg) {
1015
+ if (!installedPackages.includes(packageArg)) {
1016
+ logger.error(`Package "${packageArg}" is not installed.`);
1017
+ logger.info(`Installed packages: ${installedPackages.join(", ")}`);
1018
+ return;
1019
+ }
1020
+ packageToDiff = packageArg;
1021
+ } else if (installedPackages.length === 1) {
1022
+ packageToDiff = installedPackages[0];
1023
+ } else {
1024
+ logger.error("Please specify a package to diff.");
1025
+ logger.info(`Usage: ${pc5.cyan("vexblocks diff <package>")}`);
1026
+ logger.info(`Installed packages: ${installedPackages.join(", ")}`);
1027
+ return;
1028
+ }
1029
+ const spinner = ora4(`Comparing ${PACKAGE_NAMES[packageToDiff]}...`).start();
1030
+ try {
1031
+ const config = manifest.packages[packageToDiff];
1032
+ if (!config) {
1033
+ spinner.fail("Package configuration not found");
1034
+ return;
1035
+ }
1036
+ const localPath = path7.join(cwd, config.path);
1037
+ const remotePath = getSourcePath2(packageToDiff);
1038
+ const remoteFiles = await getPackageFiles(remotePath);
1039
+ let totalDiffs = 0;
1040
+ let newFiles = 0;
1041
+ let modifiedFiles = 0;
1042
+ let deletedFiles = 0;
1043
+ spinner.stop();
1044
+ for (const file of remoteFiles) {
1045
+ const localFilePath = path7.join(localPath, file);
1046
+ const remoteFilePath = `${remotePath}/${file}`;
1047
+ let localContent = "";
1048
+ let remoteContent = "";
1049
+ if (await fs7.pathExists(localFilePath)) {
1050
+ localContent = await fs7.readFile(localFilePath, "utf-8");
1051
+ }
1052
+ try {
1053
+ remoteContent = await fetchFile(remoteFilePath);
1054
+ } catch {
1055
+ continue;
1056
+ }
1057
+ if (!localContent && remoteContent) {
1058
+ newFiles++;
1059
+ totalDiffs++;
1060
+ logger.log(`${pc5.green("+")} ${file} ${pc5.dim("(new file)")}`);
1061
+ } else if (localContent !== remoteContent) {
1062
+ modifiedFiles++;
1063
+ totalDiffs++;
1064
+ logger.log(`${pc5.yellow("~")} ${file} ${pc5.dim("(modified)")}`);
1065
+ const patch = createTwoFilesPatch(
1066
+ `local/${file}`,
1067
+ `remote/${file}`,
1068
+ localContent,
1069
+ remoteContent,
1070
+ "",
1071
+ ""
1072
+ );
1073
+ const lines = patch.split("\n").slice(4);
1074
+ const maxLines = 10;
1075
+ let lineCount = 0;
1076
+ for (const line of lines) {
1077
+ if (lineCount >= maxLines) {
1078
+ logger.log(pc5.dim(` ... (${lines.length - maxLines} more lines)`));
1079
+ break;
1080
+ }
1081
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1082
+ logger.log(pc5.green(` ${line}`));
1083
+ lineCount++;
1084
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
1085
+ logger.log(pc5.red(` ${line}`));
1086
+ lineCount++;
1087
+ }
1088
+ }
1089
+ logger.break();
1090
+ }
1091
+ }
1092
+ const localFiles = await getLocalFiles(localPath);
1093
+ for (const file of localFiles) {
1094
+ if (!remoteFiles.includes(file)) {
1095
+ if (file.includes("node_modules") || file.includes("dist") || file.includes(".next") || file.startsWith(".")) {
1096
+ continue;
1097
+ }
1098
+ deletedFiles++;
1099
+ totalDiffs++;
1100
+ logger.log(`${pc5.red("-")} ${file} ${pc5.dim("(deleted in remote)")}`);
1101
+ }
1102
+ }
1103
+ logger.break();
1104
+ if (totalDiffs === 0) {
1105
+ logger.success(
1106
+ "No differences found. Local files match the latest version."
1107
+ );
1108
+ } else {
1109
+ logger.log(pc5.bold("Summary:"));
1110
+ if (newFiles > 0) logger.log(` ${pc5.green(`+${newFiles}`)} new files`);
1111
+ if (modifiedFiles > 0)
1112
+ logger.log(` ${pc5.yellow(`~${modifiedFiles}`)} modified files`);
1113
+ if (deletedFiles > 0)
1114
+ logger.log(` ${pc5.red(`-${deletedFiles}`)} files removed in remote`);
1115
+ logger.break();
1116
+ logger.info(
1117
+ `Run ${pc5.cyan("npx vexblocks upgrade")} to update to the latest version.`
1118
+ );
1119
+ }
1120
+ } catch (error) {
1121
+ spinner.fail("Failed to compare files");
1122
+ throw error;
1123
+ }
1124
+ }
1125
+ function getSourcePath2(pkg) {
1126
+ switch (pkg) {
1127
+ case "cms":
1128
+ return "apps/cms";
1129
+ case "backend":
1130
+ return "packages/backend";
1131
+ case "shared":
1132
+ return "packages/cms-shared";
1133
+ case "types":
1134
+ return "packages/type-generator";
1135
+ default:
1136
+ throw new Error(`Unknown package: ${pkg}`);
1137
+ }
1138
+ }
1139
+ async function getLocalFiles(dir, base = "") {
1140
+ const files = [];
1141
+ if (!await fs7.pathExists(dir)) {
1142
+ return files;
1143
+ }
1144
+ const entries = await fs7.readdir(dir, { withFileTypes: true });
1145
+ for (const entry of entries) {
1146
+ const relativePath = base ? `${base}/${entry.name}` : entry.name;
1147
+ if (entry.isDirectory()) {
1148
+ if (["node_modules", "dist", ".next", ".turbo", ".git"].includes(entry.name)) {
1149
+ continue;
1150
+ }
1151
+ const subFiles = await getLocalFiles(
1152
+ path7.join(dir, entry.name),
1153
+ relativePath
1154
+ );
1155
+ files.push(...subFiles);
1156
+ } else {
1157
+ files.push(relativePath);
1158
+ }
1159
+ }
1160
+ return files;
1161
+ }
1162
+
1163
+ // src/utils/package-info.ts
1164
+ import fs8 from "fs-extra";
1165
+ import path8 from "path";
1166
+ import { fileURLToPath } from "url";
1167
+ var __filename = fileURLToPath(import.meta.url);
1168
+ var __dirname = path8.dirname(__filename);
1169
+ async function getPackageInfo() {
1170
+ const packageJsonPath = path8.resolve(__dirname, "../../package.json");
1171
+ try {
1172
+ const packageJson = await fs8.readJson(packageJsonPath);
1173
+ return {
1174
+ name: packageJson.name || "vexblocks",
1175
+ version: packageJson.version || "0.0.0"
1176
+ };
1177
+ } catch {
1178
+ return {
1179
+ name: "vexblocks",
1180
+ version: "0.0.0"
1181
+ };
1182
+ }
1183
+ }
1184
+
1185
+ // src/index.ts
1186
+ async function main() {
1187
+ const packageInfo = await getPackageInfo();
1188
+ const program = new Command().name("vexblocks").description("Add VexBlocks Headless CMS to your Turborepo project").version(packageInfo.version, "-v, --version", "Display the version number");
1189
+ program.command("init").description(
1190
+ "Initialize a new VexBlocks project or add to existing Turborepo"
1191
+ ).option("-y, --yes", "Skip confirmation prompts").option("--cwd <path>", "Working directory (defaults to current directory)").action(initCommand);
1192
+ program.command("add [packages...]").description("Add VexBlocks packages to your project").option("-y, --yes", "Skip confirmation prompts").option("--cwd <path>", "Working directory (defaults to current directory)").option("--overwrite", "Overwrite existing files without prompting").addHelpText(
1193
+ "after",
1194
+ `
1195
+ ${pc6.bold("Available packages:")}
1196
+ cms The CMS dashboard (Next.js app)
1197
+ backend Convex backend with CMS functions
1198
+ shared Shared utilities and components
1199
+ types TypeScript type generator
1200
+ all All packages
1201
+
1202
+ ${pc6.bold("Examples:")}
1203
+ $ vexblocks add cms
1204
+ $ vexblocks add backend shared
1205
+ $ vexblocks add all
1206
+ `
1207
+ ).action(addCommand);
1208
+ program.command("upgrade [packages...]").description("Upgrade VexBlocks packages to the latest version").option("-y, --yes", "Skip confirmation prompts").option("--cwd <path>", "Working directory (defaults to current directory)").option("--check", "Check for updates without upgrading").option("--force", "Force upgrade even if local changes detected").action(upgradeCommand);
1209
+ program.command("diff [package]").description("Show differences between local files and latest version").option("--cwd <path>", "Working directory (defaults to current directory)").action(diffCommand);
1210
+ program.parse();
1211
+ }
1212
+ main().catch((error) => {
1213
+ console.error(pc6.red("Error:"), error.message);
1214
+ process.exit(1);
1215
+ });