@dalexto/lexsys-cli 0.0.3 → 0.0.4

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/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  **Source of truth for:** Package role, command surface, core module boundaries
6
6
  **Full CLI reference:** [docs/reference/cli/CLI.md](../../docs/reference/cli/CLI.md)
7
7
  **Verified against:** `packages/cli/src/`
8
+ **Last reviewed:** 2026-05-30
8
9
 
9
10
  ---
10
11
 
@@ -50,8 +51,9 @@ Installed via `npm install -g lexsys` or run directly with `pnpm exec lexsys`.
50
51
  | `lexsys init next [directory]` | Scaffold a new Next.js App Router consumer (pinned Next.js 15.3.3) |
51
52
  | `lexsys add <component>` | Install one or more components into the consumer project |
52
53
  | `lexsys update [component]` | Update installed components; `--sync`, `--utilities`, `--styles`, `--force` |
54
+ | `lexsys reset [component]` | Restore components from registry templates (backup + overwrite) |
53
55
  | `lexsys list` | List available registry components |
54
- | `lexsys status` | Show installed component versions vs registry versions |
56
+ | `lexsys status` | Show installed components and template drift vs registry |
55
57
  | `lexsys doctor` | Check project health and config validity |
56
58
  | `lexsys config` | Read or modify `lexsys.config.json` |
57
59
  | `lexsys registry` | Inspect the active registry source |
@@ -69,20 +71,11 @@ Installed via `npm install -g lexsys` or run directly with `pnpm exec lexsys`.
69
71
 
70
72
  ---
71
73
 
72
- ## Core Modules
73
-
74
- | Module | Role |
75
- | --------------------------- | --------------------------------------------------------------------------- |
76
- | `core/config.ts` | Read/write `lexsys.config.json`; defines `LexsysConfig` schema and defaults |
77
- | `core/installer.ts` | File copy, conflict detection, idempotent installs |
78
- | `core/registry-provider.ts` | Selects local vs remote registry source |
79
- | `core/registry-resolver.ts` | Resolves registry items, utilities, and styles from active registry |
80
- | `core/tailwind-setup.ts` | Detects and wires Tailwind v4 CSS entrypoint |
81
- | `core/vite-scaffold.ts` | Detects and patches Vite config for Tailwind plugin |
82
- | `core/package-manager.ts` | Detects npm/pnpm/yarn and runs installs |
83
- | `core/context.ts` | Process-level `cwd` override via `--cwd` flag |
84
- | `core/flags.ts` | Shared flag parsing utilities |
85
- | `core/cli-error.ts` | Typed CLI error class and top-level error handler |
74
+ ## Source layout
75
+
76
+ Domain modules under `packages/cli/src/`: `config/`, `install/`, `registry/`,
77
+ `commands/`, `scaffold/`, `utils/`. Command behavior and config schema:
78
+ [docs/reference/cli/CLI.md](../../docs/reference/cli/CLI.md).
86
79
 
87
80
  ---
88
81
 
@@ -0,0 +1 @@
1
+ export declare const runReset: (args: string[]) => Promise<void>;
@@ -15,7 +15,7 @@ export interface LexsysConfig {
15
15
  paths: LexsysPathsConfig;
16
16
  aliases: LexsysAliasesConfig;
17
17
  tailwind: LexsysTailwindConfig;
18
- installed?: Record<string, string>;
18
+ installed?: string[];
19
19
  registryUrl?: string | null;
20
20
  }
21
21
  export interface LexsysTailwindConfig {
@@ -0,0 +1,6 @@
1
+ export declare const isLegacyInstalledRecord: (value: unknown) => boolean;
2
+ export declare const normalizeInstalled: (value: unknown) => string[];
3
+ export declare const isInstalled: (installed: string[], name: string) => boolean;
4
+ export declare const addInstalled: (installed: string[], itemName: string) => string[];
5
+ export declare const removeInstalled: (installed: string[], itemName: string) => string[];
6
+ export declare const findInstalledKey: (installed: string[], name: string) => string | undefined;
package/dist/index.js CHANGED
@@ -37,6 +37,38 @@ var handleCliError = (error) => {
37
37
  }
38
38
  process.exit(1);
39
39
  };
40
+
41
+ // src/config/installed.ts
42
+ var isLegacyInstalledRecord = (value) => {
43
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
44
+ };
45
+ var normalizeInstalled = (value) => {
46
+ if (Array.isArray(value)) {
47
+ return value.filter((entry) => typeof entry === "string");
48
+ }
49
+ if (value && typeof value === "object" && !Array.isArray(value)) {
50
+ return Object.keys(value);
51
+ }
52
+ return [];
53
+ };
54
+ var isInstalled = (installed, name) => {
55
+ const normalized = name.toLowerCase();
56
+ return installed.some((entry) => entry.toLowerCase() === normalized);
57
+ };
58
+ var addInstalled = (installed, itemName) => {
59
+ if (isInstalled(installed, itemName)) {
60
+ return installed;
61
+ }
62
+ return [...installed, itemName];
63
+ };
64
+ var removeInstalled = (installed, itemName) => {
65
+ const normalized = itemName.toLowerCase();
66
+ return installed.filter((entry) => entry.toLowerCase() !== normalized);
67
+ };
68
+ var findInstalledKey = (installed, name) => {
69
+ const normalized = name.toLowerCase();
70
+ return installed.find((entry) => entry.toLowerCase() === normalized);
71
+ };
40
72
  var hashContent = (content) => {
41
73
  return createHash("sha256").update(content, "utf-8").digest("hex");
42
74
  };
@@ -98,7 +130,7 @@ var defaultConfig = {
98
130
  paths: defaultPathsConfig,
99
131
  aliases: defaultAliasesConfig,
100
132
  tailwind: defaultTailwindConfig,
101
- installed: {},
133
+ installed: [],
102
134
  registryUrl: null
103
135
  };
