inai-react-components 0.1.7 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +80 -0
  2. package/dist/commands/add.d.ts +38 -5
  3. package/dist/commands/add.d.ts.map +1 -1
  4. package/dist/commands/add.js +391 -80
  5. package/dist/commands/diff.d.ts +6 -0
  6. package/dist/commands/diff.d.ts.map +1 -1
  7. package/dist/commands/diff.js +92 -20
  8. package/dist/commands/init.d.ts +6 -3
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +33 -36
  11. package/dist/commands/mcp.d.ts +123 -0
  12. package/dist/commands/mcp.d.ts.map +1 -0
  13. package/dist/commands/mcp.js +289 -0
  14. package/dist/commands/migrate.d.ts +41 -0
  15. package/dist/commands/migrate.d.ts.map +1 -0
  16. package/dist/commands/migrate.js +139 -0
  17. package/dist/commands/registries.d.ts +46 -0
  18. package/dist/commands/registries.d.ts.map +1 -0
  19. package/dist/commands/registries.js +152 -0
  20. package/dist/commands/remove.d.ts +11 -0
  21. package/dist/commands/remove.d.ts.map +1 -0
  22. package/dist/commands/remove.js +170 -0
  23. package/dist/commands/schema.d.ts +16 -0
  24. package/dist/commands/schema.d.ts.map +1 -0
  25. package/dist/commands/schema.js +32 -0
  26. package/dist/commands/status.d.ts +32 -3
  27. package/dist/commands/status.d.ts.map +1 -1
  28. package/dist/commands/status.js +148 -25
  29. package/dist/commands/theme.d.ts.map +1 -1
  30. package/dist/commands/theme.js +3 -37
  31. package/dist/commands/update.d.ts +18 -1
  32. package/dist/commands/update.d.ts.map +1 -1
  33. package/dist/commands/update.js +195 -5
  34. package/dist/index.js +76 -5
  35. package/dist/schemas/registry-config.schema.json +62 -0
  36. package/dist/schemas/registry-item.schema.json +63 -0
  37. package/dist/schemas/registry.schema.json +15 -0
  38. package/dist/types/registry.d.ts +50 -0
  39. package/dist/types/registry.d.ts.map +1 -0
  40. package/dist/types/registry.js +10 -0
  41. package/dist/utils/auto-install.d.ts +11 -0
  42. package/dist/utils/auto-install.d.ts.map +1 -0
  43. package/dist/utils/auto-install.js +29 -0
  44. package/dist/utils/framework-detect.d.ts +15 -0
  45. package/dist/utils/framework-detect.d.ts.map +1 -0
  46. package/dist/utils/framework-detect.js +90 -0
  47. package/dist/utils/fuzzy-search.d.ts +16 -0
  48. package/dist/utils/fuzzy-search.d.ts.map +1 -0
  49. package/dist/utils/fuzzy-search.js +67 -0
  50. package/dist/utils/registry-config.d.ts +27 -0
  51. package/dist/utils/registry-config.d.ts.map +1 -0
  52. package/dist/utils/registry-config.js +94 -0
  53. package/dist/utils/registry-resolver.d.ts +26 -0
  54. package/dist/utils/registry-resolver.d.ts.map +1 -1
  55. package/dist/utils/registry-resolver.js +134 -10
  56. package/dist/utils/snapshot.d.ts +20 -0
  57. package/dist/utils/snapshot.d.ts.map +1 -0
  58. package/dist/utils/snapshot.js +32 -0
  59. package/dist/utils/themes.d.ts +17 -0
  60. package/dist/utils/themes.d.ts.map +1 -0
  61. package/dist/utils/themes.js +49 -0
  62. package/package.json +9 -3
@@ -4,29 +4,49 @@ import chalk from "chalk";
4
4
  import ora from "ora";
5
5
  import prompts from "prompts";
6
6
  import { readComponentsJson, readRegistryJson, } from "./status.js";
