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.
- package/README.md +80 -0
- package/dist/commands/add.d.ts +38 -5
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +391 -80
- package/dist/commands/diff.d.ts +6 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +92 -20
- package/dist/commands/init.d.ts +6 -3
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +33 -36
- package/dist/commands/mcp.d.ts +123 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +289 -0
- package/dist/commands/migrate.d.ts +41 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +139 -0
- package/dist/commands/registries.d.ts +46 -0
- package/dist/commands/registries.d.ts.map +1 -0
- package/dist/commands/registries.js +152 -0
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +170 -0
- package/dist/commands/schema.d.ts +16 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +32 -0
- package/dist/commands/status.d.ts +32 -3
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +148 -25
- package/dist/commands/theme.d.ts.map +1 -1
- package/dist/commands/theme.js +3 -37
- package/dist/commands/update.d.ts +18 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +195 -5
- package/dist/index.js +76 -5
- package/dist/schemas/registry-config.schema.json +62 -0
- package/dist/schemas/registry-item.schema.json +63 -0
- package/dist/schemas/registry.schema.json +15 -0
- package/dist/types/registry.d.ts +50 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/registry.js +10 -0
- package/dist/utils/auto-install.d.ts +11 -0
- package/dist/utils/auto-install.d.ts.map +1 -0
- package/dist/utils/auto-install.js +29 -0
- package/dist/utils/framework-detect.d.ts +15 -0
- package/dist/utils/framework-detect.d.ts.map +1 -0
- package/dist/utils/framework-detect.js +90 -0
- package/dist/utils/fuzzy-search.d.ts +16 -0
- package/dist/utils/fuzzy-search.d.ts.map +1 -0
- package/dist/utils/fuzzy-search.js +67 -0
- package/dist/utils/registry-config.d.ts +27 -0
- package/dist/utils/registry-config.d.ts.map +1 -0
- package/dist/utils/registry-config.js +94 -0
- package/dist/utils/registry-resolver.d.ts +26 -0
- package/dist/utils/registry-resolver.d.ts.map +1 -1
- package/dist/utils/registry-resolver.js +134 -10
- package/dist/utils/snapshot.d.ts +20 -0
- package/dist/utils/snapshot.d.ts.map +1 -0
- package/dist/utils/snapshot.js +32 -0
- package/dist/utils/themes.d.ts +17 -0
- package/dist/utils/themes.d.ts.map +1 -0
- package/dist/utils/themes.js +49 -0
- package/package.json +9 -3
package/dist/commands/add.js
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
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 (
|
|
40
|
+
if (rest.startsWith("block/")) {
|
|
21
41
|
prefix = "block";
|
|
22
|
-
name =
|
|
42
|
+
name = rest.slice(6).trim();
|
|
23
43
|
}
|
|
24
|
-
else if (
|
|
44
|
+
else if (rest.startsWith("template/")) {
|
|
25
45
|
prefix = "template";
|
|
26
|
-
name =
|
|
46
|
+
name = rest.slice(9).trim();
|
|
27
47
|
}
|
|
28
48
|
else {
|
|
29
|
-
name =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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:
|
|
171
|
-
npmDeps:
|
|
172
|
-
internalDeps:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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;
|
package/dist/commands/diff.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|