@work-rjkashyap/unified-ui 0.1.2 → 0.2.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/CHANGELOG.md +93 -0
- package/bin/cli.mjs +721 -0
- package/dist/{chunk-EO4WROWH.mjs → chunk-3OZJ4JLW.mjs} +116 -2
- package/dist/chunk-B3CW2WZS.cjs +20748 -0
- package/dist/chunk-CTWNFFLB.mjs +20438 -0
- package/dist/{chunk-7ITQSRGX.cjs → chunk-FUWXGHWQ.cjs} +0 -1
- package/dist/{chunk-ZPIPKY2J.cjs → chunk-HITTFB2U.cjs} +127 -1
- package/dist/{chunk-F5S6NLOT.mjs → chunk-OHEH57BV.mjs} +0 -1
- package/dist/{chunk-PQR7C4OH.cjs → chunk-TESKVASH.cjs} +332 -99
- package/dist/{chunk-ZDB557B2.mjs → chunk-YFH5JPAA.mjs} +331 -101
- package/dist/components.cjs +780 -126
- package/dist/components.d.cts +5183 -1464
- package/dist/components.d.ts +5183 -1464
- package/dist/components.mjs +3 -1
- package/dist/index.cjs +926 -214
- package/dist/index.d.cts +9 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.mjs +5 -5
- package/dist/motion.cjs +94 -46
- package/dist/motion.d.cts +53 -2
- package/dist/motion.d.ts +53 -2
- package/dist/motion.mjs +1 -1
- package/dist/primitives.cjs +13 -13
- package/dist/primitives.mjs +1 -1
- package/dist/theme.cjs +40 -28
- package/dist/theme.d.cts +100 -62
- package/dist/theme.d.ts +100 -62
- package/dist/theme.mjs +1 -1
- package/dist/utils.d.cts +1 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +190 -183
- package/styles.css +636 -473
- package/dist/chunk-4NYLE2LT.cjs +0 -10042
- package/dist/chunk-A4YYJAAJ.mjs +0 -9897
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Unified UI — CLI
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Copy-paste component installer for Unified UI.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// npx @work-rjkashyap/unified-ui add button
|
|
10
|
+
// npx @work-rjkashyap/unified-ui add button card badge
|
|
11
|
+
// npx @work-rjkashyap/unified-ui add --all
|
|
12
|
+
// npx @work-rjkashyap/unified-ui list
|
|
13
|
+
// npx @work-rjkashyap/unified-ui init
|
|
14
|
+
//
|
|
15
|
+
// Components are fetched from the registry at:
|
|
16
|
+
// https://unified-ui.vercel.app/r/<name>.json
|
|
17
|
+
//
|
|
18
|
+
// Files are written into the user's project at:
|
|
19
|
+
// src/components/ui/<component>.tsx
|
|
20
|
+
// src/lib/<util>.ts
|
|
21
|
+
// src/lib/motion/<preset>.ts
|
|
22
|
+
// src/styles/unified-ui.css
|
|
23
|
+
//
|
|
24
|
+
// This CLI resolves the full dependency tree — if you add "confirm-dialog",
|
|
25
|
+
// it also pulls in "alert-dialog", "button", "cn", "focus-ring", etc.
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { dirname, join, resolve } from "node:path";
|
|
30
|
+
import { createInterface } from "node:readline";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Config
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const REGISTRY_BASE_URL =
|
|
37
|
+
process.env.UNIFIED_UI_REGISTRY_URL ||
|
|
38
|
+
"https://unified-ui.vercel.app/r";
|
|
39
|
+
|
|
40
|
+
const CONFIG_FILE = "unified-ui.json";
|
|
41
|
+
|
|
42
|
+
const DEFAULT_CONFIG = {
|
|
43
|
+
$schema: "https://unified-ui.vercel.app/r/schema/config.json",
|
|
44
|
+
srcDir: "src",
|
|
45
|
+
aliases: {
|
|
46
|
+
components: "@/components/ui",
|
|
47
|
+
lib: "@/lib",
|
|
48
|
+
styles: "@/styles",
|
|
49
|
+
},
|
|
50
|
+
typescript: true,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const COLORS = {
|
|
54
|
+
reset: "\x1b[0m",
|
|
55
|
+
bold: "\x1b[1m",
|
|
56
|
+
dim: "\x1b[2m",
|
|
57
|
+
red: "\x1b[31m",
|
|
58
|
+
green: "\x1b[32m",
|
|
59
|
+
yellow: "\x1b[33m",
|
|
60
|
+
blue: "\x1b[34m",
|
|
61
|
+
magenta: "\x1b[35m",
|
|
62
|
+
cyan: "\x1b[36m",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const c = (color, text) => `${COLORS[color]}${text}${COLORS.reset}`;
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function log(msg = "") {
|
|
72
|
+
console.log(msg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function logStep(icon, msg) {
|
|
76
|
+
console.log(` ${icon} ${msg}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logError(msg) {
|
|
80
|
+
console.error(`\n ${c("red", "✗")} ${msg}\n`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function confirm(question) {
|
|
84
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
85
|
+
return new Promise((res) => {
|
|
86
|
+
rl.question(` ${question} ${c("dim", "(y/N)")} `, (answer) => {
|
|
87
|
+
rl.close();
|
|
88
|
+
res(answer.trim().toLowerCase() === "y");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchJSON(url) {
|
|
94
|
+
const response = await fetch(url);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
97
|
+
}
|
|
98
|
+
return response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Config management
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function findProjectRoot() {
|
|
106
|
+
let dir = process.cwd();
|
|
107
|
+
while (dir !== dirname(dir)) {
|
|
108
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
109
|
+
dir = dirname(dir);
|
|
110
|
+
}
|
|
111
|
+
return process.cwd();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function loadConfig() {
|
|
115
|
+
const root = findProjectRoot();
|
|
116
|
+
const configPath = join(root, CONFIG_FILE);
|
|
117
|
+
|
|
118
|
+
if (existsSync(configPath)) {
|
|
119
|
+
try {
|
|
120
|
+
return {
|
|
121
|
+
root,
|
|
122
|
+
...DEFAULT_CONFIG,
|
|
123
|
+
...JSON.parse(readFileSync(configPath, "utf-8")),
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return { root, ...DEFAULT_CONFIG };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { root, ...DEFAULT_CONFIG };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function saveConfig(config) {
|
|
134
|
+
const root = findProjectRoot();
|
|
135
|
+
const configPath = join(root, CONFIG_FILE);
|
|
136
|
+
const { root: _root, ...rest } = config;
|
|
137
|
+
writeFileSync(configPath, JSON.stringify(rest, null, 2) + "\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Path resolution
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
function resolveTargetPath(config, file) {
|
|
145
|
+
const srcDir = join(config.root, config.srcDir);
|
|
146
|
+
|
|
147
|
+
switch (file.type) {
|
|
148
|
+
case "component":
|
|
149
|
+
return join(srcDir, "components", "ui", basename(file.path));
|
|
150
|
+
case "util":
|
|
151
|
+
if (file.path.includes("motion/")) {
|
|
152
|
+
return join(srcDir, "lib", "motion", basename(file.path));
|
|
153
|
+
}
|
|
154
|
+
return join(srcDir, "lib", basename(file.path));
|
|
155
|
+
case "styles":
|
|
156
|
+
return join(srcDir, "styles", basename(file.path));
|
|
157
|
+
case "hook":
|
|
158
|
+
return join(srcDir, "hooks", basename(file.path));
|
|
159
|
+
default:
|
|
160
|
+
return join(srcDir, file.target || file.path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function basename(p) {
|
|
165
|
+
return p.split("/").pop();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Import path rewriting
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// The registry items have imports like:
|
|
172
|
+
// import { cn } from "@/lib/cn"
|
|
173
|
+
// import { focusRingClasses } from "@/lib/focus-ring"
|
|
174
|
+
// import { Button } from "./button"
|
|
175
|
+
//
|
|
176
|
+
// We rewrite these to match the user's alias config.
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
function rewriteContentImports(content, config) {
|
|
180
|
+
let result = content;
|
|
181
|
+
|
|
182
|
+
// Rewrite @/lib/* -> user's lib alias
|
|
183
|
+
if (config.aliases.lib !== "@/lib") {
|
|
184
|
+
result = result.replace(
|
|
185
|
+
/from\s+["']@\/lib\//g,
|
|
186
|
+
`from "${config.aliases.lib}/`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Rewrite @/components/ui/* -> user's components alias
|
|
191
|
+
if (config.aliases.components !== "@/components/ui") {
|
|
192
|
+
result = result.replace(
|
|
193
|
+
/from\s+["']@\/components\/ui\//g,
|
|
194
|
+
`from "${config.aliases.components}/`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Dependency resolution
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
async function resolveFullDependencyTree(names, registryUrl) {
|
|
206
|
+
const resolved = new Map();
|
|
207
|
+
const queue = [...names];
|
|
208
|
+
const visited = new Set();
|
|
209
|
+
|
|
210
|
+
while (queue.length > 0) {
|
|
211
|
+
const name = queue.shift();
|
|
212
|
+
if (visited.has(name)) continue;
|
|
213
|
+
visited.add(name);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const item = await fetchJSON(`${registryUrl}/${name}.json`);
|
|
217
|
+
resolved.set(name, item);
|
|
218
|
+
|
|
219
|
+
// Queue registry dependencies (other components)
|
|
220
|
+
if (item.registryDependencies) {
|
|
221
|
+
for (const dep of item.registryDependencies) {
|
|
222
|
+
if (!visited.has(dep)) queue.push(dep);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Queue internal util dependencies
|
|
227
|
+
if (item.internalDependencies) {
|
|
228
|
+
for (const util of item.internalDependencies.utils || []) {
|
|
229
|
+
if (!visited.has(util)) queue.push(util);
|
|
230
|
+
}
|
|
231
|
+
if (item.internalDependencies.motion && !visited.has("motion")) {
|
|
232
|
+
queue.push("motion");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logError(`Could not fetch "${name}" from registry: ${err.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return resolved;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// npm dependency installer
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
async function detectPackageManager(root) {
|
|
248
|
+
if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) return "bun";
|
|
249
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
250
|
+
if (existsSync(join(root, "yarn.lock"))) return "yarn";
|
|
251
|
+
return "npm";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getInstallCommand(pm, deps) {
|
|
255
|
+
const packages = deps.join(" ");
|
|
256
|
+
switch (pm) {
|
|
257
|
+
case "bun":
|
|
258
|
+
return `bun add ${packages}`;
|
|
259
|
+
case "pnpm":
|
|
260
|
+
return `pnpm add ${packages}`;
|
|
261
|
+
case "yarn":
|
|
262
|
+
return `yarn add ${packages}`;
|
|
263
|
+
default:
|
|
264
|
+
return `npm install ${packages}`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function installNpmDeps(deps, root) {
|
|
269
|
+
if (deps.length === 0) return;
|
|
270
|
+
|
|
271
|
+
const pm = await detectPackageManager(root);
|
|
272
|
+
const cmd = getInstallCommand(pm, deps);
|
|
273
|
+
|
|
274
|
+
logStep("📦", `Installing npm dependencies with ${c("cyan", pm)}...`);
|
|
275
|
+
logStep(" ", c("dim", cmd));
|
|
276
|
+
|
|
277
|
+
const { execSync } = await import("node:child_process");
|
|
278
|
+
try {
|
|
279
|
+
execSync(cmd, { cwd: root, stdio: "pipe" });
|
|
280
|
+
logStep("✓", c("green", `${deps.length} package(s) installed`));
|
|
281
|
+
} catch (err) {
|
|
282
|
+
logStep(
|
|
283
|
+
"⚠",
|
|
284
|
+
c("yellow", `Auto-install failed. Run manually:\n ${cmd}`),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// File writer
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
function writeFile(targetPath, content, config, overwrite = false) {
|
|
294
|
+
const rewritten = rewriteContentImports(content, config);
|
|
295
|
+
|
|
296
|
+
if (existsSync(targetPath) && !overwrite) {
|
|
297
|
+
return { path: targetPath, status: "skipped" };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
301
|
+
writeFileSync(targetPath, rewritten);
|
|
302
|
+
return { path: targetPath, status: "created" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Commands
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
async function cmdInit() {
|
|
310
|
+
log();
|
|
311
|
+
log(` ${c("bold", "Unified UI")} ${c("dim", "— Initialize project")}`);
|
|
312
|
+
log();
|
|
313
|
+
|
|
314
|
+
const config = loadConfig();
|
|
315
|
+
|
|
316
|
+
if (existsSync(join(config.root, CONFIG_FILE))) {
|
|
317
|
+
const overwrite = await confirm(
|
|
318
|
+
`${c("yellow", CONFIG_FILE)} already exists. Overwrite?`,
|
|
319
|
+
);
|
|
320
|
+
if (!overwrite) {
|
|
321
|
+
logStep("↩", "Cancelled.");
|
|
322
|
+
log();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
saveConfig(DEFAULT_CONFIG);
|
|
328
|
+
logStep("✓", `Created ${c("cyan", CONFIG_FILE)}`);
|
|
329
|
+
|
|
330
|
+
// Create directories
|
|
331
|
+
const srcDir = join(config.root, config.srcDir);
|
|
332
|
+
const dirs = [
|
|
333
|
+
join(srcDir, "components", "ui"),
|
|
334
|
+
join(srcDir, "lib"),
|
|
335
|
+
join(srcDir, "styles"),
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
for (const dir of dirs) {
|
|
339
|
+
mkdirSync(dir, { recursive: true });
|
|
340
|
+
logStep("✓", `Created ${c("dim", dir.replace(config.root + "/", ""))}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Fetch and write the base utilities (cn + focus-ring) and styles
|
|
344
|
+
log();
|
|
345
|
+
logStep("📡", "Fetching base utilities from registry...");
|
|
346
|
+
|
|
347
|
+
const baseItems = ["cn", "focus-ring", "styles"];
|
|
348
|
+
for (const name of baseItems) {
|
|
349
|
+
try {
|
|
350
|
+
const item = await fetchJSON(`${REGISTRY_BASE_URL}/${name}.json`);
|
|
351
|
+
for (const file of item.files) {
|
|
352
|
+
const targetPath = resolveTargetPath(config, file);
|
|
353
|
+
writeFile(targetPath, file.content, config, true);
|
|
354
|
+
logStep("✓", `${c("green", file.path)}`);
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
logStep("⚠", c("yellow", `Could not fetch ${name} — add manually later`));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Install base npm deps
|
|
362
|
+
await installNpmDeps(
|
|
363
|
+
["class-variance-authority", "clsx", "tailwind-merge"],
|
|
364
|
+
config.root,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
log();
|
|
368
|
+
logStep("🎉", c("green", "Project initialized! Start adding components:"));
|
|
369
|
+
log();
|
|
370
|
+
log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add button")}`);
|
|
371
|
+
log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add card badge tabs")}`);
|
|
372
|
+
log();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function cmdAdd(names, flags = {}) {
|
|
376
|
+
log();
|
|
377
|
+
log(` ${c("bold", "Unified UI")} ${c("dim", "— Add components")}`);
|
|
378
|
+
log();
|
|
379
|
+
|
|
380
|
+
const config = loadConfig();
|
|
381
|
+
|
|
382
|
+
if (!existsSync(join(config.root, CONFIG_FILE)) && !flags.yes) {
|
|
383
|
+
const init = await confirm(
|
|
384
|
+
`No ${c("cyan", CONFIG_FILE)} found. Initialize first?`,
|
|
385
|
+
);
|
|
386
|
+
if (init) {
|
|
387
|
+
await cmdInit();
|
|
388
|
+
log();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// If --all, fetch index and add everything
|
|
393
|
+
if (flags.all) {
|
|
394
|
+
logStep("📡", "Fetching full registry index...");
|
|
395
|
+
try {
|
|
396
|
+
const index = await fetchJSON(`${REGISTRY_BASE_URL}/index.json`);
|
|
397
|
+
names = index.items
|
|
398
|
+
.filter((item) => item.type === "unified-ui:component")
|
|
399
|
+
.map((item) => item.name);
|
|
400
|
+
logStep("✓", `Found ${c("cyan", names.length.toString())} components`);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
logError(`Could not fetch registry index: ${err.message}`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (names.length === 0) {
|
|
408
|
+
logError("No component names specified. Usage: npx @work-rjkashyap/unified-ui add <component...>");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Resolve the full dependency tree
|
|
413
|
+
logStep("🔍", `Resolving dependencies for: ${c("cyan", names.join(", "))}...`);
|
|
414
|
+
const tree = await resolveFullDependencyTree(names, REGISTRY_BASE_URL);
|
|
415
|
+
|
|
416
|
+
if (tree.size === 0) {
|
|
417
|
+
logError("No components resolved. Check the names and try again.");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Summarize what will be installed
|
|
422
|
+
const components = [];
|
|
423
|
+
const utils = [];
|
|
424
|
+
const styles = [];
|
|
425
|
+
const allNpmDeps = new Set();
|
|
426
|
+
|
|
427
|
+
for (const [name, item] of tree) {
|
|
428
|
+
switch (item.type) {
|
|
429
|
+
case "unified-ui:component":
|
|
430
|
+
components.push(name);
|
|
431
|
+
break;
|
|
432
|
+
case "unified-ui:util":
|
|
433
|
+
utils.push(name);
|
|
434
|
+
break;
|
|
435
|
+
case "unified-ui:styles":
|
|
436
|
+
styles.push(name);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const dep of item.dependencies || []) {
|
|
441
|
+
allNpmDeps.add(dep);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
log();
|
|
446
|
+
if (components.length > 0) {
|
|
447
|
+
logStep("🧩", `Components: ${c("cyan", components.join(", "))}`);
|
|
448
|
+
}
|
|
449
|
+
if (utils.length > 0) {
|
|
450
|
+
logStep("🔧", `Utilities: ${c("dim", utils.join(", "))}`);
|
|
451
|
+
}
|
|
452
|
+
if (allNpmDeps.size > 0) {
|
|
453
|
+
logStep("📦", `Packages: ${c("dim", [...allNpmDeps].join(", "))}`);
|
|
454
|
+
}
|
|
455
|
+
log();
|
|
456
|
+
|
|
457
|
+
// Confirm unless --yes
|
|
458
|
+
if (!flags.yes) {
|
|
459
|
+
const proceed = await confirm("Proceed with installation?");
|
|
460
|
+
if (!proceed) {
|
|
461
|
+
logStep("↩", "Cancelled.");
|
|
462
|
+
log();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
log();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Write files
|
|
469
|
+
const results = [];
|
|
470
|
+
const overwrite = flags.overwrite || false;
|
|
471
|
+
|
|
472
|
+
for (const [_name, item] of tree) {
|
|
473
|
+
for (const file of item.files) {
|
|
474
|
+
const targetPath = resolveTargetPath(config, file);
|
|
475
|
+
const result = writeFile(targetPath, file.content, config, overwrite);
|
|
476
|
+
results.push(result);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Report file results
|
|
481
|
+
const created = results.filter((r) => r.status === "created");
|
|
482
|
+
const skipped = results.filter((r) => r.status === "skipped");
|
|
483
|
+
|
|
484
|
+
for (const r of created) {
|
|
485
|
+
logStep("✓", c("green", r.path.replace(config.root + "/", "")));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (skipped.length > 0) {
|
|
489
|
+
log();
|
|
490
|
+
for (const r of skipped) {
|
|
491
|
+
logStep(
|
|
492
|
+
"↩",
|
|
493
|
+
`${c("dim", r.path.replace(config.root + "/", ""))} ${c("yellow", "(exists, skipped)")}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
log();
|
|
497
|
+
logStep(
|
|
498
|
+
"💡",
|
|
499
|
+
c("dim", "Use --overwrite to replace existing files."),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Install npm dependencies
|
|
504
|
+
const depsToInstall = [...allNpmDeps].filter((dep) => {
|
|
505
|
+
// Check if already in package.json
|
|
506
|
+
try {
|
|
507
|
+
const pkg = JSON.parse(
|
|
508
|
+
readFileSync(join(config.root, "package.json"), "utf-8"),
|
|
509
|
+
);
|
|
510
|
+
const allPkgDeps = {
|
|
511
|
+
...pkg.dependencies,
|
|
512
|
+
...pkg.devDependencies,
|
|
513
|
+
...pkg.peerDependencies,
|
|
514
|
+
};
|
|
515
|
+
return !allPkgDeps[dep];
|
|
516
|
+
} catch {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (depsToInstall.length > 0) {
|
|
522
|
+
log();
|
|
523
|
+
await installNpmDeps(depsToInstall, config.root);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
log();
|
|
527
|
+
logStep(
|
|
528
|
+
"🎉",
|
|
529
|
+
c("green", `Done! ${created.length} file(s) added.`),
|
|
530
|
+
);
|
|
531
|
+
log();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function cmdList() {
|
|
535
|
+
log();
|
|
536
|
+
log(` ${c("bold", "Unified UI")} ${c("dim", "— Available components")}`);
|
|
537
|
+
log();
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const index = await fetchJSON(`${REGISTRY_BASE_URL}/index.json`);
|
|
541
|
+
|
|
542
|
+
// Group by category
|
|
543
|
+
const groups = {};
|
|
544
|
+
for (const item of index.items) {
|
|
545
|
+
if (item.type !== "unified-ui:component") continue;
|
|
546
|
+
const cat = item.category || "other";
|
|
547
|
+
if (!groups[cat]) groups[cat] = [];
|
|
548
|
+
groups[cat].push(item);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Find category labels
|
|
552
|
+
const catLabels = {};
|
|
553
|
+
for (const cat of index.categories || []) {
|
|
554
|
+
catLabels[cat.name] = cat.label;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
for (const [cat, items] of Object.entries(groups)) {
|
|
558
|
+
log(` ${c("bold", catLabels[cat] || cat)}`);
|
|
559
|
+
for (const item of items) {
|
|
560
|
+
const deps =
|
|
561
|
+
item.registryDependencies?.length > 0
|
|
562
|
+
? c("dim", ` → ${item.registryDependencies.join(", ")}`)
|
|
563
|
+
: "";
|
|
564
|
+
log(` ${c("cyan", item.name.padEnd(22))} ${c("dim", item.description || "")}${deps}`);
|
|
565
|
+
}
|
|
566
|
+
log();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
log(` ${c("dim", `${index.totalItems} items total`)}`);
|
|
570
|
+
log();
|
|
571
|
+
log(` ${c("dim", "Add a component:")} npx @work-rjkashyap/unified-ui add ${c("cyan", "<name>")}`);
|
|
572
|
+
log();
|
|
573
|
+
} catch (err) {
|
|
574
|
+
logError(`Could not fetch registry: ${err.message}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function cmdDiff(names) {
|
|
579
|
+
log();
|
|
580
|
+
log(` ${c("bold", "Unified UI")} ${c("dim", "— Diff local vs registry")}`);
|
|
581
|
+
log();
|
|
582
|
+
|
|
583
|
+
const config = loadConfig();
|
|
584
|
+
|
|
585
|
+
for (const name of names) {
|
|
586
|
+
try {
|
|
587
|
+
const item = await fetchJSON(`${REGISTRY_BASE_URL}/${name}.json`);
|
|
588
|
+
for (const file of item.files) {
|
|
589
|
+
const targetPath = resolveTargetPath(config, file);
|
|
590
|
+
if (!existsSync(targetPath)) {
|
|
591
|
+
logStep("✗", `${c("red", name)}: not installed locally`);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const localContent = readFileSync(targetPath, "utf-8");
|
|
596
|
+
const registryContent = rewriteContentImports(file.content, config);
|
|
597
|
+
|
|
598
|
+
if (localContent === registryContent) {
|
|
599
|
+
logStep("✓", `${c("green", name)}: up to date`);
|
|
600
|
+
} else {
|
|
601
|
+
logStep("~", `${c("yellow", name)}: local changes detected`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} catch (err) {
|
|
605
|
+
logStep("✗", `${c("red", name)}: ${err.message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
log();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function cmdHelp() {
|
|
613
|
+
log();
|
|
614
|
+
log(` ${c("bold", "Unified UI")} ${c("dim", "— Component Registry CLI")}`);
|
|
615
|
+
log();
|
|
616
|
+
log(" Usage:");
|
|
617
|
+
log(` ${c("cyan", "npx @work-rjkashyap/unified-ui")} ${c("green", "<command>")} [options]`);
|
|
618
|
+
log();
|
|
619
|
+
log(" Commands:");
|
|
620
|
+
log(` ${c("green", "init")} Initialize project config & base utils`);
|
|
621
|
+
log(` ${c("green", "add")} <component...> Add component(s) with dependencies`);
|
|
622
|
+
log(` ${c("green", "add")} --all Add all components`);
|
|
623
|
+
log(` ${c("green", "list")} List all available components`);
|
|
624
|
+
log(` ${c("green", "diff")} <component...> Compare local files with registry`);
|
|
625
|
+
log(` ${c("green", "help")} Show this help message`);
|
|
626
|
+
log();
|
|
627
|
+
log(" Options:");
|
|
628
|
+
log(` ${c("yellow", "--yes, -y")} Skip confirmation prompts`);
|
|
629
|
+
log(` ${c("yellow", "--overwrite")} Overwrite existing files`);
|
|
630
|
+
log(` ${c("yellow", "--all")} Add all components (with 'add')`);
|
|
631
|
+
log();
|
|
632
|
+
log(" Examples:");
|
|
633
|
+
log(` ${c("dim", "# Initialize project")} `);
|
|
634
|
+
log(` npx @work-rjkashyap/unified-ui init`);
|
|
635
|
+
log();
|
|
636
|
+
log(` ${c("dim", "# Add specific components")}`);
|
|
637
|
+
log(` npx @work-rjkashyap/unified-ui add button card badge`);
|
|
638
|
+
log();
|
|
639
|
+
log(` ${c("dim", "# Add all components, skip prompts")}`);
|
|
640
|
+
log(` npx @work-rjkashyap/unified-ui add --all -y`);
|
|
641
|
+
log();
|
|
642
|
+
log(` ${c("dim", "# Check for upstream changes")}`);
|
|
643
|
+
log(` npx @work-rjkashyap/unified-ui diff button card`);
|
|
644
|
+
log();
|
|
645
|
+
log(` Registry: ${c("cyan", REGISTRY_BASE_URL)}`);
|
|
646
|
+
log();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Argument parsing
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
function parseArgs(argv) {
|
|
654
|
+
const args = argv.slice(2);
|
|
655
|
+
const command = args[0];
|
|
656
|
+
const flags = {};
|
|
657
|
+
const positional = [];
|
|
658
|
+
|
|
659
|
+
for (let i = 1; i < args.length; i++) {
|
|
660
|
+
const arg = args[i];
|
|
661
|
+
if (arg === "--yes" || arg === "-y") {
|
|
662
|
+
flags.yes = true;
|
|
663
|
+
} else if (arg === "--overwrite") {
|
|
664
|
+
flags.overwrite = true;
|
|
665
|
+
} else if (arg === "--all") {
|
|
666
|
+
flags.all = true;
|
|
667
|
+
} else if (arg.startsWith("--registry=")) {
|
|
668
|
+
flags.registryUrl = arg.split("=")[1];
|
|
669
|
+
} else if (!arg.startsWith("-")) {
|
|
670
|
+
positional.push(arg);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return { command, positional, flags };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// Main
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
async function main() {
|
|
682
|
+
const { command, positional, flags } = parseArgs(process.argv);
|
|
683
|
+
|
|
684
|
+
// Allow custom registry URL
|
|
685
|
+
if (flags.registryUrl) {
|
|
686
|
+
// Override the global — we use a let binding workaround below.
|
|
687
|
+
// (The const REGISTRY_BASE_URL is used by all functions via closure.)
|
|
688
|
+
// For simplicity, we set an env var that's already checked at the top.
|
|
689
|
+
process.env.UNIFIED_UI_REGISTRY_URL = flags.registryUrl;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
switch (command) {
|
|
693
|
+
case "init":
|
|
694
|
+
await cmdInit();
|
|
695
|
+
break;
|
|
696
|
+
case "add":
|
|
697
|
+
await cmdAdd(positional, flags);
|
|
698
|
+
break;
|
|
699
|
+
case "list":
|
|
700
|
+
case "ls":
|
|
701
|
+
await cmdList();
|
|
702
|
+
break;
|
|
703
|
+
case "diff":
|
|
704
|
+
await cmdDiff(positional);
|
|
705
|
+
break;
|
|
706
|
+
case "help":
|
|
707
|
+
case "--help":
|
|
708
|
+
case "-h":
|
|
709
|
+
case undefined:
|
|
710
|
+
cmdHelp();
|
|
711
|
+
break;
|
|
712
|
+
default:
|
|
713
|
+
logError(`Unknown command: "${command}". Run "npx @work-rjkashyap/unified-ui help" for usage.`);
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
main().catch((err) => {
|
|
719
|
+
logError(err.message);
|
|
720
|
+
process.exit(1);
|
|
721
|
+
});
|