7
- import { resolveRegistryDir } from "../utils/registry-resolver.js";
7
+ import { resolveRegistryDir, resolveRegistry } from "../utils/registry-resolver.js";
8
+ import { loadRegistries, getDefaultRegistry, getRegistryByName } from "../utils/registry-config.js";
9
+ import { saveSnapshot, sha256, } from "../utils/snapshot.js";
10
+ import { fuzzySearchNames } from "../utils/fuzzy-search.js";
8
11
  import { normalizeThemeCss, getLocalTailwindCssTemplate, getLegacyTailwindCssTemplate, } from "./init.js";
12
+ import { autoInstallDeps } from "../utils/auto-install.js";
9
13
  /**
10
- * Parse a component spec like "button", "block/auth-login", or "template/admin-dashboard"
11
- * into its type prefix and name.
14
+ * Parse a component spec into its registry namespace, type prefix and name.
15
+ *
16
+ * Supported forms:
17
+ * - `button` → { registry: null, prefix: null, name: "button" }
18
+ * - `block/auth-login` → { registry: null, prefix: "block", name: "auth-login" }
19
+ * - `template/admin-dashboard` → { registry: null, prefix: "template", name: "admin-dashboard" }
20
+ * - `@acme/custom-button` → { registry: "acme", prefix: null, name: "custom-button" }
21
+ * - `@acme/block/hero` → { registry: "acme", prefix: "block", name: "hero" }
22
+ *
23
+ * `registry` is null when the caller did not specify a `@namespace/` — the
24
+ * default registry (from `components.json`) should then be used.
12
25
  */
