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.
- package/README.md +80 -0
- package/dist/commands/add.d.ts +42 -12
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +413 -93
- package/dist/commands/diff.d.ts +6 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +90 -20
- package/dist/commands/init.d.ts +17 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +87 -64
- 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 +171 -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 +40 -4
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +149 -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 +197 -6
- 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 +20 -0
- package/dist/utils/framework-detect.d.ts.map +1 -0
- package/dist/utils/framework-detect.js +97 -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/paths.d.ts +26 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +35 -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/dist/utils/tsconfig-detect.d.ts +18 -0
- package/dist/utils/tsconfig-detect.d.ts.map +1 -0
- package/dist/utils/tsconfig-detect.js +121 -0
- package/package.json +9 -3
package/dist/commands/add.js
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
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 (
|
|
41
|
+
if (rest.startsWith("block/")) {
|
|
21
42
|
prefix = "block";
|
|
22
|
-
name =
|
|
43
|
+
name = rest.slice(6).trim();
|
|
23
44
|
}
|
|
24
|
-
else if (
|
|
45
|
+
else if (rest.startsWith("template/")) {
|
|
25
46
|
prefix = "template";
|
|
26
|
-
name =
|
|
47
|
+
name = rest.slice(9).trim();
|
|
27
48
|
}
|
|
28
49
|
else {
|
|
29
|
-
name =
|
|
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
|
|
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,
|
|
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
|
|
86
|
+
return resolveLocalDir(aliases.blocks, componentsJson);
|
|
64
87
|
}
|
|
65
88
|
if (type === "template") {
|
|
66
89
|
if (aliases.templates) {
|
|
67
|
-
return aliases.templates
|
|
90
|
+
return resolveLocalDir(aliases.templates, componentsJson);
|
|
68
91
|
}
|
|
69
92
|
// Fallback: derive templates dir from blocks dir
|
|
70
|
-
const blocksDir = aliases.blocks
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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],
|
|
361
|
+
stack.delete(comp.name);
|
|
362
|
+
resolved.add(comp.name);
|
|
363
|
+
installOrder.push(comp);
|
|
157
364
|
};
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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:
|
|
171
|
-
npmDeps:
|
|
172
|
-
internalDeps:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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,
|
|
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
|
|
601
|
+
// Regenerate CSS entry
|
|
290
602
|
const font = componentsJson.font ?? { body: "outfit", heading: "outfit" };
|
|
291
603
|
const radius = componentsJson.radius ?? 0.5;
|
|
292
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
622
|
+
console.log(chalk.dim(` - ${cssRelPath}`));
|
|
311
623
|
if (componentsJson.registrySource) {
|
|
312
|
-
console.log(chalk.dim(` -
|
|
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;
|