inai-react-components 0.1.7 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +80 -0
  2. package/dist/commands/add.d.ts +42 -12
  3. package/dist/commands/add.d.ts.map +1 -1
  4. package/dist/commands/add.js +413 -93
  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 +90 -20
  8. package/dist/commands/init.d.ts +17 -5
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +87 -64
  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 +171 -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 +40 -4
  27. package/dist/commands/status.d.ts.map +1 -1
  28. package/dist/commands/status.js +149 -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 +197 -6
  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 +20 -0
  45. package/dist/utils/framework-detect.d.ts.map +1 -0
  46. package/dist/utils/framework-detect.js +97 -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/paths.d.ts +26 -0
  51. package/dist/utils/paths.d.ts.map +1 -0
  52. package/dist/utils/paths.js +35 -0
  53. package/dist/utils/registry-config.d.ts +27 -0
  54. package/dist/utils/registry-config.d.ts.map +1 -0
  55. package/dist/utils/registry-config.js +94 -0
  56. package/dist/utils/registry-resolver.d.ts +26 -0
  57. package/dist/utils/registry-resolver.d.ts.map +1 -1
  58. package/dist/utils/registry-resolver.js +134 -10
  59. package/dist/utils/snapshot.d.ts +20 -0
  60. package/dist/utils/snapshot.d.ts.map +1 -0
  61. package/dist/utils/snapshot.js +32 -0
  62. package/dist/utils/themes.d.ts +17 -0
  63. package/dist/utils/themes.d.ts.map +1 -0
  64. package/dist/utils/themes.js +49 -0
  65. package/dist/utils/tsconfig-detect.d.ts +18 -0
  66. package/dist/utils/tsconfig-detect.d.ts.map +1 -0
  67. package/dist/utils/tsconfig-detect.js +121 -0
  68. package/package.json +9 -3
@@ -4,29 +4,50 @@ 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";
13
+ import { resolveLocalDir, getSourceRoot } from "../utils/paths.js";
9
14
  /**
10
- * Parse a component spec like "button", "block/auth-login", or "template/admin-dashboard"
11
- * into its type prefix and name.
15
+ * Parse a component spec into its registry namespace, type prefix and name.
16
+ *
17
+ * Supported forms:
18
+ * - `button` → { registry: null, prefix: null, name: "button" }
19
+ * - `block/auth-login` → { registry: null, prefix: "block", name: "auth-login" }
20
+ * - `template/admin-dashboard` → { registry: null, prefix: "template", name: "admin-dashboard" }
21
+ * - `@acme/custom-button` → { registry: "acme", prefix: null, name: "custom-button" }
22
+ * - `@acme/block/hero` → { registry: "acme", prefix: "block", name: "hero" }
23
+ *
24
+ * `registry` is null when the caller did not specify a `@namespace/` — the
25
+ * default registry (from `components.json`) should then be used.
12
26
  */