13
26
  export function parseComponentSpec(spec) {
14
27
  const trimmed = spec.trim();
15
28
  if (!trimmed) {
16
29
  throw new Error('Component spec cannot be empty.');
17
30
  }
31
+ let registry = null;
32
+ let rest = trimmed;
33
+ const nsMatch = trimmed.match(/^@([a-z0-9][a-z0-9-]*)\/(.+)$/i);
34
+ if (nsMatch) {
35
+ registry = nsMatch[1];
36
+ rest = nsMatch[2];
37
+ }
18
38
  let prefix = null;
19
39
  let name;
20
- if (trimmed.startsWith("block/")) {
40
+ if (rest.startsWith("block/")) {
21
41
  prefix = "block";
22
- name = trimmed.slice(6).trim();
42
+ name = rest.slice(6).trim();
23
43
  }
24
- else if (trimmed.startsWith("template/")) {
44
+ else if (rest.startsWith("template/")) {
25
45
  prefix = "template";
26
- name = trimmed.slice(9).trim();
46
+ name = rest.slice(9).trim();
27
47
  }
28
48
  else {
29
- name = trimmed;
49
+ name = rest;
30
50
  }
31
51
  if (!name) {
32
52
  throw new Error(`Invalid component spec "${spec}" — name cannot be empty.`);
@@ -34,7 +54,7 @@ export function parseComponentSpec(spec) {
34
54
  if (!/^[a-z0-9][a-z0-9\-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
35
55
  throw new Error(`Invalid component name "${name}" — use only lowercase letters, numbers, and hyphens (e.g. "my-component").`);
36
56
  }
37
- return { prefix, name };
57
+ return { registry, prefix, name };
38
58
  }
39
59
  /**
40
60
  * Find a component in the registry, optionally narrowing by type prefix.
@@ -74,7 +94,130 @@ export function getTargetDir(component, aliases) {
74
94
  // Components, forms, and other types go to the components directory
75
95
  return aliases.components.replace(/^@\//, "src/");
76
96
  }
77
- export async function runAdd(componentSpec, rootDir) {
97
+ /**
98
+ * Install a single component (no dep resolution). Internal helper used by
99
+ * `runAdd` after deps have been resolved. Returns the list of files that
100
+ * were written (or would be written, in dry-run mode) and files skipped.
101
+ */
102
+ async function installOne(component, registry, componentsJson, registryDir, rootDir, options) {
103
+ const name = component.name;
104
+ const baseTargetDir = getTargetDir(component, componentsJson.aliases);
105
+ const isMultiFile = component.type === "block" || component.type === "template";
106
+ const targetDir = isMultiFile ? path.join(baseTargetDir, name) : baseTargetDir;
107
+ const copiedFiles = [];
108
+ const skippedFiles = [];
109
+ // Track captured content for the snapshot (only for files that were
110
+ // actually written — skipped ones retain whatever the user had).
111
+ const capturedFiles = {};
112
+ for (const filePath of component.files) {
113
+ const registryFilePath = path.join(registryDir, filePath);
114
+ const fileName = path.basename(filePath);
115
+ const localFilePath = path.join(rootDir, targetDir, fileName);
116
+ if (!fs.existsSync(registryFilePath)) {
117
+ continue;
118
+ }
119
+ const exists = fs.existsSync(localFilePath);
120
+ if (exists && !options.force) {
121
+ if (options.skipExisting) {
122
+ skippedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
123
+ continue;
124
+ }
125
+ if (options.dryRun) {
126
+ // In dry-run we do not prompt; treat as would-overwrite.
127
+ }
128
+ else if (process.stdin.isTTY) {
129
+ const res = await prompts({
130
+ type: "confirm",
131
+ name: "overwrite",
132
+ message: `File "${path.relative(rootDir, localFilePath)}" already exists. Overwrite?`,
133
+ initial: false,
134
+ });
135
+ if (!res.overwrite) {
136
+ skippedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
137
+ continue;
138
+ }
139
+ }
140
+ else {
141
+ throw new Error(`File "${path.relative(rootDir, localFilePath)}" already exists. Use --force to overwrite or --skip-existing to ignore (stdin is not a TTY, cannot prompt).`);
142
+ }
143
+ }
144
+ const content = fs.readFileSync(registryFilePath, "utf-8");
145
+ if (options.dryRun) {
146
+ console.log(chalk.dim(` [dry-run] would write ${path.relative(rootDir, localFilePath)}`));
147
+ }
148
+ else {
149
+ fs.mkdirSync(path.dirname(localFilePath), { recursive: true });
150
+ fs.writeFileSync(localFilePath, content);
151
+ const relLocal = path.relative(rootDir, localFilePath);
152
+ capturedFiles[relLocal] = {
153
+ localPath: relLocal,
154
+ hash: sha256(content),
155
+ content,
156
+ };
157
+ }
158
+ copiedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
159
+ }
160
+ // Internal deps (lib/cn, lib/animations, ...)
161
+ const copiedInternalDeps = [];
162
+ for (const dep of component.internalDeps) {
163
+ const depFileName = dep.split("/").pop() + ".ts";
164
+ const depSourcePath = path.join(registryDir, "packages", "ui", "src", dep + ".ts");
165
+ const depTargetPath = path.join(rootDir, "src", "lib", depFileName);
166
+ if (!fs.existsSync(depSourcePath)) {
167
+ continue;
168
+ }
169
+ const content = fs.readFileSync(depSourcePath, "utf-8");
170
+ if (options.dryRun) {
171
+ console.log(chalk.dim(` [dry-run] would write ${path.relative(rootDir, depTargetPath)}`));
172
+ }
173
+ else {
174
+ fs.mkdirSync(path.dirname(depTargetPath), { recursive: true });
175
+ fs.writeFileSync(depTargetPath, content);
176
+ }
177
+ copiedInternalDeps.push(dep);
178
+ }
179
+ // Update components.json (skipped in dry-run).
180
+ if (!options.dryRun) {
181
+ const registryVersion = registry.version;
182
+ const installed = componentsJson.installedComponents ?? [];
183
+ const existingIdx = installed.findIndex((c) => c.name === name);
184
+ const entry = {
185
+ name,
186
+ version: registryVersion,
187
+ installedAt: new Date().toISOString().split("T")[0],
188
+ };
189
+ if (existingIdx >= 0) {
190
+ installed[existingIdx] = entry;
191
+ }
192
+ else {
193
+ installed.push(entry);
194
+ }
195
+ componentsJson.installedComponents = installed;
196
+ const componentsJsonPath = path.join(rootDir, "components.json");
197
+ fs.writeFileSync(componentsJsonPath, JSON.stringify(componentsJson, null, 2) + "\n");
198
+ // Persist the snapshot of the original content so `update` can run a
199
+ // real 3-way merge against the user's local edits and `status` can
200
+ // detect drift vs the originally-installed content.
201
+ if (Object.keys(capturedFiles).length > 0) {
202
+ const snapshot = {
203
+ name,
204
+ version: registryVersion,
205
+ files: capturedFiles,
206
+ capturedAt: new Date().toISOString(),
207
+ };
208
+ saveSnapshot(rootDir, snapshot);
209
+ }
210
+ }
211
+ return {
212
+ added: true,
213
+ message: `Component "${name}" added successfully.`,
214
+ files: copiedFiles,
215
+ npmDeps: component.npmDeps,
216
+ internalDeps: copiedInternalDeps,
217
+ skipped: skippedFiles,
218
+ };
219
+ }
220
+ export async function runAdd(componentSpec, rootDir, options = {}) {
78
221
  const componentsJson = readComponentsJson(rootDir);
79
222
  if (!componentsJson) {
80
223
  return {
@@ -85,8 +228,69 @@ export async function runAdd(componentSpec, rootDir) {
85
228
  internalDeps: [],
86
229
  };
87
230
  }
88
- const { prefix, name } = parseComponentSpec(componentSpec);
89
- const registryDir = resolveRegistryDir(rootDir);
231
+ const { registry: nsRegistry, prefix, name } = parseComponentSpec(componentSpec);
232
+ // Multi-registry (M3) resolution: if components.json declares a
233
+ // `registries` array, honour the namespace. Otherwise fall back to
234
+ // the legacy single-registry code path so existing projects keep
235
+ // working unchanged.
236
+ const registries = await loadRegistries(rootDir);
237
+ let registryDir;
238
+ let activeRegistryName = null;
239
+ if (registries.length > 0) {
240
+ let target;
241
+ if (nsRegistry) {
242
+ target = getRegistryByName(registries, nsRegistry);
243
+ if (!target) {
244
+ return {
245
+ added: false,
246
+ message: `Registry "@${nsRegistry}" is not configured in components.json. Available: ${registries.map((r) => `@${r.name}`).join(", ")}.`,
247
+ files: [],
248
+ npmDeps: [],
249
+ internalDeps: [],
250
+ };
251
+ }
252
+ }
253
+ else {
254
+ target = getDefaultRegistry(registries);
255
+ if (!target) {
256
+ return {
257
+ added: false,
258
+ message: `No default registry configured. Run \`inai-ui registries add <name>\` first.`,
259
+ files: [],
260
+ npmDeps: [],
261
+ internalDeps: [],
262
+ };
263
+ }
264
+ }
265
+ try {
266
+ registryDir = await resolveRegistry(target, rootDir, {
267
+ offline: options.offline,
268
+ });
269
+ activeRegistryName = target.name;
270
+ }
271
+ catch (err) {
272
+ return {
273
+ added: false,
274
+ message: err instanceof Error ? err.message : String(err),
275
+ files: [],
276
+ npmDeps: [],
277
+ internalDeps: [],
278
+ };
279
+ }
280
+ }
281
+ else {
282
+ if (nsRegistry) {
283
+ return {
284
+ added: false,
285
+ message: `No registries configured in components.json — cannot resolve "@${nsRegistry}/${name}".`,
286
+ files: [],
287
+ npmDeps: [],
288
+ internalDeps: [],
289
+ };
290
+ }
291
+ registryDir = resolveRegistryDir(rootDir);
292
+ }
293
+ void activeRegistryName;
90
294
  const registryPath = path.join(registryDir, "registry.json");
91
295
  const registry = readRegistryJson(registryPath);
92
296
  if (!registry) {
@@ -101,91 +305,116 @@ export async function runAdd(componentSpec, rootDir) {
101
305
  const component = findInRegistry(registry, name, prefix);
102
306
  if (!component) {
103
307
  const label = prefix ? `${prefix}/${name}` : name;
308
+ // Gather all registry names for fuzzy matching
309
+ const allNames = [
310
+ ...registry.components.map((c) => c.name),
311
+ ...registry.blocks.map((c) => c.name),
312
+ ...registry.templates.map((c) => c.name),
313
+ ];
314
+ const suggestions = fuzzySearchNames(name, allNames, 0.4).slice(0, 5);
104
315
  return {
105
316
  added: false,
106
317
  message: `Component "${label}" not found in registry.`,
107
318
  files: [],
108
319
  npmDeps: [],
109
320
  internalDeps: [],
321
+ suggestions,
110
322
  };
111
323
  }
112
- // Determine target directory
113
- const baseTargetDir = getTargetDir(component, componentsJson.aliases);
114
- const isMultiFile = component.type === "block" || component.type === "template";
115
- // Blocks/templates go into a named subdirectory (e.g. src/components/blocks/auth-login/)
116
- const targetDir = isMultiFile
117
- ? path.join(baseTargetDir, name)
118
- : baseTargetDir;
119
- const copiedFiles = [];
120
- // Copy component source files
121
- for (const filePath of component.files) {
122
- const registryFilePath = path.join(registryDir, filePath);
123
- const fileName = path.basename(filePath);
124
- const localFilePath = path.join(rootDir, targetDir, fileName);
125
- if (!fs.existsSync(registryFilePath)) {
126
- continue;
324
+ // Resolve the dependency graph in topological order (deps first).
325
+ const installed = componentsJson.installedComponents ?? [];
326
+ const alreadyInstalled = new Set(installed.map((c) => c.name));
327
+ const resolved = new Set();
328
+ const installOrder = [];
329
+ const visited = new Set();
330
+ const visit = (comp, stack) => {
331
+ if (resolved.has(comp.name))
332
+ return;
333
+ if (stack.has(comp.name)) {
334
+ console.log(chalk.yellow(` ⚠ circular dependency detected: "${comp.name}" already in resolution stack [${[...stack].join(" → ")} → ${comp.name}], skipping`));
335
+ return;
127
336
  }
128
- fs.mkdirSync(path.dirname(localFilePath), { recursive: true });
129
- const content = fs.readFileSync(registryFilePath, "utf-8");
130
- fs.writeFileSync(localFilePath, content);
131
- copiedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
132
- }
133
- // Resolve and copy internal dependencies
134
- const copiedInternalDeps = [];
135
- for (const dep of component.internalDeps) {
136
- // dep is like "lib/cn" or "lib/animations"
137
- const depFileName = dep.split("/").pop() + ".ts";
138
- const depSourcePath = path.join(registryDir, "packages", "ui", "src", dep + ".ts");
139
- const depTargetPath = path.join(rootDir, "src", "lib", depFileName);
140
- if (!fs.existsSync(depSourcePath)) {
141
- continue;
337
+ if (visited.has(comp.name))
338
+ return;
339
+ visited.add(comp.name);
340
+ stack.add(comp.name);
341
+ const deps = comp.registryDependencies ?? [];
342
+ for (const depName of deps) {
343
+ if (alreadyInstalled.has(depName)) {
344
+ // already present — skip silently (still mark resolved so we don't recheck)
345
+ resolved.add(depName);
346
+ continue;
347
+ }
348
+ const depComp = findInRegistry(registry, depName, null);
349
+ if (!depComp) {
350
+ // Missing dep in the registry — warn but continue.
351
+ console.log(chalk.yellow(` ! registry dependency "${depName}" of "${comp.name}" not found in registry, skipping`));
352
+ continue;
353
+ }
354
+ visit(depComp, stack);
142
355
  }
143
- // Copy even if it already exists (ensure latest version)
144
- fs.mkdirSync(path.dirname(depTargetPath), { recursive: true });
145
- const content = fs.readFileSync(depSourcePath, "utf-8");
146
- fs.writeFileSync(depTargetPath, content);
147
- copiedInternalDeps.push(dep);
148
- }
149
- // Update components.json with installed component entry
150
- const registryVersion = registry.version;
151
- const installed = componentsJson.installedComponents ?? [];
152
- const existingIdx = installed.findIndex((c) => c.name === name);
153
- const entry = {
154
- name,
155
- version: registryVersion,
156
- installedAt: new Date().toISOString().split("T")[0],
356
+ stack.delete(comp.name);
357
+ resolved.add(comp.name);
358
+ installOrder.push(comp);
157
359
  };
158
- if (existingIdx >= 0) {
159
- installed[existingIdx] = entry;
160
- }
161
- else {
162
- installed.push(entry);
163
- }
164
- componentsJson.installedComponents = installed;
165
- const componentsJsonPath = path.join(rootDir, "components.json");
166
- fs.writeFileSync(componentsJsonPath, JSON.stringify(componentsJson, null, 2) + "\n");
360
+ visit(component, new Set());
361
+ const allFiles = [];
362
+ const allNpmDeps = new Set();
363
+ const allInternalDeps = new Set();
364
+ const allSkipped = [];
365
+ const transitiveDeps = [];
366
+ for (const comp of installOrder) {
367
+ if (comp.name !== name) {
368
+ console.log(chalk.dim(` → installing dependency: ${comp.name}`));
369
+ transitiveDeps.push(comp.name);
370
+ }
371
+ const result = await installOne(comp, registry, componentsJson, registryDir, rootDir, options);
372
+ for (const f of result.files)
373
+ allFiles.push(f);
374
+ for (const d of result.npmDeps)
375
+ allNpmDeps.add(d);
376
+ for (const d of result.internalDeps)
377
+ allInternalDeps.add(d);
378
+ for (const s of result.skipped ?? [])
379
+ allSkipped.push(s);
380
+ }
381
+ const suffix = options.dryRun ? " (dry-run)" : "";
167
382
  return {
168
383
  added: true,
169
- message: `Component "${name}" added successfully.`,
170
- files: copiedFiles,
171
- npmDeps: component.npmDeps,
172
- internalDeps: copiedInternalDeps,
384
+ message: `Component "${name}" added successfully${suffix}.`,
385
+ files: allFiles,
386
+ npmDeps: [...allNpmDeps],
387
+ internalDeps: [...allInternalDeps],
388
+ resolvedDeps: transitiveDeps,
389
+ skipped: allSkipped,
173
390
  };
174
391
  }
175
- function printAddResult(result) {
392
+ function printAddResult(result, showNpmHint = true) {
176
393
  if (result.files.length > 0) {
177
394
  console.log(chalk.green("\nCopied files:"));
178
395
  for (const file of result.files) {
179
396
  console.log(chalk.dim(` - ${file}`));
180
397
  }
181
398
  }
399
+ if (result.skipped && result.skipped.length > 0) {
400
+ console.log(chalk.yellow("\nSkipped files (already existed):"));
401
+ for (const file of result.skipped) {
402
+ console.log(chalk.dim(` - ${file}`));
403
+ }
404
+ }
405
+ if (result.resolvedDeps && result.resolvedDeps.length > 0) {
406
+ console.log(chalk.green("\nRegistry dependencies resolved:"));
407
+ for (const dep of result.resolvedDeps) {
408
+ console.log(chalk.dim(` - ${dep}`));
409
+ }
410
+ }
182
411
  if (result.internalDeps.length > 0) {
183
412
  console.log(chalk.green("\nInternal dependencies resolved:"));
184
413
  for (const dep of result.internalDeps) {
185
414
  console.log(chalk.dim(` - ${dep}`));
186
415
  }
187
416
  }
188
- if (result.npmDeps.length > 0) {
417
+ if (result.npmDeps.length > 0 && showNpmHint) {
189
418
  console.log(chalk.yellow("\nRequired npm dependencies:"));
190
419
  console.log(chalk.cyan(` pnpm add ${result.npmDeps.join(" ")}`));
191
420
  }
@@ -193,24 +422,98 @@ function printAddResult(result) {
193
422
  export async function addCommand(componentSpec, options) {
194
423
  // If --all flag, install everything
195
424
  if (options?.all) {
196
- await installAll();
425
+ await installAll(options);
197
426
  return;
198
427
  }
199
428
  // If no component specified, show interactive picker
200
429
  if (!componentSpec) {
201
- await interactiveAdd();
430
+ await interactiveAdd(options);
202
431
  return;
203
432
  }
204
433
  const spinner = ora(`Adding "${componentSpec}"...`).start();
205
434
  try {
206
435
  const rootDir = process.cwd();
207
- const result = await runAdd(componentSpec, rootDir);
436
+ // Pause spinner so prompts (for overwrite) work cleanly.
437
+ spinner.stop();
438
+ const result = await runAdd(componentSpec, rootDir, {
439
+ force: options?.force,
440
+ dryRun: options?.dryRun,
441
+ skipExisting: options?.skipExisting,
442
+ skipInstall: options?.skipInstall,
443
+ });
208
444
  if (!result.added) {
209
- spinner.fail(result.message);
445
+ console.error(chalk.red(result.message));
446
+ // Offer fuzzy suggestions if available
447
+ if (result.suggestions && result.suggestions.length > 0) {
448
+ const top = result.suggestions[0];
449
+ // Single high-confidence match: auto-suggest with confirm
450
+ if (top.score > 0.8 && process.stdin.isTTY) {
451
+ const confirm = await prompts({
452
+ type: "confirm",
453
+ name: "accept",
454
+ message: `Did you mean "${chalk.cyan(top.name)}"?`,
455
+ initial: true,
456
+ });
457
+ if (confirm.accept) {
458
+ // Re-run add with the corrected name
459
+ const correctedSpec = componentSpec.replace(/[^/]+$/, top.name);
460
+ const correctedResult = await runAdd(correctedSpec, rootDir, {
461
+ force: options?.force,
462
+ dryRun: options?.dryRun,
463
+ skipExisting: options?.skipExisting,
464
+ skipInstall: options?.skipInstall,
465
+ });
466
+ if (!correctedResult.added) {
467
+ console.error(chalk.red(correctedResult.message));
468
+ process.exit(1);
469
+ }
470
+ console.log(chalk.green(correctedResult.message));
471
+ const willAutoInstall = correctedResult.npmDeps.length > 0 &&
472
+ !options?.skipInstall &&
473
+ !options?.dryRun;
474
+ printAddResult(correctedResult, !willAutoInstall);
475
+ if (correctedResult.npmDeps.length > 0 &&
476
+ !options?.skipInstall &&
477
+ !options?.dryRun) {
478
+ const installSpinner = ora("Installing npm dependencies...").start();
479
+ try {
480
+ autoInstallDeps(rootDir, correctedResult.npmDeps);
481
+ installSpinner.succeed("npm dependencies installed");
482
+ }
483
+ catch {
484
+ installSpinner.warn("Failed to auto-install dependencies. Install them manually:");
485
+ console.log(chalk.cyan(` pnpm add ${correctedResult.npmDeps.join(" ")}`));
486
+ }
487
+ }
488
+ return;
489
+ }
490
+ }
491
+ // Show all suggestions
492
+ console.log(chalk.yellow(`\nDid you mean: ${result.suggestions
493
+ .map((s) => `"${chalk.cyan(s.name)}" ${chalk.dim(`(score: ${s.score.toFixed(2)})`)}`)
494
+ .join(", ")}?`));
495
+ }
210
496
  process.exit(1);
211
497
  }
212
- spinner.succeed(result.message);
213
- printAddResult(result);
498
+ console.log(chalk.green(result.message));
499
+ const willAutoInstall = result.npmDeps.length > 0 &&
500
+ !options?.skipInstall &&
501
+ !options?.dryRun;
502
+ printAddResult(result, !willAutoInstall);
503
+ // Auto-install npm dependencies unless opted out or dry-run
504
+ if (result.npmDeps.length > 0 &&
505
+ !options?.skipInstall &&
506
+ !options?.dryRun) {
507
+ const installSpinner = ora("Installing npm dependencies...").start();
508
+ try {
509
+ autoInstallDeps(rootDir, result.npmDeps);
510
+ installSpinner.succeed("npm dependencies installed");
511
+ }
512
+ catch {
513
+ installSpinner.warn("Failed to auto-install dependencies. Install them manually:");
514
+ console.log(chalk.cyan(` pnpm add ${result.npmDeps.join(" ")}`));
515
+ }
516
+ }
214
517
  }
215
518
  catch (error) {
216
519
  spinner.fail("Failed to add component.");
@@ -318,7 +621,7 @@ async function changeTheme(rootDir, registryDir) {
318
621
  console.error(chalk.red(`\nError: ${message}`));
319
622
  }
320
623
  }
321
- async function installAll() {
624
+ async function installAll(options) {
322
625
  const rootDir = process.cwd();
323
626
  const componentsJson = readComponentsJson(rootDir);
324
627
  if (!componentsJson) {
@@ -350,7 +653,11 @@ async function installAll() {
350
653
  const spinner = ora(`${action} "${prefix}${item.name}"...`).start();
351
654
  try {
352
655
  const spec = prefix ? `${prefix}${item.name}` : item.name;
353
- const result = await runAdd(spec, rootDir);
656
+ const result = await runAdd(spec, rootDir, {
657
+ force: options?.force,
658
+ dryRun: options?.dryRun,
659
+ skipExisting: options?.skipExisting,
660
+ });
354
661
  if (!result.added) {
355
662
  spinner.fail(result.message);
356
663
  continue;
@@ -386,7 +693,7 @@ async function installAll() {
386
693
  console.log(chalk.cyan(` pnpm add ${[...allNpmDeps].join(" ")}`));
387
694
  }
388
695
  }
389
- async function interactiveAdd() {
696
+ async function interactiveAdd(options) {
390
697
  const rootDir = process.cwd();
391
698
  const componentsJson = readComponentsJson(rootDir);
392
699
  if (!componentsJson) {
@@ -466,7 +773,11 @@ async function interactiveAdd() {
466
773
  const action = isInstalled ? "Updating" : "Adding";
467
774
  const spinner = ora(`${action} "${name}"...`).start();
468
775
  try {
469
- const result = await runAdd(name, rootDir);
776
+ const result = await runAdd(name, rootDir, {
777
+ force: options?.force,
778
+ dryRun: options?.dryRun,
779
+ skipExisting: options?.skipExisting,
780
+ });
470
781
  if (!result.added) {
471
782
  spinner.fail(result.message);
472
783
  continue;
@@ -1,4 +1,10 @@
1
1
  import { type RegistryComponent } from "./status.js";
2
+ /**
3
+ * Render a unified-style colored diff between two strings using a real
4
+ * LCS-based algorithm (`diffLines` from the `diff` package). Lines that
5
+ * changed are shown in red/green; unchanged lines are only shown as dim
6
+ * context within `CONTEXT_LINES` of a change.
7
+ */
2
8
  export declare function computeDiff(localContent: string, registryContent: string): string;
3
9
  export declare function findComponentInRegistry(registryPath: string, componentName: string): RegistryComponent | null;
4
10
  export declare function runDiff(componentName: string, rootDir: string): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/commands/diff.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAC;AAGrB,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CA+BjF;AAED,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GACpB,iBAAiB,GAAG,IAAI,CAW1B;AAED,wBAAsB,OAAO,CAC3B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAgEjB;AAED,wBAAsB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CActE"}
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/commands/diff.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,aAAa,CAAC;AAKrB;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAqEjF;AAED,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GACpB,iBAAiB,GAAG,IAAI,CAW1B;AA+BD,wBAAsB,OAAO,CAC3B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAgEjB;AAED,wBAAsB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CActE"}