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.
- package/README.md +80 -0
- package/dist/commands/add.d.ts +39 -5
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +415 -81
- 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 -26
- 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 -17
- 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 +12 -3
package/dist/commands/add.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
48
|
+
else {
|
|
49
|
+
name = rest;
|
|
50
|
+
}
|
|
51
|
+
if (!name) {
|
|
52
|
+
throw new Error(`Invalid component spec "${spec}" — name cannot be empty.`);
|
|
19
53
|
}
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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:
|
|
148
|
-
npmDeps:
|
|
149
|
-
internalDeps:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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 ?? "
|
|
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;
|
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"}
|