13
27
  export function parseComponentSpec(spec) {
14
28
  const trimmed = spec.trim();
15
29
  if (!trimmed) {
16
30
  throw new Error('Component spec cannot be empty.');
17
31
  }
32
+ let registry = null;
33
+ let rest = trimmed;
34
+ const nsMatch = trimmed.match(/^@([a-z0-9][a-z0-9-]*)\/(.+)$/i);
35
+ if (nsMatch) {
36
+ registry = nsMatch[1];
37
+ rest = nsMatch[2];
38
+ }
18
39
  let prefix = null;
19
40
  let name;
20
- if (trimmed.startsWith("block/")) {
41
+ if (rest.startsWith("block/")) {
21
42
  prefix = "block";
22
- name = trimmed.slice(6).trim();
43
+ name = rest.slice(6).trim();
23
44
  }
24
- else if (trimmed.startsWith("template/")) {
45
+ else if (rest.startsWith("template/")) {
25
46
  prefix = "template";
26
- name = trimmed.slice(9).trim();
47
+ name = rest.slice(9).trim();
27
48
  }
28
49
  else {
29
- name = trimmed;
50
+ name = rest;
30
51
  }
31
52
  if (!name) {
32
53
  throw new Error(`Invalid component spec "${spec}" — name cannot be empty.`);
@@ -34,7 +55,7 @@ export function parseComponentSpec(spec) {
34
55
  if (!/^[a-z0-9][a-z0-9\-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
35
56
  throw new Error(`Invalid component name "${name}" — use only lowercase letters, numbers, and hyphens (e.g. "my-component").`);
36
57
  }
37
- return { prefix, name };
58
+ return { registry, prefix, name };
38
59
  }
39
60
  /**
40
61
  * Find a component in the registry, optionally narrowing by type prefix.
@@ -55,26 +76,153 @@ export function findInRegistry(registry, name, prefix) {
55
76
  return allItems.find((c) => c.name === name) ?? null;
56
77
  }
57
78
  /**
58
- * Determine the target directory for a component based on its type and aliases.
79
+ * Determine the target directory for a component based on its type and the
80
+ * project's aliases + layout from `components.json`.
59
81
  */
60
- export function getTargetDir(component, aliases) {
82
+ export function getTargetDir(component, componentsJson) {
83
+ const { aliases } = componentsJson;
61
84
  const type = component.type.toLowerCase();
62
85
  if (type === "block") {
63
- return aliases.blocks.replace(/^@\//, "src/");
86
+ return resolveLocalDir(aliases.blocks, componentsJson);
64
87
  }
65
88
  if (type === "template") {
66
89
  if (aliases.templates) {
67
- return aliases.templates.replace(/^@\//, "src/");
90
+ return resolveLocalDir(aliases.templates, componentsJson);
68
91
  }
69
92
  // Fallback: derive templates dir from blocks dir
70
- const blocksDir = aliases.blocks.replace(/^@\//, "src/");
93
+ const blocksDir = resolveLocalDir(aliases.blocks, componentsJson);
71
94
  const parent = path.dirname(blocksDir);
72
95
  return path.join(parent, "templates");
73
96
  }
74
97
  // Components, forms, and other types go to the components directory
75
- return aliases.components.replace(/^@\//, "src/");
98
+ return resolveLocalDir(aliases.components, componentsJson);
99
+ }
100
+ /**
101
+ * Install a single component (no dep resolution). Internal helper used by
102
+ * `runAdd` after deps have been resolved. Returns the list of files that
103
+ * were written (or would be written, in dry-run mode) and files skipped.
104
+ */
105
+ async function installOne(component, registry, componentsJson, registryDir, rootDir, options) {
106
+ const name = component.name;
107
+ const baseTargetDir = getTargetDir(component, componentsJson);
108
+ const isMultiFile = component.type === "block" || component.type === "template";
109
+ const targetDir = isMultiFile ? path.join(baseTargetDir, name) : baseTargetDir;
110
+ const copiedFiles = [];
111
+ const skippedFiles = [];
112
+ // Track captured content for the snapshot (only for files that were
113
+ // actually written — skipped ones retain whatever the user had).
114
+ const capturedFiles = {};
115
+ for (const filePath of component.files) {
116
+ const registryFilePath = path.join(registryDir, filePath);
117
+ const fileName = path.basename(filePath);
118
+ const localFilePath = path.join(rootDir, targetDir, fileName);
119
+ if (!fs.existsSync(registryFilePath)) {
120
+ continue;
121
+ }
122
+ const exists = fs.existsSync(localFilePath);
123
+ if (exists && !options.force) {
124
+ if (options.skipExisting) {
125
+ skippedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
126
+ continue;
127
+ }
128
+ if (options.dryRun) {
129
+ // In dry-run we do not prompt; treat as would-overwrite.
130
+ }
131
+ else if (process.stdin.isTTY) {
132
+ const res = await prompts({
133
+ type: "confirm",
134
+ name: "overwrite",
135
+ message: `File "${path.relative(rootDir, localFilePath)}" already exists. Overwrite?`,
136
+ initial: false,
137
+ });
138
+ if (!res.overwrite) {
139
+ skippedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
140
+ continue;
141
+ }
142
+ }
143
+ else {
144
+ 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).`);
145
+ }
146
+ }
147
+ const content = fs.readFileSync(registryFilePath, "utf-8");
148
+ if (options.dryRun) {
149
+ console.log(chalk.dim(` [dry-run] would write ${path.relative(rootDir, localFilePath)}`));
150
+ }
151
+ else {
152
+ fs.mkdirSync(path.dirname(localFilePath), { recursive: true });
153
+ fs.writeFileSync(localFilePath, content);
154
+ const relLocal = path.relative(rootDir, localFilePath);
155
+ capturedFiles[relLocal] = {
156
+ localPath: relLocal,
157
+ hash: sha256(content),
158
+ content,
159
+ };
160
+ }
161
+ copiedFiles.push(isMultiFile ? `${name}/${fileName}` : fileName);
162
+ }
163
+ // Internal deps (lib/cn, lib/animations, ...) — installed under the
164
+ // project's detected sourceRoot so they match where `init` placed cn.ts.
165
+ const sourceRoot = getSourceRoot(componentsJson);
166
+ const copiedInternalDeps = [];
167
+ for (const dep of component.internalDeps) {
168
+ const depFileName = dep.split("/").pop() + ".ts";
169
+ const depSourcePath = path.join(registryDir, "packages", "ui", "src", dep + ".ts");
170
+ const depTargetPath = path.join(rootDir, sourceRoot, "lib", depFileName);
171
+ if (!fs.existsSync(depSourcePath)) {
172
+ continue;
173
+ }
174
+ const content = fs.readFileSync(depSourcePath, "utf-8");
175
+ if (options.dryRun) {
176
+ console.log(chalk.dim(` [dry-run] would write ${path.relative(rootDir, depTargetPath)}`));
177
+ }
178
+ else {
179
+ fs.mkdirSync(path.dirname(depTargetPath), { recursive: true });
180
+ fs.writeFileSync(depTargetPath, content);
181
+ }
182
+ copiedInternalDeps.push(dep);
183
+ }
184
+ // Update components.json (skipped in dry-run).
185
+ if (!options.dryRun) {
186
+ const registryVersion = registry.version;
187
+ const installed = componentsJson.installedComponents ?? [];
188
+ const existingIdx = installed.findIndex((c) => c.name === name);
189
+ const entry = {
190
+ name,
191
+ version: registryVersion,
192
+ installedAt: new Date().toISOString().split("T")[0],
193
+ };
194
+ if (existingIdx >= 0) {
195
+ installed[existingIdx] = entry;
196
+ }
197
+ else {
198
+ installed.push(entry);
199
+ }
200
+ componentsJson.installedComponents = installed;
201
+ const componentsJsonPath = path.join(rootDir, "components.json");
202
+ fs.writeFileSync(componentsJsonPath, JSON.stringify(componentsJson, null, 2) + "\n");
203
+ // Persist the snapshot of the original content so `update` can run a
204
+ // real 3-way merge against the user's local edits and `status` can
205
+ // detect drift vs the originally-installed content.
206
+ if (Object.keys(capturedFiles).length > 0) {
207
+ const snapshot = {
208
+ name,
209
+ version: registryVersion,
210
+ files: capturedFiles,
211
+ capturedAt: new Date().toISOString(),
212
+ };
213
+ saveSnapshot(rootDir, snapshot);
214
+ }
215
+ }
216
+ return {
217
+ added: true,
218
+ message: `Component "${name}" added successfully.`,
219
+ files: copiedFiles,
220
+ npmDeps: component.npmDeps,
221
+ internalDeps: copiedInternalDeps,
222
+ skipped: skippedFiles,
223
+ };
76
224
  }
77
- export async function runAdd(componentSpec, rootDir) {
225
+ export async function runAdd(componentSpec, rootDir, options = {}) {
78
226
  const componentsJson = readComponentsJson(rootDir);
79
227
  if (!componentsJson) {
80
228
  return {
@@ -85,8 +233,69 @@ export async function runAdd(componentSpec, rootDir) {
85
233
  internalDeps: [],
86
234
  };
87
235
  }
88
- const { prefix, name } = parseComponentSpec(componentSpec);
89
- const registryDir = resolveRegistryDir(rootDir);
236
+ const { registry: nsRegistry, prefix, name } = parseComponentSpec(componentSpec);
237
+ // Multi-registry (M3) resolution: if components.json declares a
238
+ // `registries` array, honour the namespace. Otherwise fall back to
239
+ // the legacy single-registry code path so existing projects keep
240
+ // working unchanged.
241
+ const registries = await loadRegistries(rootDir);
242
+ let registryDir;
243
+ let activeRegistryName = null;
244
+ if (registries.length > 0) {
245
+ let target;
246
+ if (nsRegistry) {
247
+ target = getRegistryByName(registries, nsRegistry);
248
+ if (!target) {
249
+ return {
250
+ added: false,
251
+ message: `Registry "@${nsRegistry}" is not configured in components.json. Available: ${registries.map((r) => `@${r.name}`).join(", ")}.`,
252
+ files: [],
253
+ npmDeps: [],
254
+ internalDeps: [],
255
+ };
256
+ }
257
+ }
258
+ else {
259
+ target = getDefaultRegistry(registries);
260
+ if (!target) {
261
+ return {
262
+ added: false,
263
+ message: `No default registry configured. Run \`inai-ui registries add <name>\` first.`,
264
+ files: [],
265
+ npmDeps: [],
266
+ internalDeps: [],
267
+ };
268
+ }
269
+ }
270
+ try {
271
+ registryDir = await resolveRegistry(target, rootDir, {
272
+ offline: options.offline,
273
+ });
274
+ activeRegistryName = target.name;
275
+ }
276
+ catch (err) {
277
+ return {
278
+ added: false,
279
+ message: err instanceof Error ? err.message : String(err),
280
+ files: [],
281
+ npmDeps: [],
282
+ internalDeps: [],
283
+ };
284
+ }
285
+ }
286
+ else {
287
+ if (nsRegistry) {
288
+ return {
289
+ added: false,
290
+ message: `No registries configured in components.json — cannot resolve "@${nsRegistry}/${name}".`,
291
+ files: [],
292
+ npmDeps: [],
293
+ internalDeps: [],
294
+ };
295
+ }
296
+ registryDir = resolveRegistryDir(rootDir);
297
+ }
298
+ void activeRegistryName;
90
299
  const registryPath = path.join(registryDir, "registry.json");
91
300
  const registry = readRegistryJson(registryPath);
92
301
  if (!registry) {
@@ -101,91 +310,116 @@ export async function runAdd(componentSpec, rootDir) {
101
310
  const component = findInRegistry(registry, name, prefix);
102
311
  if (!component) {
103
312
  const label = prefix ? `${prefix}/${name}` : name;
313
+ // Gather all registry names for fuzzy matching
314
+ const allNames = [
315
+ ...registry.components.map((c) => c.name),
316
+ ...registry.blocks.map((c) => c.name),
317
+ ...registry.templates.map((c) => c.name),
318
+ ];
319
+ const suggestions = fuzzySearchNames(name, allNames, 0.4).slice(0, 5);
104
320
  return {
105
321
  added: false,
106
322
  message: `Component "${label}" not found in registry.`,
107
323
  files: [],
108
324
  npmDeps: [],
109
325
  internalDeps: [],
326
+ suggestions,
110
327
  };
111
328
  }
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;
329
+ // Resolve the dependency graph in topological order (deps first).
330
+ const installed = componentsJson.installedComponents ?? [];
331
+ const alreadyInstalled = new Set(installed.map((c) => c.name));
332
+ const resolved = new Set();
333
+ const installOrder = [];
334
+ const visited = new Set();
335
+ const visit = (comp, stack) => {
336
+ if (resolved.has(comp.name))
337
+ return;
338
+ if (stack.has(comp.name)) {
339
+ console.log(chalk.yellow(` ⚠ circular dependency detected: "${comp.name}" already in resolution stack [${[...stack].join(" → ")} → ${comp.name}], skipping`));
340
+ return;
127
341
  }
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;
342
+ if (visited.has(comp.name))
343
+ return;
344
+ visited.add(comp.name);
345
+ stack.add(comp.name);
346
+ const deps = comp.registryDependencies ?? [];
347
+ for (const depName of deps) {
348
+ if (alreadyInstalled.has(depName)) {
349
+ // already present — skip silently (still mark resolved so we don't recheck)
350
+ resolved.add(depName);
351
+ continue;
352
+ }
353
+ const depComp = findInRegistry(registry, depName, null);
354
+ if (!depComp) {
355
+ // Missing dep in the registry — warn but continue.
356
+ console.log(chalk.yellow(` ! registry dependency "${depName}" of "${comp.name}" not found in registry, skipping`));
357
+ continue;
358
+ }
359
+ visit(depComp, stack);
142
360
  }
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],
361
+ stack.delete(comp.name);
362
+ resolved.add(comp.name);
363
+ installOrder.push(comp);
157
364
  };
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");
365
+ visit(component, new Set());
366
+ const allFiles = [];
367
+ const allNpmDeps = new Set();
368
+ const allInternalDeps = new Set();
369
+ const allSkipped = [];
370
+ const transitiveDeps = [];
371
+ for (const comp of installOrder) {
372
+ if (comp.name !== name) {
373
+ console.log(chalk.dim(` → installing dependency: ${comp.name}`));
374
+ transitiveDeps.push(comp.name);
375
+ }
376
+ const result = await installOne(comp, registry, componentsJson, registryDir, rootDir, options);
377
+ for (const f of result.files)
378
+ allFiles.push(f);
379
+ for (const d of result.npmDeps)
380
+ allNpmDeps.add(d);
381
+ for (const d of result.internalDeps)
382
+ allInternalDeps.add(d);
383
+ for (const s of result.skipped ?? [])
384
+ allSkipped.push(s);
385
+ }
386
+ const suffix = options.dryRun ? " (dry-run)" : "";
167
387
  return {
168
388
  added: true,
169
- message: `Component "${name}" added successfully.`,
170
- files: copiedFiles,
171
- npmDeps: component.npmDeps,
172
- internalDeps: copiedInternalDeps,
389
+ message: `Component "${name}" added successfully${suffix}.`,
390
+ files: allFiles,
391
+ npmDeps: [...allNpmDeps],
392
+ internalDeps: [...allInternalDeps],
393
+ resolvedDeps: transitiveDeps,
394
+ skipped: allSkipped,
173
395
  };
174
396
  }
175
- function printAddResult(result) {
397
+ function printAddResult(result, showNpmHint = true) {
176
398
  if (result.files.length > 0) {
177
399
  console.log(chalk.green("\nCopied files:"));
178
400
  for (const file of result.files) {
179
401
  console.log(chalk.dim(` - ${file}`));
180
402
  }
181
403
  }
404
+ if (result.skipped && result.skipped.length > 0) {
405
+ console.log(chalk.yellow("\nSkipped files (already existed):"));
406
+ for (const file of result.skipped) {
407
+ console.log(chalk.dim(` - ${file}`));
408
+ }
409
+ }
410
+ if (result.resolvedDeps && result.resolvedDeps.length > 0) {
411
+ console.log(chalk.green("\nRegistry dependencies resolved:"));
412
+ for (const dep of result.resolvedDeps) {
413
+ console.log(chalk.dim(` - ${dep}`));
414
+ }
415
+ }
182
416
  if (result.internalDeps.length > 0) {
183
417
  console.log(chalk.green("\nInternal dependencies resolved:"));
184
418
  for (const dep of result.internalDeps) {
185
419
  console.log(chalk.dim(` - ${dep}`));
186
420
  }
187
421
  }
188
- if (result.npmDeps.length > 0) {
422
+ if (result.npmDeps.length > 0 && showNpmHint) {
189
423
  console.log(chalk.yellow("\nRequired npm dependencies:"));
190
424
  console.log(chalk.cyan(` pnpm add ${result.npmDeps.join(" ")}`));
191
425
  }
@@ -193,24 +427,98 @@ function printAddResult(result) {
193
427
  export async function addCommand(componentSpec, options) {
194
428
  // If --all flag, install everything
195
429
  if (options?.all) {
196
- await installAll();
430
+ await installAll(options);
197
431
  return;
198
432
  }
199
433
  // If no component specified, show interactive picker
200
434
  if (!componentSpec) {
201
- await interactiveAdd();
435
+ await interactiveAdd(options);
202
436
  return;
203
437
  }
204
438
  const spinner = ora(`Adding "${componentSpec}"...`).start();
205
439
  try {
206
440
  const rootDir = process.cwd();
207
- const result = await runAdd(componentSpec, rootDir);
441
+ // Pause spinner so prompts (for overwrite) work cleanly.
442
+ spinner.stop();
443
+ const result = await runAdd(componentSpec, rootDir, {
444
+ force: options?.force,
445
+ dryRun: options?.dryRun,
446
+ skipExisting: options?.skipExisting,
447
+ skipInstall: options?.skipInstall,
448
+ });
208
449
  if (!result.added) {
209
- spinner.fail(result.message);
450
+ console.error(chalk.red(result.message));
451
+ // Offer fuzzy suggestions if available
452
+ if (result.suggestions && result.suggestions.length > 0) {
453
+ const top = result.suggestions[0];
454
+ // Single high-confidence match: auto-suggest with confirm
455
+ if (top.score > 0.8 && process.stdin.isTTY) {
456
+ const confirm = await prompts({
457
+ type: "confirm",
458
+ name: "accept",
459
+ message: `Did you mean "${chalk.cyan(top.name)}"?`,
460
+ initial: true,
461
+ });
462
+ if (confirm.accept) {
463
+ // Re-run add with the corrected name
464
+ const correctedSpec = componentSpec.replace(/[^/]+$/, top.name);
465
+ const correctedResult = await runAdd(correctedSpec, rootDir, {
466
+ force: options?.force,
467
+ dryRun: options?.dryRun,
468
+ skipExisting: options?.skipExisting,
469
+ skipInstall: options?.skipInstall,
470
+ });
471
+ if (!correctedResult.added) {
472
+ console.error(chalk.red(correctedResult.message));
473
+ process.exit(1);
474
+ }
475
+ console.log(chalk.green(correctedResult.message));
476
+ const willAutoInstall = correctedResult.npmDeps.length > 0 &&
477
+ !options?.skipInstall &&
478
+ !options?.dryRun;
479
+ printAddResult(correctedResult, !willAutoInstall);
480
+ if (correctedResult.npmDeps.length > 0 &&
481
+ !options?.skipInstall &&
482
+ !options?.dryRun) {
483
+ const installSpinner = ora("Installing npm dependencies...").start();
484
+ try {
485
+ autoInstallDeps(rootDir, correctedResult.npmDeps);
486
+ installSpinner.succeed("npm dependencies installed");
487
+ }
488
+ catch {
489
+ installSpinner.warn("Failed to auto-install dependencies. Install them manually:");
490
+ console.log(chalk.cyan(` pnpm add ${correctedResult.npmDeps.join(" ")}`));
491
+ }
492
+ }
493
+ return;
494
+ }
495
+ }
496
+ // Show all suggestions
497
+ console.log(chalk.yellow(`\nDid you mean: ${result.suggestions
498
+ .map((s) => `"${chalk.cyan(s.name)}" ${chalk.dim(`(score: ${s.score.toFixed(2)})`)}`)
499
+ .join(", ")}?`));
500
+ }
210
501
  process.exit(1);
211
502
  }
212
- spinner.succeed(result.message);
213
- printAddResult(result);
503
+ console.log(chalk.green(result.message));
504
+ const willAutoInstall = result.npmDeps.length > 0 &&
505
+ !options?.skipInstall &&
506
+ !options?.dryRun;
507
+ printAddResult(result, !willAutoInstall);
508
+ // Auto-install npm dependencies unless opted out or dry-run
509
+ if (result.npmDeps.length > 0 &&
510
+ !options?.skipInstall &&
511
+ !options?.dryRun) {
512
+ const installSpinner = ora("Installing npm dependencies...").start();
513
+ try {
514
+ autoInstallDeps(rootDir, result.npmDeps);
515
+ installSpinner.succeed("npm dependencies installed");
516
+ }
517
+ catch {
518
+ installSpinner.warn("Failed to auto-install dependencies. Install them manually:");
519
+ console.log(chalk.cyan(` pnpm add ${result.npmDeps.join(" ")}`));
520
+ }
521
+ }
214
522
  }
215
523
  catch (error) {
216
524
  spinner.fail("Failed to add component.");
@@ -269,10 +577,14 @@ async function changeTheme(rootDir, registryDir) {
269
577
  try {
270
578
  const tokensSrc = path.join(registryDir, "packages", "tokens", "src");
271
579
  const isRemote = !!componentsJson.registrySource;
580
+ const sourceRoot = getSourceRoot(componentsJson);
581
+ const cssRelPath = componentsJson.tailwind?.css ?? `${sourceRoot}/index.css`;
582
+ const cssPath = path.join(rootDir, cssRelPath);
583
+ const themesRelPath = `${sourceRoot}/styles/tokens/themes`;
272
584
  if (isRemote) {
273
585
  // Remote mode: copy and normalize theme file
274
586
  const themesSrc = path.join(tokensSrc, "themes");
275
- const themesDest = path.join(rootDir, "src", "styles", "tokens", "themes");
587
+ const themesDest = path.join(rootDir, sourceRoot, "styles", "tokens", "themes");
276
588
  // Remove old theme file
277
589
  const oldThemeFile = path.join(themesDest, `${currentTheme}.css`);
278
590
  if (fs.existsSync(oldThemeFile)) {
@@ -286,17 +598,17 @@ async function changeTheme(rootDir, registryDir) {
286
598
  const normalizedCss = normalizeThemeCss(rawCss, newTheme);
287
599
  fs.writeFileSync(path.join(themesDest, `${newTheme}.css`), normalizedCss);
288
600
  }
289
- // Regenerate index.css
601
+ // Regenerate CSS entry
290
602
  const font = componentsJson.font ?? { body: "outfit", heading: "outfit" };
291
603
  const radius = componentsJson.radius ?? 0.5;
292
- const cssPath = path.join(rootDir, "src", "index.css");
604
+ fs.mkdirSync(path.dirname(cssPath), { recursive: true });
293
605
  fs.writeFileSync(cssPath, getLocalTailwindCssTemplate(newTheme, font.body, font.heading, radius));
294
606
  }
295
607
  else {
296
- // Local mode: just regenerate index.css with new import
608
+ // Local mode: just regenerate CSS entry with new import
297
609
  const font = componentsJson.font ?? { body: "outfit", heading: "outfit" };
298
610
  const radius = componentsJson.radius ?? 0.5;
299
- const cssPath = path.join(rootDir, "src", "index.css");
611
+ fs.mkdirSync(path.dirname(cssPath), { recursive: true });
300
612
  fs.writeFileSync(cssPath, getLegacyTailwindCssTemplate(newTheme, font.body, font.heading, radius));
301
613
  }
302
614
  // Update components.json
@@ -307,9 +619,9 @@ async function changeTheme(rootDir, registryDir) {
307
619
  spinner.succeed(`Theme switched to "${newTheme}"`);
308
620
  console.log(chalk.green("\nUpdated files:"));
309
621
  console.log(chalk.dim(" - components.json"));
310
- console.log(chalk.dim(" - src/index.css"));
622
+ console.log(chalk.dim(` - ${cssRelPath}`));
311
623
  if (componentsJson.registrySource) {
312
- console.log(chalk.dim(` - src/styles/tokens/themes/${newTheme}.css`));
624
+ console.log(chalk.dim(` - ${themesRelPath}/${newTheme}.css`));
313
625
  }
314
626
  }
315
627
  catch (error) {
@@ -318,7 +630,7 @@ async function changeTheme(rootDir, registryDir) {
318
630
  console.error(chalk.red(`\nError: ${message}`));
319
631
  }
320
632
  }
321
- async function installAll() {
633
+ async function installAll(options) {
322
634
  const rootDir = process.cwd();
323
635
  const componentsJson = readComponentsJson(rootDir);
324
636
  if (!componentsJson) {
@@ -350,7 +662,11 @@ async function installAll() {
350
662
  const spinner = ora(`${action} "${prefix}${item.name}"...`).start();
351
663
  try {
352
664
  const spec = prefix ? `${prefix}${item.name}` : item.name;
353
- const result = await runAdd(spec, rootDir);
665
+ const result = await runAdd(spec, rootDir, {
666
+ force: options?.force,
667
+ dryRun: options?.dryRun,
668
+ skipExisting: options?.skipExisting,
669
+ });
354
670
  if (!result.added) {
355
671
  spinner.fail(result.message);
356
672
  continue;
@@ -386,7 +702,7 @@ async function installAll() {
386
702
  console.log(chalk.cyan(` pnpm add ${[...allNpmDeps].join(" ")}`));
387
703
  }
388
704
  }
389
- async function interactiveAdd() {
705
+ async function interactiveAdd(options) {
390
706
  const rootDir = process.cwd();
391
707
  const componentsJson = readComponentsJson(rootDir);
392
708
  if (!componentsJson) {
@@ -466,7 +782,11 @@ async function interactiveAdd() {
466
782
  const action = isInstalled ? "Updating" : "Adding";
467
783
  const spinner = ora(`${action} "${name}"...`).start();
468
784
  try {
469
- const result = await runAdd(name, rootDir);
785
+ const result = await runAdd(name, rootDir, {
786
+ force: options?.force,
787
+ dryRun: options?.dryRun,
788
+ skipExisting: options?.skipExisting,
789
+ });
470
790
  if (!result.added) {
471
791
  spinner.fail(result.message);
472
792
  continue;