inai-react-components 0.1.6 → 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 +39 -5
  3. package/dist/commands/add.d.ts.map +1 -1
  4. package/dist/commands/add.js +415 -81
  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 -26
  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 -17
  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 +12 -3
@@ -4,20 +4,57 @@ 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
- if (spec.startsWith("block/")) {
15
- return { prefix: "block", name: spec.slice(6) };
27
+ const trimmed = spec.trim();
28
+ if (!trimmed) {
29
+ throw new Error('Component spec cannot be empty.');
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
+ }
38
+ let prefix = null;
39
+ let name;
40
+ if (rest.startsWith("block/")) {
41
+ prefix = "block";
42
+ name = rest.slice(6).trim();
43
+ }
44
+ else if (rest.startsWith("template/")) {
45
+ prefix = "template";
46
+ name = rest.slice(9).trim();
16
47
  }
17
- if (spec.startsWith("template/")) {
18
- return { prefix: "template", name: spec.slice(9) };
48
+ else {
49
+ name = rest;
50
+ }
51
+ if (!name) {
52
+ throw new Error(`Invalid component spec "${spec}" — name cannot be empty.`);
19
53
  }
20
- return { prefix: null, name: spec };
54
+ if (!/^[a-z0-9][a-z0-9\-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
55
+ throw new Error(`Invalid component name "${name}" — use only lowercase letters, numbers, and hyphens (e.g. "my-component").`);
56
+ }
57
+ return { registry, prefix, name };
21
58
  }
22
59
  /**
23
60
  * Find a component in the registry, optionally narrowing by type prefix.
@@ -46,12 +83,141 @@ export function getTargetDir(component, aliases) {
46
83
  return aliases.blocks.replace(/^@\//, "src/");
47
84
  }
48
85
  if (type === "template") {
49
- return aliases.blocks.replace(/^@\//, "src/").replace(/blocks$/, "templates");
86
+ if (aliases.templates) {
87
+ return aliases.templates.replace(/^@\//, "src/");
88
+ }
89
+ // Fallback: derive templates dir from blocks dir
90
+ const blocksDir = aliases.blocks.replace(/^@\//, "src/");
91
+ const parent = path.dirname(blocksDir);
92
+ return path.join(parent, "templates");
50
93
  }
51
94
  // Components, forms, and other types go to the components directory
52
95
  return aliases.components.replace(/^@\//, "src/");
53
96
  }
54
- 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 = {}) {
55
221
  const componentsJson = readComponentsJson(rootDir);
56
222
  if (!componentsJson) {
57
223
  return {
@@ -62,8 +228,69 @@ export async function runAdd(componentSpec, rootDir) {
62
228
  internalDeps: [],
63
229
  };
64
230
  }
65
- const { prefix, name } = parseComponentSpec(componentSpec);
66
- 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;
67
294
  const registryPath = path.join(registryDir, "registry.json");
68
295
  const registry = readRegistryJson(registryPath);
69
296
  if (!registry) {
@@ -78,91 +305,116 @@ export async function runAdd(componentSpec, rootDir) {
78
305
  const component = findInRegistry(registry, name, prefix);
79
306
  if (!component) {
80
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);
81
315
  return {
82
316
  added: false,
83
317
  message: `Component "${label}" not found in registry.`,
84
318
  files: [],
85
319
  npmDeps: [],
86
320
  internalDeps: [],
321
+ suggestions,
87
322
  };
88
323
  }
89
- // Determine target directory
90
- const baseTargetDir = getTargetDir(component, componentsJson.aliases);
91
- const isMultiFile = component.type === "block" || component.type === "template";
92
- // Blocks/templates go into a named subdirectory (e.g. src/components/blocks/auth-login/)
93
- const targetDir = isMultiFile
94
- ? path.join(baseTargetDir, name)
95
- : baseTargetDir;
96
- const copiedFiles = [];
97
- // Copy component source files
98
- for (const filePath of component.files) {
99
- const registryFilePath = path.join(registryDir, filePath);
100
- const fileName = path.basename(filePath);
101
- const localFilePath = path.join(rootDir, targetDir, fileName);
102
- if (!fs.existsSync(registryFilePath)) {
103
- 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;
104
336
  }
105
- fs.mkdirSync(path.dirname(localFilePath), { recursive: true });
106
- const content = fs.readFileSync(registryFilePath, "utf-8");
107
- fs.writeFileSync(localFilePath, content);
108
- copiedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
109
- }
110
- // Resolve and copy internal dependencies
111
- const copiedInternalDeps = [];
112
- for (const dep of component.internalDeps) {
113
- // dep is like "lib/cn" or "lib/animations"
114
- const depFileName = dep.split("/").pop() + ".ts";
115
- const depSourcePath = path.join(registryDir, "packages", "ui", "src", dep + ".ts");
116
- const depTargetPath = path.join(rootDir, "src", "lib", depFileName);
117
- if (!fs.existsSync(depSourcePath)) {
118
- 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);
119
355
  }
120
- // Copy even if it already exists (ensure latest version)
121
- fs.mkdirSync(path.dirname(depTargetPath), { recursive: true });
122
- const content = fs.readFileSync(depSourcePath, "utf-8");
123
- fs.writeFileSync(depTargetPath, content);
124
- copiedInternalDeps.push(dep);
125
- }
126
- // Update components.json with installed component entry
127
- const registryVersion = registry.version;
128
- const installed = componentsJson.installedComponents ?? [];
129
- const existingIdx = installed.findIndex((c) => c.name === name);
130
- const entry = {
131
- name,
132
- version: registryVersion,
133
- installedAt: new Date().toISOString().split("T")[0],
356
+ stack.delete(comp.name);
357
+ resolved.add(comp.name);
358
+ installOrder.push(comp);
134
359
  };
135
- if (existingIdx >= 0) {
136
- installed[existingIdx] = entry;
137
- }
138
- else {
139
- installed.push(entry);
140
- }
141
- componentsJson.installedComponents = installed;
142
- const componentsJsonPath = path.join(rootDir, "components.json");
143
- 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)" : "";
144
382
  return {
145
383
  added: true,
146
- message: `Component "${name}" added successfully.`,
147
- files: copiedFiles,
148
- npmDeps: component.npmDeps,
149
- internalDeps: copiedInternalDeps,
384
+ message: `Component "${name}" added successfully${suffix}.`,
385
+ files: allFiles,
386
+ npmDeps: [...allNpmDeps],
387
+ internalDeps: [...allInternalDeps],
388
+ resolvedDeps: transitiveDeps,
389
+ skipped: allSkipped,
150
390
  };
151
391
  }
152
- function printAddResult(result) {
392
+ function printAddResult(result, showNpmHint = true) {
153
393
  if (result.files.length > 0) {
154
394
  console.log(chalk.green("\nCopied files:"));
155
395
  for (const file of result.files) {
156
396
  console.log(chalk.dim(` - ${file}`));
157
397
  }
158
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
+ }
159
411
  if (result.internalDeps.length > 0) {
160
412
  console.log(chalk.green("\nInternal dependencies resolved:"));
161
413
  for (const dep of result.internalDeps) {
162
414
  console.log(chalk.dim(` - ${dep}`));
163
415
  }
164
416
  }
165
- if (result.npmDeps.length > 0) {
417
+ if (result.npmDeps.length > 0 && showNpmHint) {
166
418
  console.log(chalk.yellow("\nRequired npm dependencies:"));
167
419
  console.log(chalk.cyan(` pnpm add ${result.npmDeps.join(" ")}`));
168
420
  }
@@ -170,24 +422,98 @@ function printAddResult(result) {
170
422
  export async function addCommand(componentSpec, options) {
171
423
  // If --all flag, install everything
172
424
  if (options?.all) {
173
- await installAll();
425
+ await installAll(options);
174
426
  return;
175
427
  }
176
428
  // If no component specified, show interactive picker
177
429
  if (!componentSpec) {
178
- await interactiveAdd();
430
+ await interactiveAdd(options);
179
431
  return;
180
432
  }
181
433
  const spinner = ora(`Adding "${componentSpec}"...`).start();
182
434
  try {
183
435
  const rootDir = process.cwd();
184
- 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
+ });
185
444
  if (!result.added) {
186
- 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
+ }
187
496
  process.exit(1);
188
497
  }
189
- spinner.succeed(result.message);
190
- 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
+ }
191
517
  }
192
518
  catch (error) {
193
519
  spinner.fail("Failed to add component.");
@@ -216,7 +542,7 @@ function discoverThemes(registryDir) {
216
542
  */
217
543
  async function changeTheme(rootDir, registryDir) {
218
544
  const componentsJson = readComponentsJson(rootDir);
219
- const currentTheme = componentsJson.theme ?? "monday";
545
+ const currentTheme = componentsJson.theme ?? "inai";
220
546
  const themes = discoverThemes(registryDir);
221
547
  if (themes.length === 0) {
222
548
  console.log(chalk.red("No themes found in registry."));
@@ -295,7 +621,7 @@ async function changeTheme(rootDir, registryDir) {
295
621
  console.error(chalk.red(`\nError: ${message}`));
296
622
  }
297
623
  }
298
- async function installAll() {
624
+ async function installAll(options) {
299
625
  const rootDir = process.cwd();
300
626
  const componentsJson = readComponentsJson(rootDir);
301
627
  if (!componentsJson) {
@@ -327,7 +653,11 @@ async function installAll() {
327
653
  const spinner = ora(`${action} "${prefix}${item.name}"...`).start();
328
654
  try {
329
655
  const spec = prefix ? `${prefix}${item.name}` : item.name;
330
- 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
+ });
331
661
  if (!result.added) {
332
662
  spinner.fail(result.message);
333
663
  continue;
@@ -363,7 +693,7 @@ async function installAll() {
363
693
  console.log(chalk.cyan(` pnpm add ${[...allNpmDeps].join(" ")}`));
364
694
  }
365
695
  }
366
- async function interactiveAdd() {
696
+ async function interactiveAdd(options) {
367
697
  const rootDir = process.cwd();
368
698
  const componentsJson = readComponentsJson(rootDir);
369
699
  if (!componentsJson) {
@@ -443,7 +773,11 @@ async function interactiveAdd() {
443
773
  const action = isInstalled ? "Updating" : "Adding";
444
774
  const spinner = ora(`${action} "${name}"...`).start();
445
775
  try {
446
- 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
+ });
447
781
  if (!result.added) {
448
782
  spinner.fail(result.message);
449
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"}