104
136
  var getConfigPath = () => {
@@ -111,7 +143,7 @@ var loadConfig = async () => {
111
143
  }
112
144
  const content = await readFile(configPath, "utf-8");
113
145
  const parsed = JSON.parse(content);
114
- return {
146
+ const config = {
115
147
  ...defaultConfig,
116
148
  ...parsed,
117
149
  paths: {
@@ -125,8 +157,13 @@ var loadConfig = async () => {
125
157
  tailwind: {
126
158
  ...defaultTailwindConfig,
127
159
  ...parsed.tailwind
128
- }
160
+ },
161
+ installed: normalizeInstalled(parsed.installed)
129
162
  };
163
+ if (isLegacyInstalledRecord(parsed.installed)) {
164
+ await saveConfig(config);
165
+ }
166
+ return config;
130
167
  };
131
168
  var saveConfig = async (config) => {
132
169
  const configPath = getConfigPath();
@@ -738,7 +775,7 @@ var isRegistryItem = (value) => {
738
775
  return false;
739
776
  }
740
777
  const item = value;
741
- return typeof item.name === "string" && typeof item.canonicalName === "string" && typeof item.version === "string" && typeof item.type === "string" && typeof item.category === "string" && Array.isArray(item.aliases) && isStringArray(item.files) && isStringArray(item.dependencies) && isStringArray(item.registryDependencies) && isStringArray(item.utilities) && isStringArray(item.styles) && typeof item.target === "string";
778
+ return typeof item.name === "string" && typeof item.canonicalName === "string" && typeof item.type === "string" && typeof item.category === "string" && Array.isArray(item.aliases) && isStringArray(item.files) && isStringArray(item.dependencies) && isStringArray(item.registryDependencies) && isStringArray(item.utilities) && isStringArray(item.styles) && typeof item.target === "string";
742
779
  };
743
780
  var findInvalidRegistryItemIndex = (items) => {
744
781
  return items.findIndex((item) => {
@@ -1106,7 +1143,7 @@ var runAdd = async (args2) => {
1106
1143
  console.log("Dry run: no files or dependencies will be changed.\n");
1107
1144
  console.log("Components:");
1108
1145
  for (const item of resolvedItems) {
1109
- console.log(`- ${item.canonicalName} v${item.version}`);
1146
+ console.log(`- ${item.canonicalName}`);
1110
1147
  }
1111
1148
  console.log("\nDependencies:");
1112
1149
  for (const dependency of dependencies) {
@@ -1145,11 +1182,9 @@ var runAdd = async (args2) => {
1145
1182
  }
1146
1183
  console.log("");
1147
1184
  }
1148
- const installed = {
1149
- ...config.installed ?? {}
1150
- };
1185
+ let installed = [...config.installed ?? []];
1151
1186
  for (const item of successfullyInstalled) {
1152
- installed[item.name] = item.version;
1187
+ installed = addInstalled(installed, item.name);
1153
1188
  }
1154
1189
  await saveConfig({
1155
1190
  ...config,
@@ -1253,13 +1288,13 @@ var runDoctor = async (options = {}) => {
1253
1288
  if (registryFailed && options.noFallback) {
1254
1289
  return;
1255
1290
  }
1256
- const installed = config.installed ?? {};
1257
- if (Object.keys(installed).length) {
1291
+ const installed = config.installed ?? [];
1292
+ if (installed.length) {
1258
1293
  console.log("\nTracked components:");
1259
- for (const [name, version] of Object.entries(installed)) {
1294
+ for (const name of installed) {
1260
1295
  const item = await findItem(name);
1261
1296
  if (!item) {
1262
- console.log(`\xD7 ${name} v${version} (missing from registry)`);
1297
+ console.log(`\xD7 ${name} (missing from registry)`);
1263
1298
  continue;
1264
1299
  }
1265
1300
  const componentPath = join(
@@ -1268,9 +1303,7 @@ var runDoctor = async (options = {}) => {
1268
1303
  );
1269
1304
  const exists = await fileExists(componentPath);
1270
1305
  const layer = getInstallLayer(item) ?? "unknown";
1271
- console.log(
1272
- `${exists ? "\u2713" : "\xD7"} ${item.canonicalName} v${version} (${layer})`
1273
- );
1306
+ console.log(`${exists ? "\u2713" : "\xD7"} ${item.canonicalName} (${layer})`);
1274
1307
  }
1275
1308
  }
1276
1309
  };
@@ -1324,7 +1357,7 @@ Options
1324
1357
  --all, -a Update all tracked components
1325
1358
  --styles, -S Update generated token/theme CSS files
1326
1359
  --utilities, -u Update shared utility files
1327
- --sync Refresh tracked components even when versions match
1360
+ --sync Refresh tracked components even when templates already match
1328
1361
  --dry-run, -d Preview update without writing files
1329
1362
  --force, -f Write conflicted updates after creating backups
1330
1363
  --yes, -y Auto-confirm safe prompts
@@ -1363,6 +1396,24 @@ Usage
1363
1396
  Options
1364
1397
  --no-fallback Fail instead of falling back to local registry
1365
1398
  --help, -h Show this help
1399
+ `,
1400
+ reset: `
1401
+ Usage
1402
+ lexsys reset [component...]
1403
+
1404
+ Options
1405
+ --dry-run, -d Preview reset without writing files
1406
+ --with-deps, -w Also reset installed registry dependencies in the closure
1407
+ --no-fallback Fail instead of falling back to local registry
1408
+ --cwd, -C <path> Run from a different project directory
1409
+ --help, -h Show this help
1410
+
1411
+ Run without arguments for guided reset picker.
1412
+
1413
+ Examples
1414
+ lexsys reset button
1415
+ lexsys reset sidebar --with-deps
1416
+ lexsys reset button --dry-run
1366
1417
  `,
1367
1418
  uninstall: `
1368
1419
  Usage
@@ -1445,6 +1496,7 @@ Scaffold
1445
1496
  Components
1446
1497
  add <component...> Install components into your project [alias: a]
1447
1498
  update [component...] Update installed components [alias: up]
1499
+ reset [component...] Restore components from registry templates
1448
1500
  uninstall [component...] Remove installed components [alias: rm]
1449
1501
 
1450
1502
  Inspect
@@ -2387,7 +2439,6 @@ var runList = async (options = {}) => {
2387
2439
  const simplified = registryItems3.map((item) => ({
2388
2440
  name: item.name,
2389
2441
  canonicalName: item.canonicalName,
2390
- version: item.version,
2391
2442
  category: item.category,
2392
2443
  layer: getInstallLayer(item)
2393
2444
  }));
@@ -2409,9 +2460,7 @@ var runList = async (options = {}) => {
2409
2460
  }
2410
2461
  console.log(`${layerLabels[layer] ?? layer}:`);
2411
2462
  for (const item of items) {
2412
- console.log(
2413
- `- ${item.canonicalName} v${item.version} (${item.category})`
2414
- );
2463
+ console.log(`- ${item.canonicalName} (${item.category})`);
2415
2464
  }
2416
2465
  console.log("");
2417
2466
  }
@@ -2435,7 +2484,7 @@ var printRegistrySummary = (result) => {
2435
2484
  for (const item of result.items) {
2436
2485
  const remoteFileCount = item.remoteFiles?.length ?? 0;
2437
2486
  console.log(
2438
- `- ${item.canonicalName} v${item.version} (${item.type}/${item.category}, remote files: ${remoteFileCount})`
2487
+ `- ${item.canonicalName} (${item.type}/${item.category}, remote files: ${remoteFileCount})`
2439
2488
  );
2440
2489
  }
2441
2490
  };
@@ -2521,12 +2570,43 @@ var runRegistry = async (options = {}) => {
2521
2570
  process.exitCode = 1;
2522
2571
  }
2523
2572
  };
2573
+ var getComponentDriftStatus = async (name) => {
2574
+ const item = await findItem(name);
2575
+ if (!item) {
2576
+ return "missing";
2577
+ }
2578
+ const hasDrift = await itemHasTemplateDrift(item);
2579
+ return hasDrift ? "drift" : "in-sync";
2580
+ };
2581
+ var itemHasTemplateDrift = async (item) => {
2582
+ const config = await loadConfig();
2583
+ const installTarget = resolveItemInstallTarget(config, item);
2584
+ for (const file of item.files) {
2585
+ const fileName = file.split("/").at(-1);
2586
+ if (!fileName) {
2587
+ return true;
2588
+ }
2589
+ const targetPath = join(getCwd(), installTarget, fileName);
2590
+ if (!await fileExists(targetPath)) {
2591
+ return true;
2592
+ }
2593
+ const preparedContent = prepareInstalledFileContent(
2594
+ await readFile(getRegistryTemplatePath(file), "utf-8"),
2595
+ item
2596
+ );
2597
+ const targetContent = await readFile(targetPath, "utf-8");
2598
+ if (!hashesAreEqual(preparedContent, targetContent)) {
2599
+ return true;
2600
+ }
2601
+ }
2602
+ return false;
2603
+ };
2524
2604
 
2525
2605
  // src/commands/status.ts
2526
2606
  var runStatus = async (options = {}) => {
2527
2607
  const config = await loadConfig();
2528
- const installed = config.installed ?? {};
2529
- if (!Object.keys(installed).length) {
2608
+ const installed = config.installed ?? [];
2609
+ if (!installed.length) {
2530
2610
  console.log("No Lexsys components are currently tracked.");
2531
2611
  return;
2532
2612
  }
@@ -2541,14 +2621,15 @@ var runStatus = async (options = {}) => {
2541
2621
  return;
2542
2622
  }
2543
2623
  console.log("Installed Lexsys components:\n");
2544
- for (const [name, installedVersion] of Object.entries(installed)) {
2624
+ for (const name of installed) {
2545
2625
  const item = await findItem(name);
2546
2626
  if (!item) {
2547
- console.log(`- ${name} v${installedVersion} (missing from registry)`);
2627
+ console.log(`- ${name} (missing from registry)`);
2548
2628
  continue;
2549
2629
  }
2550
- const status = item.version === installedVersion ? "up to date" : `update available: v${installedVersion} \u2192 v${item.version}`;
2551
- console.log(`- ${item.canonicalName} v${installedVersion} (${status})`);
2630
+ const driftStatus = await getComponentDriftStatus(name);
2631
+ const status = driftStatus === "drift" ? "out of sync with registry" : "up to date with registry";
2632
+ console.log(`- ${item.canonicalName} (${status})`);
2552
2633
  }
2553
2634
  };
2554
2635
 
@@ -2573,7 +2654,7 @@ var computeRegistryClosure = (rootNames, items) => {
2573
2654
  return closure;
2574
2655
  };
2575
2656
  var findOrphanInstalledItems = (removedTargetNames, remainingInstalled, items) => {
2576
- const remainingNames = Object.keys(remainingInstalled);
2657
+ const remainingNames = remainingInstalled;
2577
2658
  const removedDependencyNames = /* @__PURE__ */ new Set();
2578
2659
  for (const removedName of removedTargetNames) {
2579
2660
  const closure = computeRegistryClosure([removedName], items);
@@ -2592,17 +2673,263 @@ var findOrphanInstalledItems = (removedTargetNames, remainingInstalled, items) =
2592
2673
  )
2593
2674
  ).filter((item) => Boolean(item)).filter((item) => removedDependencyNames.has(item.name));
2594
2675
  };
2676
+ var checkItemFiles = async (name) => {
2677
+ const item = await findItem(name);
2678
+ const config = await loadConfig();
2679
+ if (!item) {
2680
+ console.log(`Component "${name}" no longer exists in the registry.`);
2681
+ return;
2682
+ }
2683
+ const installTarget = resolveItemInstallTarget(config, item);
2684
+ console.log(`File check for ${item.canonicalName}:`);
2685
+ for (const file of item.files) {
2686
+ const fileName = file.split("/").at(-1);
2687
+ if (!fileName) {
2688
+ console.log(`- invalid registry file path: ${file}`);
2689
+ continue;
2690
+ }
2691
+ const targetPath = join(getCwd(), installTarget, fileName);
2692
+ if (!await fileExists(targetPath)) {
2693
+ console.log(`- missing: ${targetPath}`);
2694
+ continue;
2695
+ }
2696
+ const preparedContent = prepareInstalledFileContent(
2697
+ await readFile(getRegistryTemplatePath(file), "utf-8"),
2698
+ item
2699
+ );
2700
+ const targetContent = await readFile(targetPath, "utf-8");
2701
+ if (hashesAreEqual(preparedContent, targetContent)) {
2702
+ console.log(`- identical: ${targetPath}`);
2703
+ continue;
2704
+ }
2705
+ console.log(`- conflict: ${targetPath}`);
2706
+ }
2707
+ };
2708
+ var applyItemOverwrite = async (name, force) => {
2709
+ const item = await findItem(name);
2710
+ const config = await loadConfig();
2711
+ if (!item) {
2712
+ console.log(`Component "${name}" no longer exists in the registry.`);
2713
+ return false;
2714
+ }
2715
+ const installTarget = resolveItemInstallTarget(config, item);
2716
+ let hasConflict = false;
2717
+ console.log(`Applying update for ${item.canonicalName}:`);
2718
+ for (const file of item.files) {
2719
+ const sourcePath = getRegistryTemplatePath(file);
2720
+ const fileName = file.split("/").at(-1);
2721
+ if (!fileName) {
2722
+ console.log(`- invalid registry file path: ${file}`);
2723
+ hasConflict = true;
2724
+ continue;
2725
+ }
2726
+ const targetPath = join(getCwd(), installTarget, fileName);
2727
+ const preparedContent = prepareInstalledFileContent(
2728
+ await readFile(sourcePath, "utf-8"),
2729
+ item
2730
+ );
2731
+ await mkdir(dirname(targetPath), { recursive: true });
2732
+ if (!await fileExists(targetPath)) {
2733
+ await writeFile(targetPath, preparedContent, "utf-8");
2734
+ console.log(`- restored missing file: ${targetPath}`);
2735
+ continue;
2736
+ }
2737
+ const targetContent = await readFile(targetPath, "utf-8");
2738
+ if (hashesAreEqual(preparedContent, targetContent)) {
2739
+ console.log(`- identical: ${targetPath}`);
2740
+ continue;
2741
+ }
2742
+ if (force) {
2743
+ const backupPath = await createBackupFile(targetPath);
2744
+ if (backupPath) {
2745
+ console.log(`- backup created: ${backupPath}`);
2746
+ }
2747
+ await writeFile(targetPath, preparedContent, "utf-8");
2748
+ console.log(`- force updated file: ${targetPath}`);
2749
+ continue;
2750
+ }
2751
+ hasConflict = true;
2752
+ console.log(`- conflict (user modified): ${targetPath}`);
2753
+ }
2754
+ if (hasConflict) {
2755
+ console.log(
2756
+ "Update finished with conflicts. Review conflicted files before retrying."
2757
+ );
2758
+ return false;
2759
+ }
2760
+ console.log(`\u2714 ${item.canonicalName} updated successfully`);
2761
+ return true;
2762
+ };
2763
+ var checkItemUpdate = async (name, dryRun, force, sync = false) => {
2764
+ const item = await findItem(name);
2765
+ if (!item) {
2766
+ console.log(`Component "${name}" no longer exists in the registry.`);
2767
+ return false;
2768
+ }
2769
+ const driftStatus = await getComponentDriftStatus(name);
2770
+ if (driftStatus === "missing") {
2771
+ console.log(`Component "${name}" no longer exists in the registry.`);
2772
+ return false;
2773
+ }
2774
+ const hasDrift = driftStatus === "drift";
2775
+ if (!sync && !hasDrift) {
2776
+ console.log(`${item.canonicalName} is up to date with the registry.`);
2777
+ return false;
2778
+ }
2779
+ if (sync && !hasDrift) {
2780
+ console.log(
2781
+ `${item.canonicalName} template sync (already matches registry)`
2782
+ );
2783
+ } else {
2784
+ console.log(`${item.canonicalName} can be updated from registry templates`);
2785
+ }
2786
+ if (dryRun) {
2787
+ console.log("\nChanged file candidates:");
2788
+ for (const file of item.files) {
2789
+ console.log(`~ ${file}`);
2790
+ }
2791
+ console.log("\nDry run: no files will be changed.");
2792
+ console.log("Update plan:");
2793
+ if (force) {
2794
+ console.log(
2795
+ "- Force mode requested: conflicted files require backup before overwrite"
2796
+ );
2797
+ console.log(
2798
+ "- Dry run: backups would be created before forced overwrites"
2799
+ );
2800
+ }
2801
+ console.log(`- Check installed files for ${item.canonicalName}`);
2802
+ console.log("- Compare existing files with registry templates");
2803
+ console.log("- Report conflicts before writing changes");
2804
+ console.log("- Never overwrite user-modified files silently");
2805
+ await checkItemFiles(name);
2806
+ return false;
2807
+ }
2808
+ return await applyItemOverwrite(name, force);
2809
+ };
2810
+ var resetItem = async (name, dryRun) => {
2811
+ const item = await findItem(name);
2812
+ if (!item) {
2813
+ console.log(`Component "${name}" no longer exists in the registry.`);
2814
+ return false;
2815
+ }
2816
+ const hasDrift = await itemHasTemplateDrift(item);
2817
+ if (!hasDrift) {
2818
+ console.log(`${item.canonicalName} already matches registry templates.`);
2819
+ return false;
2820
+ }
2821
+ if (dryRun) {
2822
+ console.log(`Reset plan for ${item.canonicalName}:`);
2823
+ console.log("- Backups would be created before overwriting changed files");
2824
+ console.log("- Files would be restored from registry templates");
2825
+ for (const file of item.files) {
2826
+ console.log(`~ ${file}`);
2827
+ }
2828
+ console.log("\nDry run: no files will be changed.");
2829
+ await checkItemFiles(name);
2830
+ return false;
2831
+ }
2832
+ return await applyItemOverwrite(name, true);
2833
+ };
2595
2834
 
2596
- // src/commands/uninstall.ts
2597
- var normalizeInstalledKey = (name) => {
2598
- return name.toLowerCase();
2835
+ // src/commands/reset.ts
2836
+ var resolveInstalledKey = async (name, installed) => {
2837
+ const direct = findInstalledKey(installed, name);
2838
+ if (direct) {
2839
+ return direct;
2840
+ }
2841
+ const item = await findItem(name);
2842
+ if (!item) {
2843
+ return void 0;
2844
+ }
2845
+ return findInstalledKey(installed, item.name);
2846
+ };
2847
+ var runReset = async (args2) => {
2848
+ const dryRun = hasFlag(args2, "--dry-run", "-d");
2849
+ const withDeps = hasFlag(args2, "--with-deps", "-w");
2850
+ const noFallback = hasFlag(args2, "--no-fallback");
2851
+ const targetArgs = removeFlagsWithValues(
2852
+ removeFlags(args2, [
2853
+ "--dry-run",
2854
+ "-d",
2855
+ "--with-deps",
2856
+ "-w",
2857
+ "--no-fallback"
2858
+ ]),
2859
+ ["--cwd", "-C"]
2860
+ );
2861
+ const config = await loadConfig();
2862
+ const installed = [...config.installed ?? []];
2863
+ if (!installed.length) {
2864
+ console.log("No components installed.");
2865
+ return;
2866
+ }
2867
+ try {
2868
+ await getRegistryProviderResult({
2869
+ fallback: !noFallback
2870
+ });
2871
+ } catch (error) {
2872
+ console.log("Failed to resolve registry.");
2873
+ console.log(error instanceof Error ? error.message : String(error));
2874
+ process.exitCode = 1;
2875
+ return;
2876
+ }
2877
+ if (!targetArgs.length) {
2878
+ const selected = await promptMultiselect(
2879
+ "Select components to reset",
2880
+ installed.map((name) => ({ title: name, value: name })),
2881
+ { min: 1 }
2882
+ );
2883
+ if (!selected.length) return;
2884
+ targetArgs.push(...selected);
2885
+ }
2886
+ const resetNames = /* @__PURE__ */ new Set();
2887
+ for (const name of targetArgs) {
2888
+ const installedKey = await resolveInstalledKey(name, installed);
2889
+ if (!installedKey) {
2890
+ console.log(`Component "${name}" is not tracked as installed.`);
2891
+ continue;
2892
+ }
2893
+ resetNames.add(installedKey);
2894
+ }
2895
+ if (!resetNames.size) {
2896
+ console.log("No installed components matched the request.");
2897
+ return;
2898
+ }
2899
+ if (withDeps) {
2900
+ const allItems = await getRegistryItems({ fallback: !noFallback });
2901
+ for (const rootName of [...resetNames]) {
2902
+ const closure = computeRegistryClosure([rootName], allItems);
2903
+ for (const dependencyName of closure) {
2904
+ if (isInstalled(installed, dependencyName)) {
2905
+ resetNames.add(dependencyName);
2906
+ }
2907
+ }
2908
+ }
2909
+ }
2910
+ const resetItems = await resolveRegistryItems([...resetNames], {
2911
+ fallback: !noFallback
2912
+ });
2913
+ if (dryRun) {
2914
+ console.log("Dry run: no files will be changed.\n");
2915
+ console.log("Components:");
2916
+ for (const item of resetItems) {
2917
+ console.log(`- ${item.canonicalName}`);
2918
+ }
2919
+ console.log("");
2920
+ }
2921
+ for (const item of resetItems) {
2922
+ await resetItem(item.name, dryRun);
2923
+ console.log("");
2924
+ }
2599
2925
  };
2926
+
2927
+ // src/commands/uninstall.ts
2600
2928
  var resolveInstalledItems = async (installed) => {
2601
- const names = Object.keys(installed);
2602
- if (!names.length) {
2929
+ if (!installed.length) {
2603
2930
  return [];
2604
2931
  }
2605
- return resolveRegistryItems(names);
2932
+ return resolveRegistryItems(installed);
2606
2933
  };
2607
2934
  var collectOrphanedSharedResources = (removedItems, remainingItems) => {
2608
2935
  const removedUtilities = collectUtilities(removedItems);
@@ -2618,14 +2945,6 @@ var collectOrphanedSharedResources = (removedItems, remainingItems) => {
2618
2945
  })
2619
2946
  };
2620
2947
  };
2621
- var removeInstalledKey = (installed, itemName) => {
2622
- const installedKey = Object.keys(installed).find((key) => {
2623
- return normalizeInstalledKey(key) === normalizeInstalledKey(itemName);
2624
- });
2625
- if (installedKey) {
2626
- delete installed[installedKey];
2627
- }
2628
- };
2629
2948
  var runUninstall = async (args2) => {
2630
2949
  const dryRun = hasFlag(args2, "--dry-run", "-d");
2631
2950
  const withDeps = hasFlag(args2, "--with-deps", "-w");
@@ -2642,7 +2961,7 @@ var runUninstall = async (args2) => {
2642
2961
  );
2643
2962
  if (!targetArgs.length) {
2644
2963
  const preConfig = await loadConfig();
2645
- const installedNames = Object.keys(preConfig.installed ?? {});
2964
+ const installedNames = preConfig.installed ?? [];
2646
2965
  if (installedNames.length === 0) {
2647
2966
  console.log("No components installed.");
2648
2967
  return;
@@ -2656,7 +2975,7 @@ var runUninstall = async (args2) => {
2656
2975
  targetArgs.push(...selected);
2657
2976
  }
2658
2977
  const config = await loadConfig();
2659
- const installed = config.installed ?? {};
2978
+ const installed = [...config.installed ?? []];
2660
2979
  const resolvedTargets = [];
2661
2980
  const notTracked = [];
2662
2981
  for (const name of targetArgs) {
@@ -2665,10 +2984,7 @@ var runUninstall = async (args2) => {
2665
2984
  console.log(`Component "${name}" not found in registry.`);
2666
2985
  continue;
2667
2986
  }
2668
- const installedKey = Object.keys(installed).find((key) => {
2669
- return normalizeInstalledKey(key) === normalizeInstalledKey(item.name);
2670
- });
2671
- if (!installedKey) {
2987
+ if (!isInstalled(installed, item.name)) {
2672
2988
  notTracked.push(name);
2673
2989
  console.log(`Component "${name}" is not tracked as installed.`);
2674
2990
  continue;
@@ -2681,10 +2997,11 @@ var runUninstall = async (args2) => {
2681
2997
  }
2682
2998
  return;
2683
2999
  }
2684
- const remainingInstalled = { ...installed };
2685
- for (const item of resolvedTargets) {
2686
- removeInstalledKey(remainingInstalled, item.name);
2687
- }
3000
+ const remainingInstalled = installed.filter((entry) => {
3001
+ return !resolvedTargets.some((item) => {
3002
+ return findInstalledKey([entry], item.name) !== void 0;
3003
+ });
3004
+ });
2688
3005
  const allItems = await getRegistryItems({ fallback: !noFallback });
2689
3006
  const orphanItems = withDeps ? findOrphanInstalledItems(
2690
3007
  resolvedTargets.map((item) => item.name),
@@ -2706,9 +3023,7 @@ var runUninstall = async (args2) => {
2706
3023
  console.log("Dry run: no files will be removed.\n");
2707
3024
  console.log("Components:");
2708
3025
  for (const item of resolvedTargets) {
2709
- console.log(
2710
- `- ${item.canonicalName} v${installed[item.name] ?? "unknown"}`
2711
- );
3026
+ console.log(`- ${item.canonicalName}`);
2712
3027
  }
2713
3028
  if (withDeps && orphanItems.length) {
2714
3029
  console.log("\nOrphan registry items (--with-deps):");
@@ -2724,10 +3039,12 @@ var runUninstall = async (args2) => {
2724
3039
  console.log(`- ${item.canonicalName}`);
2725
3040
  }
2726
3041
  }
2727
- const postOrphanRemaining = { ...remainingInstalled };
2728
- for (const item of withDeps ? orphanItems : []) {
2729
- removeInstalledKey(postOrphanRemaining, item.name);
2730
- }
3042
+ const postOrphanRemaining = remainingInstalled.filter((entry) => {
3043
+ if (!withDeps) {
3044
+ return true;
3045
+ }
3046
+ return !orphanItems.some((item) => isInstalled([entry], item.name));
3047
+ });
2731
3048
  const dryRunOrphans = collectOrphanedSharedResources(
2732
3049
  allRemovalTargets,
2733
3050
  await resolveInstalledItems(postOrphanRemaining)
@@ -2761,11 +3078,11 @@ var runUninstall = async (args2) => {
2761
3078
  }
2762
3079
  console.log("");
2763
3080
  }
2764
- const postUninstallInstalled = { ...installed };
3081
+ let updatedInstalled = [...installed];
2765
3082
  for (const item of successfullyUninstalled) {
2766
- removeInstalledKey(postUninstallInstalled, item.name);
3083
+ updatedInstalled = removeInstalled(updatedInstalled, item.name);
2767
3084
  }
2768
- const remainingItems = await resolveInstalledItems(postUninstallInstalled);
3085
+ const remainingItems = await resolveInstalledItems(updatedInstalled);
2769
3086
  const orphanedResources = collectOrphanedSharedResources(
2770
3087
  successfullyUninstalled,
2771
3088
  remainingItems
@@ -2785,12 +3102,6 @@ var runUninstall = async (args2) => {
2785
3102
  }
2786
3103
  const utilitiesResult = await uninstallUtilities(resolvedUtilities, config);
2787
3104
  const stylesResult = await uninstallStyles(resolvedStyles, config);
2788
- const updatedInstalled = {
2789
- ...installed
2790
- };
2791
- for (const item of successfullyUninstalled) {
2792
- removeInstalledKey(updatedInstalled, item.name);
2793
- }
2794
3105
  await saveConfig({
2795
3106
  ...config,
2796
3107
  installed: updatedInstalled
@@ -2810,172 +3121,18 @@ var runUninstall = async (args2) => {
2810
3121
  }
2811
3122
  };
2812
3123
 
2813
- // src/utils/version.ts
2814
- var compareVersions = (a, b) => {
2815
- const pa = a.split(".").map(Number);
2816
- const pb = b.split(".").map(Number);
2817
- const length = Math.max(pa.length, pb.length);
2818
- for (let i = 0; i < length; i++) {
2819
- const va = pa[i] ?? 0;
2820
- const vb = pb[i] ?? 0;
2821
- if (va > vb) return 1;
2822
- if (va < vb) return -1;
2823
- }
2824
- return 0;
2825
- };
2826
- var isUpdateAvailable = (installed, latest) => {
2827
- return compareVersions(latest, installed) === 1;
2828
- };
2829
-
2830
- // src/install/update-engine.ts
2831
- var checkItemFiles = async (name) => {
2832
- const item = await findItem(name);
2833
- const config = await loadConfig();
2834
- if (!item) {
2835
- console.log(`Component "${name}" no longer exists in the registry.`);
2836
- return;
2837
- }
2838
- const installTarget = resolveItemInstallTarget(config, item);
2839
- console.log(`File check for ${item.canonicalName}:`);
2840
- for (const file of item.files) {
2841
- const fileName = file.split("/").at(-1);
2842
- if (!fileName) {
2843
- console.log(`- invalid registry file path: ${file}`);
2844
- continue;
2845
- }
2846
- const targetPath = join(getCwd(), installTarget, fileName);
2847
- if (!await fileExists(targetPath)) {
2848
- console.log(`- missing: ${targetPath}`);
2849
- continue;
2850
- }
2851
- const preparedContent = prepareInstalledFileContent(
2852
- await readFile(getRegistryTemplatePath(file), "utf-8"),
2853
- item
2854
- );
2855
- const targetContent = await readFile(targetPath, "utf-8");
2856
- if (hashesAreEqual(preparedContent, targetContent)) {
2857
- console.log(`- identical: ${targetPath}`);
2858
- continue;
2859
- }
2860
- console.log(`- conflict: ${targetPath}`);
2861
- }
2862
- };
2863
- var applySafeItemUpdate = async (name, force) => {
2864
- const item = await findItem(name);
2865
- const config = await loadConfig();
2866
- if (!item) {
2867
- console.log(`Component "${name}" no longer exists in the registry.`);
2868
- return false;
2869
- }
2870
- const installTarget = resolveItemInstallTarget(config, item);
2871
- let hasConflict = false;
2872
- console.log(`Applying update for ${item.canonicalName}:`);
2873
- for (const file of item.files) {
2874
- const sourcePath = getRegistryTemplatePath(file);
2875
- const fileName = file.split("/").at(-1);
2876
- if (!fileName) {
2877
- console.log(`- invalid registry file path: ${file}`);
2878
- hasConflict = true;
2879
- continue;
2880
- }
2881
- const targetPath = join(getCwd(), installTarget, fileName);
2882
- const preparedContent = prepareInstalledFileContent(
2883
- await readFile(sourcePath, "utf-8"),
2884
- item
2885
- );
2886
- await mkdir(dirname(targetPath), { recursive: true });
2887
- if (!await fileExists(targetPath)) {
2888
- await writeFile(targetPath, preparedContent, "utf-8");
2889
- console.log(`- restored missing file: ${targetPath}`);
2890
- continue;
2891
- }
2892
- const targetContent = await readFile(targetPath, "utf-8");
2893
- if (hashesAreEqual(preparedContent, targetContent)) {
2894
- console.log(`- identical: ${targetPath}`);
2895
- continue;
2896
- }
2897
- if (force) {
2898
- const backupPath = await createBackupFile(targetPath);
2899
- if (backupPath) {
2900
- console.log(`- backup created: ${backupPath}`);
2901
- }
2902
- await writeFile(targetPath, preparedContent, "utf-8");
2903
- console.log(`- force updated file: ${targetPath}`);
2904
- continue;
2905
- }
2906
- hasConflict = true;
2907
- console.log(`- conflict (user modified): ${targetPath}`);
2908
- }
2909
- if (hasConflict) {
2910
- console.log(
2911
- "Update finished with conflicts. Installed version was not changed."
2912
- );
2913
- return false;
2914
- }
2915
- console.log(
2916
- `\u2714 ${item.canonicalName} updated successfully to v${item.version}`
2917
- );
2918
- return true;
2919
- };
2920
- var checkItemUpdate = async (name, installedVersion, dryRun, force, sync = false) => {
2921
- const item = await findItem(name);
2922
- if (!item) {
2923
- console.log(`Component "${name}" no longer exists in the registry.`);
2924
- return false;
2925
- }
2926
- const versionUpdateAvailable = isUpdateAvailable(
2927
- installedVersion,
2928
- item.version
2929
- );
2930
- if (!sync && !versionUpdateAvailable) {
2931
- console.log(`${item.canonicalName} is up to date (v${installedVersion}).`);
2932
- return false;
2933
- }
2934
- if (sync && !versionUpdateAvailable) {
2935
- console.log(
2936
- `${item.canonicalName} template sync (installed v${installedVersion}, registry v${item.version})`
2937
- );
2938
- } else {
2939
- console.log(
2940
- `${item.canonicalName} can be updated: v${installedVersion} \u2192 v${item.version}`
2941
- );
2942
- }
2943
- if (dryRun) {
2944
- console.log("\nChanged file candidates:");
2945
- for (const file of item.files) {
2946
- console.log(`~ ${file}`);
2947
- }
2948
- console.log("\nDry run: no files will be changed.");
2949
- console.log("Update plan:");
2950
- if (force) {
2951
- console.log(
2952
- "- Force mode requested: conflicted files require backup before overwrite"
2953
- );
2954
- console.log(
2955
- "- Dry run: backups would be created before forced overwrites"
2956
- );
2957
- }
2958
- console.log(`- Check installed files for ${item.canonicalName}`);
2959
- console.log("- Compare existing files with registry templates");
2960
- console.log("- Report conflicts before writing changes");
2961
- console.log("- Never overwrite user-modified files silently");
2962
- await checkItemFiles(name);
2963
- return false;
2964
- }
2965
- return await applySafeItemUpdate(name, force);
2966
- };
2967
-
2968
3124
  // src/commands/update.ts
2969
3125
  var styleUpdateNames = ["theme"];
2970
- var resolveInstalledKey = async (name, installed) => {
2971
- if (installed[name]) {
2972
- return name;
3126
+ var resolveInstalledKey2 = async (name, installed) => {
3127
+ const direct = findInstalledKey(installed, name);
3128
+ if (direct) {
3129
+ return direct;
2973
3130
  }
2974
3131
  const item = await findItem(name);
2975
3132
  if (!item) {
2976
3133
  return void 0;
2977
3134
  }
2978
- return installed[item.name] ? item.name : void 0;
3135
+ return findInstalledKey(installed, item.name);
2979
3136
  };
2980
3137
  var runStylesUpdate = async (config, dryRun) => {
2981
3138
  const styles = resolveRegistryStyles(styleUpdateNames);
@@ -3002,9 +3159,7 @@ Tailwind CSS entrypoint: ${config.tailwind.css}`);
3002
3159
  }
3003
3160
  };
3004
3161
  var runUtilitiesUpdate = async (config, installed, dryRun, force) => {
3005
- const installedItems = (await Promise.all(
3006
- Object.keys(installed).map(async (name) => findItem(name))
3007
- )).filter((item) => Boolean(item));
3162
+ const installedItems = (await Promise.all(installed.map(async (name) => findItem(name)))).filter((item) => Boolean(item));
3008
3163
  const utilityNames = collectUtilities(installedItems);
3009
3164
  if (!utilityNames.length) {
3010
3165
  console.log("No shared utilities are tracked for installed components.");
@@ -3029,26 +3184,18 @@ var runUtilitiesUpdate = async (config, installed, dryRun, force) => {
3029
3184
  );
3030
3185
  }
3031
3186
  };
3032
- var runComponentUpdates = async (config, installed, targetNames, dryRun, force, sync) => {
3033
- let changed = false;
3034
- if (!Object.keys(installed).length) {
3187
+ var runComponentUpdates = async (installed, targetNames, dryRun, force, sync) => {
3188
+ if (!installed.length) {
3035
3189
  console.log("No Lexsys components are currently tracked.");
3036
- return false;
3190
+ return;
3037
3191
  }
3038
3192
  console.log("Checking installed Lexsys components:\n");
3039
3193
  for (const name of targetNames) {
3040
- const version = installed[name];
3041
- if (!version) {
3194
+ if (!isInstalled(installed, name)) {
3042
3195
  continue;
3043
3196
  }
3044
- const didUpdate = await checkItemUpdate(name, version, dryRun, force, sync);
3045
- const item = await findItem(name);
3046
- if (didUpdate && item) {
3047
- installed[name] = item.version;
3048
- changed = true;
3049
- }
3197
+ await checkItemUpdate(name, dryRun, force, sync);
3050
3198
  }
3051
- return changed;
3052
3199
  };
3053
3200
  var runUpdate = async (args2) => {
3054
3201
  const dryRun = hasFlag(args2, "--dry-run", "-d");
@@ -3076,7 +3223,7 @@ var runUpdate = async (args2) => {
3076
3223
  "-a"
3077
3224
  ]);
3078
3225
  const config = await loadConfig();
3079
- const installed = { ...config.installed ?? {} };
3226
+ const installed = [...config.installed ?? []];
3080
3227
  try {
3081
3228
  await getRegistryProviderResult({
3082
3229
  fallback: !noFallback
@@ -3088,19 +3235,18 @@ var runUpdate = async (args2) => {
3088
3235
  return;
3089
3236
  }
3090
3237
  if (!updateAll && !stylesFlag && !utilitiesFlag && targetArgs.length === 0) {
3091
- const installedNames = Object.keys(installed);
3092
- if (installedNames.length === 0) {
3238
+ if (installed.length === 0) {
3093
3239
  console.log(
3094
3240
  "No components installed. Run `lexsys add <component>` first."
3095
3241
  );
3096
3242
  return;
3097
3243
  }
3098
3244
  if (yes) {
3099
- targetArgs.push(...installedNames);
3245
+ targetArgs.push(...installed);
3100
3246
  } else {
3101
3247
  const selected = await promptMultiselect(
3102
3248
  "Select components to update",
3103
- installedNames.map((name) => ({ title: name, value: name })),
3249
+ installed.map((name) => ({ title: name, value: name })),
3104
3250
  { min: 1 }
3105
3251
  );
3106
3252
  if (!selected.length) return;
@@ -3127,42 +3273,18 @@ var runUpdate = async (args2) => {
3127
3273
  if (!shouldUpdateComponents) {
3128
3274
  return;
3129
3275
  }
3130
- let changed = false;
3131
3276
  if (updateAll) {
3132
- changed = await runComponentUpdates(
3133
- config,
3134
- installed,
3135
- Object.keys(installed),
3136
- dryRun,
3137
- force,
3138
- sync
3139
- );
3277
+ await runComponentUpdates(installed, installed, dryRun, force, sync);
3140
3278
  } else {
3141
3279
  for (const name of targetArgs) {
3142
- const installedKey = await resolveInstalledKey(name, installed);
3280
+ const installedKey = await resolveInstalledKey2(name, installed);
3143
3281
  if (!installedKey) {
3144
3282
  console.log(`Component "${name}" is not tracked as installed.`);
3145
3283
  continue;
3146
3284
  }
3147
- const didUpdateOne = await runComponentUpdates(
3148
- config,
3149
- installed,
3150
- [installedKey],
3151
- dryRun,
3152
- force,
3153
- sync
3154
- );
3155
- if (didUpdateOne) {
3156
- changed = true;
3157
- }
3285
+ await runComponentUpdates(installed, [installedKey], dryRun, force, sync);
3158
3286
  }
3159
3287
  }
3160
- if (changed) {
3161
- await saveConfig({
3162
- ...config,
3163
- installed
3164
- });
3165
- }
3166
3288
  };
3167
3289
  var cliDistDir = dirname(fileURLToPath(import.meta.url));
3168
3290
  var packageJsonPath = join(cliDistDir, "..", "package.json");
@@ -3269,6 +3391,14 @@ try {
3269
3391
  });
3270
3392
  process.exit(0);
3271
3393
  }
3394
+ if (command === "reset") {
3395
+ if (hasFlag(args, "--help", "-h")) {
3396
+ runHelpFor("reset");
3397
+ process.exit(0);
3398
+ }
3399
+ await runReset(args);
3400
+ process.exit(0);
3401
+ }
3272
3402
  if (command === "uninstall" || command === "rm") {
3273
3403
  if (hasFlag(args, "--help", "-h")) {
3274
3404
  runHelpFor("uninstall");
@@ -0,0 +1,4 @@
1
+ import type { RegistryItem } from "@dalexto/lexsys-registry";
2
+ export type ComponentDriftStatus = "missing" | "in-sync" | "drift";
3
+ export declare const getComponentDriftStatus: (name: string) => Promise<ComponentDriftStatus>;
4
+ export declare const itemHasTemplateDrift: (item: RegistryItem) => Promise<boolean>;
@@ -1,2 +1,4 @@
1
1
  export declare const checkItemFiles: (name: string) => Promise<void>;
2
- export declare const checkItemUpdate: (name: string, installedVersion: string, dryRun: boolean, force: boolean, sync?: boolean) => Promise<boolean>;
2
+ export declare const applyItemOverwrite: (name: string, force: boolean) => Promise<boolean>;
3
+ export declare const checkItemUpdate: (name: string, dryRun: boolean, force: boolean, sync?: boolean) => Promise<boolean>;
4
+ export declare const resetItem: (name: string, dryRun: boolean) => Promise<boolean>;
@@ -1,3 +1,3 @@
1
1
  import type { RegistryItem } from "@dalexto/lexsys-registry";
2
2
  export declare const computeRegistryClosure: (rootNames: string[], items: RegistryItem[]) => Set<string>;
3
- export declare const findOrphanInstalledItems: (removedTargetNames: string[], remainingInstalled: Record<string, string>, items: RegistryItem[]) => RegistryItem[];
3
+ export declare const findOrphanInstalledItems: (removedTargetNames: string[], remainingInstalled: string[], items: RegistryItem[]) => RegistryItem[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalexto/lexsys-cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Registry-first CLI that installs Lexsys React UI components into your project",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "prompts": "^2.4.2",
35
- "@dalexto/lexsys-registry": "0.0.3"
35
+ "@dalexto/lexsys-registry": "0.0.4"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.9.1",
@@ -1,2 +0,0 @@
1
- export declare const compareVersions: (a: string, b: string) => number;
2
- export declare const isUpdateAvailable: (installed: string, latest: string) => boolean;