@work-rjkashyap/unified-ui 0.1.2 → 0.2.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/bin/cli.mjs ADDED
@@ -0,0 +1,721 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ============================================================================
4
+ // Unified UI — CLI
5
+ // ============================================================================
6
+ // Copy-paste component installer for Unified UI.
7
+ //
8
+ // Usage:
9
+ // npx @work-rjkashyap/unified-ui add button
10
+ // npx @work-rjkashyap/unified-ui add button card badge
11
+ // npx @work-rjkashyap/unified-ui add --all
12
+ // npx @work-rjkashyap/unified-ui list
13
+ // npx @work-rjkashyap/unified-ui init
14
+ //
15
+ // Components are fetched from the registry at:
16
+ // https://unified-ui.vercel.app/r/<name>.json
17
+ //
18
+ // Files are written into the user's project at:
19
+ // src/components/ui/<component>.tsx
20
+ // src/lib/<util>.ts
21
+ // src/lib/motion/<preset>.ts
22
+ // src/styles/unified-ui.css
23
+ //
24
+ // This CLI resolves the full dependency tree — if you add "confirm-dialog",
25
+ // it also pulls in "alert-dialog", "button", "cn", "focus-ring", etc.
26
+ // ============================================================================
27
+
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
29
+ import { dirname, join, resolve } from "node:path";
30
+ import { createInterface } from "node:readline";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Config
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const REGISTRY_BASE_URL =
37
+ process.env.UNIFIED_UI_REGISTRY_URL ||
38
+ "https://unified-ui.vercel.app/r";
39
+
40
+ const CONFIG_FILE = "unified-ui.json";
41
+
42
+ const DEFAULT_CONFIG = {
43
+ $schema: "https://unified-ui.vercel.app/r/schema/config.json",
44
+ srcDir: "src",
45
+ aliases: {
46
+ components: "@/components/ui",
47
+ lib: "@/lib",
48
+ styles: "@/styles",
49
+ },
50
+ typescript: true,
51
+ };
52
+
53
+ const COLORS = {
54
+ reset: "\x1b[0m",
55
+ bold: "\x1b[1m",
56
+ dim: "\x1b[2m",
57
+ red: "\x1b[31m",
58
+ green: "\x1b[32m",
59
+ yellow: "\x1b[33m",
60
+ blue: "\x1b[34m",
61
+ magenta: "\x1b[35m",
62
+ cyan: "\x1b[36m",
63
+ };
64
+
65
+ const c = (color, text) => `${COLORS[color]}${text}${COLORS.reset}`;
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function log(msg = "") {
72
+ console.log(msg);
73
+ }
74
+
75
+ function logStep(icon, msg) {
76
+ console.log(` ${icon} ${msg}`);
77
+ }
78
+
79
+ function logError(msg) {
80
+ console.error(`\n ${c("red", "✗")} ${msg}\n`);
81
+ }
82
+
83
+ async function confirm(question) {
84
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
85
+ return new Promise((res) => {
86
+ rl.question(` ${question} ${c("dim", "(y/N)")} `, (answer) => {
87
+ rl.close();
88
+ res(answer.trim().toLowerCase() === "y");
89
+ });
90
+ });
91
+ }
92
+
93
+ async function fetchJSON(url) {
94
+ const response = await fetch(url);
95
+ if (!response.ok) {
96
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
97
+ }
98
+ return response.json();
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Config management
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function findProjectRoot() {
106
+ let dir = process.cwd();
107
+ while (dir !== dirname(dir)) {
108
+ if (existsSync(join(dir, "package.json"))) return dir;
109
+ dir = dirname(dir);
110
+ }
111
+ return process.cwd();
112
+ }
113
+
114
+ function loadConfig() {
115
+ const root = findProjectRoot();
116
+ const configPath = join(root, CONFIG_FILE);
117
+
118
+ if (existsSync(configPath)) {
119
+ try {
120
+ return {
121
+ root,
122
+ ...DEFAULT_CONFIG,
123
+ ...JSON.parse(readFileSync(configPath, "utf-8")),
124
+ };
125
+ } catch {
126
+ return { root, ...DEFAULT_CONFIG };
127
+ }
128
+ }
129
+
130
+ return { root, ...DEFAULT_CONFIG };
131
+ }
132
+
133
+ function saveConfig(config) {
134
+ const root = findProjectRoot();
135
+ const configPath = join(root, CONFIG_FILE);
136
+ const { root: _root, ...rest } = config;
137
+ writeFileSync(configPath, JSON.stringify(rest, null, 2) + "\n");
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Path resolution
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function resolveTargetPath(config, file) {
145
+ const srcDir = join(config.root, config.srcDir);
146
+
147
+ switch (file.type) {
148
+ case "component":
149
+ return join(srcDir, "components", "ui", basename(file.path));
150
+ case "util":
151
+ if (file.path.includes("motion/")) {
152
+ return join(srcDir, "lib", "motion", basename(file.path));
153
+ }
154
+ return join(srcDir, "lib", basename(file.path));
155
+ case "styles":
156
+ return join(srcDir, "styles", basename(file.path));
157
+ case "hook":
158
+ return join(srcDir, "hooks", basename(file.path));
159
+ default:
160
+ return join(srcDir, file.target || file.path);
161
+ }
162
+ }
163
+
164
+ function basename(p) {
165
+ return p.split("/").pop();
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Import path rewriting
170
+ // ---------------------------------------------------------------------------
171
+ // The registry items have imports like:
172
+ // import { cn } from "@/lib/cn"
173
+ // import { focusRingClasses } from "@/lib/focus-ring"
174
+ // import { Button } from "./button"
175
+ //
176
+ // We rewrite these to match the user's alias config.
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function rewriteContentImports(content, config) {
180
+ let result = content;
181
+
182
+ // Rewrite @/lib/* -> user's lib alias
183
+ if (config.aliases.lib !== "@/lib") {
184
+ result = result.replace(
185
+ /from\s+["']@\/lib\//g,
186
+ `from "${config.aliases.lib}/`,
187
+ );
188
+ }
189
+
190
+ // Rewrite @/components/ui/* -> user's components alias
191
+ if (config.aliases.components !== "@/components/ui") {
192
+ result = result.replace(
193
+ /from\s+["']@\/components\/ui\//g,
194
+ `from "${config.aliases.components}/`,
195
+ );
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Dependency resolution
203
+ // ---------------------------------------------------------------------------
204
+
205
+ async function resolveFullDependencyTree(names, registryUrl) {
206
+ const resolved = new Map();
207
+ const queue = [...names];
208
+ const visited = new Set();
209
+
210
+ while (queue.length > 0) {
211
+ const name = queue.shift();
212
+ if (visited.has(name)) continue;
213
+ visited.add(name);
214
+
215
+ try {
216
+ const item = await fetchJSON(`${registryUrl}/${name}.json`);
217
+ resolved.set(name, item);
218
+
219
+ // Queue registry dependencies (other components)
220
+ if (item.registryDependencies) {
221
+ for (const dep of item.registryDependencies) {
222
+ if (!visited.has(dep)) queue.push(dep);
223
+ }
224
+ }
225
+
226
+ // Queue internal util dependencies
227
+ if (item.internalDependencies) {
228
+ for (const util of item.internalDependencies.utils || []) {
229
+ if (!visited.has(util)) queue.push(util);
230
+ }
231
+ if (item.internalDependencies.motion && !visited.has("motion")) {
232
+ queue.push("motion");
233
+ }
234
+ }
235
+ } catch (err) {
236
+ logError(`Could not fetch "${name}" from registry: ${err.message}`);
237
+ }
238
+ }
239
+
240
+ return resolved;
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // npm dependency installer
245
+ // ---------------------------------------------------------------------------
246
+
247
+ async function detectPackageManager(root) {
248
+ if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) return "bun";
249
+ if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
250
+ if (existsSync(join(root, "yarn.lock"))) return "yarn";
251
+ return "npm";
252
+ }
253
+
254
+ function getInstallCommand(pm, deps) {
255
+ const packages = deps.join(" ");
256
+ switch (pm) {
257
+ case "bun":
258
+ return `bun add ${packages}`;
259
+ case "pnpm":
260
+ return `pnpm add ${packages}`;
261
+ case "yarn":
262
+ return `yarn add ${packages}`;
263
+ default:
264
+ return `npm install ${packages}`;
265
+ }
266
+ }
267
+
268
+ async function installNpmDeps(deps, root) {
269
+ if (deps.length === 0) return;
270
+
271
+ const pm = await detectPackageManager(root);
272
+ const cmd = getInstallCommand(pm, deps);
273
+
274
+ logStep("📦", `Installing npm dependencies with ${c("cyan", pm)}...`);
275
+ logStep(" ", c("dim", cmd));
276
+
277
+ const { execSync } = await import("node:child_process");
278
+ try {
279
+ execSync(cmd, { cwd: root, stdio: "pipe" });
280
+ logStep("✓", c("green", `${deps.length} package(s) installed`));
281
+ } catch (err) {
282
+ logStep(
283
+ "⚠",
284
+ c("yellow", `Auto-install failed. Run manually:\n ${cmd}`),
285
+ );
286
+ }
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // File writer
291
+ // ---------------------------------------------------------------------------
292
+
293
+ function writeFile(targetPath, content, config, overwrite = false) {
294
+ const rewritten = rewriteContentImports(content, config);
295
+
296
+ if (existsSync(targetPath) && !overwrite) {
297
+ return { path: targetPath, status: "skipped" };
298
+ }
299
+
300
+ mkdirSync(dirname(targetPath), { recursive: true });
301
+ writeFileSync(targetPath, rewritten);
302
+ return { path: targetPath, status: "created" };
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Commands
307
+ // ---------------------------------------------------------------------------
308
+
309
+ async function cmdInit() {
310
+ log();
311
+ log(` ${c("bold", "Unified UI")} ${c("dim", "— Initialize project")}`);
312
+ log();
313
+
314
+ const config = loadConfig();
315
+
316
+ if (existsSync(join(config.root, CONFIG_FILE))) {
317
+ const overwrite = await confirm(
318
+ `${c("yellow", CONFIG_FILE)} already exists. Overwrite?`,
319
+ );
320
+ if (!overwrite) {
321
+ logStep("↩", "Cancelled.");
322
+ log();
323
+ return;
324
+ }
325
+ }
326
+
327
+ saveConfig(DEFAULT_CONFIG);
328
+ logStep("✓", `Created ${c("cyan", CONFIG_FILE)}`);
329
+
330
+ // Create directories
331
+ const srcDir = join(config.root, config.srcDir);
332
+ const dirs = [
333
+ join(srcDir, "components", "ui"),
334
+ join(srcDir, "lib"),
335
+ join(srcDir, "styles"),
336
+ ];
337
+
338
+ for (const dir of dirs) {
339
+ mkdirSync(dir, { recursive: true });
340
+ logStep("✓", `Created ${c("dim", dir.replace(config.root + "/", ""))}`);
341
+ }
342
+
343
+ // Fetch and write the base utilities (cn + focus-ring) and styles
344
+ log();
345
+ logStep("📡", "Fetching base utilities from registry...");
346
+
347
+ const baseItems = ["cn", "focus-ring", "styles"];
348
+ for (const name of baseItems) {
349
+ try {
350
+ const item = await fetchJSON(`${REGISTRY_BASE_URL}/${name}.json`);
351
+ for (const file of item.files) {
352
+ const targetPath = resolveTargetPath(config, file);
353
+ writeFile(targetPath, file.content, config, true);
354
+ logStep("✓", `${c("green", file.path)}`);
355
+ }
356
+ } catch {
357
+ logStep("⚠", c("yellow", `Could not fetch ${name} — add manually later`));
358
+ }
359
+ }
360
+
361
+ // Install base npm deps
362
+ await installNpmDeps(
363
+ ["class-variance-authority", "clsx", "tailwind-merge"],
364
+ config.root,
365
+ );
366
+
367
+ log();
368
+ logStep("🎉", c("green", "Project initialized! Start adding components:"));
369
+ log();
370
+ log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add button")}`);
371
+ log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add card badge tabs")}`);
372
+ log();
373
+ }
374
+
375
+ async function cmdAdd(names, flags = {}) {
376
+ log();
377
+ log(` ${c("bold", "Unified UI")} ${c("dim", "— Add components")}`);
378
+ log();
379
+
380
+ const config = loadConfig();
381
+
382
+ if (!existsSync(join(config.root, CONFIG_FILE)) && !flags.yes) {
383
+ const init = await confirm(
384
+ `No ${c("cyan", CONFIG_FILE)} found. Initialize first?`,
385
+ );
386
+ if (init) {
387
+ await cmdInit();
388
+ log();
389
+ }
390
+ }
391
+
392
+ // If --all, fetch index and add everything
393
+ if (flags.all) {
394
+ logStep("📡", "Fetching full registry index...");
395
+ try {
396
+ const index = await fetchJSON(`${REGISTRY_BASE_URL}/index.json`);
397
+ names = index.items
398
+ .filter((item) => item.type === "unified-ui:component")
399
+ .map((item) => item.name);
400
+ logStep("✓", `Found ${c("cyan", names.length.toString())} components`);
401
+ } catch (err) {
402
+ logError(`Could not fetch registry index: ${err.message}`);
403
+ return;
404
+ }
405
+ }
406
+
407
+ if (names.length === 0) {
408
+ logError("No component names specified. Usage: npx @work-rjkashyap/unified-ui add <component...>");
409
+ return;
410
+ }
411
+
412
+ // Resolve the full dependency tree
413
+ logStep("🔍", `Resolving dependencies for: ${c("cyan", names.join(", "))}...`);
414
+ const tree = await resolveFullDependencyTree(names, REGISTRY_BASE_URL);
415
+
416
+ if (tree.size === 0) {
417
+ logError("No components resolved. Check the names and try again.");
418
+ return;
419
+ }
420
+
421
+ // Summarize what will be installed
422
+ const components = [];
423
+ const utils = [];
424
+ const styles = [];
425
+ const allNpmDeps = new Set();
426
+
427
+ for (const [name, item] of tree) {
428
+ switch (item.type) {
429
+ case "unified-ui:component":
430
+ components.push(name);
431
+ break;
432
+ case "unified-ui:util":
433
+ utils.push(name);
434
+ break;
435
+ case "unified-ui:styles":
436
+ styles.push(name);
437
+ break;
438
+ }
439
+
440
+ for (const dep of item.dependencies || []) {
441
+ allNpmDeps.add(dep);
442
+ }
443
+ }
444
+
445
+ log();
446
+ if (components.length > 0) {
447
+ logStep("🧩", `Components: ${c("cyan", components.join(", "))}`);
448
+ }
449
+ if (utils.length > 0) {
450
+ logStep("🔧", `Utilities: ${c("dim", utils.join(", "))}`);
451
+ }
452
+ if (allNpmDeps.size > 0) {
453
+ logStep("📦", `Packages: ${c("dim", [...allNpmDeps].join(", "))}`);
454
+ }
455
+ log();
456
+
457
+ // Confirm unless --yes
458
+ if (!flags.yes) {
459
+ const proceed = await confirm("Proceed with installation?");
460
+ if (!proceed) {
461
+ logStep("↩", "Cancelled.");
462
+ log();
463
+ return;
464
+ }
465
+ log();
466
+ }
467
+
468
+ // Write files
469
+ const results = [];
470
+ const overwrite = flags.overwrite || false;
471
+
472
+ for (const [_name, item] of tree) {
473
+ for (const file of item.files) {
474
+ const targetPath = resolveTargetPath(config, file);
475
+ const result = writeFile(targetPath, file.content, config, overwrite);
476
+ results.push(result);
477
+ }
478
+ }
479
+
480
+ // Report file results
481
+ const created = results.filter((r) => r.status === "created");
482
+ const skipped = results.filter((r) => r.status === "skipped");
483
+
484
+ for (const r of created) {
485
+ logStep("✓", c("green", r.path.replace(config.root + "/", "")));
486
+ }
487
+
488
+ if (skipped.length > 0) {
489
+ log();
490
+ for (const r of skipped) {
491
+ logStep(
492
+ "↩",
493
+ `${c("dim", r.path.replace(config.root + "/", ""))} ${c("yellow", "(exists, skipped)")}`,
494
+ );
495
+ }
496
+ log();
497
+ logStep(
498
+ "💡",
499
+ c("dim", "Use --overwrite to replace existing files."),
500
+ );
501
+ }
502
+
503
+ // Install npm dependencies
504
+ const depsToInstall = [...allNpmDeps].filter((dep) => {
505
+ // Check if already in package.json
506
+ try {
507
+ const pkg = JSON.parse(
508
+ readFileSync(join(config.root, "package.json"), "utf-8"),
509
+ );
510
+ const allPkgDeps = {
511
+ ...pkg.dependencies,
512
+ ...pkg.devDependencies,
513
+ ...pkg.peerDependencies,
514
+ };
515
+ return !allPkgDeps[dep];
516
+ } catch {
517
+ return true;
518
+ }
519
+ });
520
+
521
+ if (depsToInstall.length > 0) {
522
+ log();
523
+ await installNpmDeps(depsToInstall, config.root);
524
+ }
525
+
526
+ log();
527
+ logStep(
528
+ "🎉",
529
+ c("green", `Done! ${created.length} file(s) added.`),
530
+ );
531
+ log();
532
+ }
533
+
534
+ async function cmdList() {
535
+ log();
536
+ log(` ${c("bold", "Unified UI")} ${c("dim", "— Available components")}`);
537
+ log();
538
+
539
+ try {
540
+ const index = await fetchJSON(`${REGISTRY_BASE_URL}/index.json`);
541
+
542
+ // Group by category
543
+ const groups = {};
544
+ for (const item of index.items) {
545
+ if (item.type !== "unified-ui:component") continue;
546
+ const cat = item.category || "other";
547
+ if (!groups[cat]) groups[cat] = [];
548
+ groups[cat].push(item);
549
+ }
550
+
551
+ // Find category labels
552
+ const catLabels = {};
553
+ for (const cat of index.categories || []) {
554
+ catLabels[cat.name] = cat.label;
555
+ }
556
+
557
+ for (const [cat, items] of Object.entries(groups)) {
558
+ log(` ${c("bold", catLabels[cat] || cat)}`);
559
+ for (const item of items) {
560
+ const deps =
561
+ item.registryDependencies?.length > 0
562
+ ? c("dim", ` → ${item.registryDependencies.join(", ")}`)
563
+ : "";
564
+ log(` ${c("cyan", item.name.padEnd(22))} ${c("dim", item.description || "")}${deps}`);
565
+ }
566
+ log();
567
+ }
568
+
569
+ log(` ${c("dim", `${index.totalItems} items total`)}`);
570
+ log();
571
+ log(` ${c("dim", "Add a component:")} npx @work-rjkashyap/unified-ui add ${c("cyan", "<name>")}`);
572
+ log();
573
+ } catch (err) {
574
+ logError(`Could not fetch registry: ${err.message}`);
575
+ }
576
+ }
577
+
578
+ async function cmdDiff(names) {
579
+ log();
580
+ log(` ${c("bold", "Unified UI")} ${c("dim", "— Diff local vs registry")}`);
581
+ log();
582
+
583
+ const config = loadConfig();
584
+
585
+ for (const name of names) {
586
+ try {
587
+ const item = await fetchJSON(`${REGISTRY_BASE_URL}/${name}.json`);
588
+ for (const file of item.files) {
589
+ const targetPath = resolveTargetPath(config, file);
590
+ if (!existsSync(targetPath)) {
591
+ logStep("✗", `${c("red", name)}: not installed locally`);
592
+ continue;
593
+ }
594
+
595
+ const localContent = readFileSync(targetPath, "utf-8");
596
+ const registryContent = rewriteContentImports(file.content, config);
597
+
598
+ if (localContent === registryContent) {
599
+ logStep("✓", `${c("green", name)}: up to date`);
600
+ } else {
601
+ logStep("~", `${c("yellow", name)}: local changes detected`);
602
+ }
603
+ }
604
+ } catch (err) {
605
+ logStep("✗", `${c("red", name)}: ${err.message}`);
606
+ }
607
+ }
608
+
609
+ log();
610
+ }
611
+
612
+ function cmdHelp() {
613
+ log();
614
+ log(` ${c("bold", "Unified UI")} ${c("dim", "— Component Registry CLI")}`);
615
+ log();
616
+ log(" Usage:");
617
+ log(` ${c("cyan", "npx @work-rjkashyap/unified-ui")} ${c("green", "<command>")} [options]`);
618
+ log();
619
+ log(" Commands:");
620
+ log(` ${c("green", "init")} Initialize project config & base utils`);
621
+ log(` ${c("green", "add")} <component...> Add component(s) with dependencies`);
622
+ log(` ${c("green", "add")} --all Add all components`);
623
+ log(` ${c("green", "list")} List all available components`);
624
+ log(` ${c("green", "diff")} <component...> Compare local files with registry`);
625
+ log(` ${c("green", "help")} Show this help message`);
626
+ log();
627
+ log(" Options:");
628
+ log(` ${c("yellow", "--yes, -y")} Skip confirmation prompts`);
629
+ log(` ${c("yellow", "--overwrite")} Overwrite existing files`);
630
+ log(` ${c("yellow", "--all")} Add all components (with 'add')`);
631
+ log();
632
+ log(" Examples:");
633
+ log(` ${c("dim", "# Initialize project")} `);
634
+ log(` npx @work-rjkashyap/unified-ui init`);
635
+ log();
636
+ log(` ${c("dim", "# Add specific components")}`);
637
+ log(` npx @work-rjkashyap/unified-ui add button card badge`);
638
+ log();
639
+ log(` ${c("dim", "# Add all components, skip prompts")}`);
640
+ log(` npx @work-rjkashyap/unified-ui add --all -y`);
641
+ log();
642
+ log(` ${c("dim", "# Check for upstream changes")}`);
643
+ log(` npx @work-rjkashyap/unified-ui diff button card`);
644
+ log();
645
+ log(` Registry: ${c("cyan", REGISTRY_BASE_URL)}`);
646
+ log();
647
+ }
648
+
649
+ // ---------------------------------------------------------------------------
650
+ // Argument parsing
651
+ // ---------------------------------------------------------------------------
652
+
653
+ function parseArgs(argv) {
654
+ const args = argv.slice(2);
655
+ const command = args[0];
656
+ const flags = {};
657
+ const positional = [];
658
+
659
+ for (let i = 1; i < args.length; i++) {
660
+ const arg = args[i];
661
+ if (arg === "--yes" || arg === "-y") {
662
+ flags.yes = true;
663
+ } else if (arg === "--overwrite") {
664
+ flags.overwrite = true;
665
+ } else if (arg === "--all") {
666
+ flags.all = true;
667
+ } else if (arg.startsWith("--registry=")) {
668
+ flags.registryUrl = arg.split("=")[1];
669
+ } else if (!arg.startsWith("-")) {
670
+ positional.push(arg);
671
+ }
672
+ }
673
+
674
+ return { command, positional, flags };
675
+ }
676
+
677
+ // ---------------------------------------------------------------------------
678
+ // Main
679
+ // ---------------------------------------------------------------------------
680
+
681
+ async function main() {
682
+ const { command, positional, flags } = parseArgs(process.argv);
683
+
684
+ // Allow custom registry URL
685
+ if (flags.registryUrl) {
686
+ // Override the global — we use a let binding workaround below.
687
+ // (The const REGISTRY_BASE_URL is used by all functions via closure.)
688
+ // For simplicity, we set an env var that's already checked at the top.
689
+ process.env.UNIFIED_UI_REGISTRY_URL = flags.registryUrl;
690
+ }
691
+
692
+ switch (command) {
693
+ case "init":
694
+ await cmdInit();
695
+ break;
696
+ case "add":
697
+ await cmdAdd(positional, flags);
698
+ break;
699
+ case "list":
700
+ case "ls":
701
+ await cmdList();
702
+ break;
703
+ case "diff":
704
+ await cmdDiff(positional);
705
+ break;
706
+ case "help":
707
+ case "--help":
708
+ case "-h":
709
+ case undefined:
710
+ cmdHelp();
711
+ break;
712
+ default:
713
+ logError(`Unknown command: "${command}". Run "npx @work-rjkashyap/unified-ui help" for usage.`);
714
+ process.exit(1);
715
+ }
716
+ }
717
+
718
+ main().catch((err) => {
719
+ logError(err.message);
720
+ process.exit(1);
721
+ });