@brxndxndiaz/ui 0.1.1 → 0.1.3

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 CHANGED
@@ -5,7 +5,9 @@ CLI for the brxndxndiaz UI registry.
5
5
  ## Usage
6
6
 
7
7
  ```bash
8
+ npx @brxndxndiaz/ui init
8
9
  npx @brxndxndiaz/ui add animated-timeline
10
+ npx @brxndxndiaz/ui add
9
11
  ```
10
12
 
11
13
  This pulls registry items from the public registry and writes the component files into your project.
@@ -3,6 +3,8 @@
3
3
 
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
+ const { spawnSync } = require("child_process");
7
+ const readline = require("readline");
6
8
 
7
9
  const REGISTRY_BASE = "https://ui.brndndiaz.dev/r";
8
10
  const SOURCE_BASE = process.env.BRXN_SOURCE_BASE;
@@ -11,10 +13,13 @@ function printHelp() {
11
13
  console.log(`brxndxndiaz-ui
12
14
 
13
15
  Usage:
14
- brxndxndiaz-ui add <component>
16
+ brxndxndiaz-ui init
17
+ brxndxndiaz-ui add [component]
15
18
 
16
19
  Examples:
20
+ brxndxndiaz-ui init
17
21
  brxndxndiaz-ui add animated-timeline
22
+ brxndxndiaz-ui add
18
23
  `);
19
24
  }
20
25
 
@@ -40,15 +45,257 @@ async function writeFile(targetPath, content) {
40
45
  await fs.promises.writeFile(fullPath, content, "utf8");
41
46
  }
42
47
 
43
- async function addComponent(name) {
48
+ function fileExists(filePath) {
49
+ try {
50
+ fs.accessSync(filePath, fs.constants.F_OK);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function readJson(filePath) {
58
+ try {
59
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function prompt(question) {
66
+ const rl = readline.createInterface({
67
+ input: process.stdin,
68
+ output: process.stdout,
69
+ });
70
+ return new Promise((resolve) => {
71
+ rl.question(question, (answer) => {
72
+ rl.close();
73
+ resolve(answer.trim());
74
+ });
75
+ });
76
+ }
77
+
78
+ function parseSelection(input, max) {
79
+ const normalized = input.replace(/\s+/g, "").toLowerCase();
80
+ if (!normalized) return [];
81
+ if (normalized === "all" || normalized === "*") {
82
+ return Array.from({ length: max }, (_, i) => i + 1);
83
+ }
84
+
85
+ const parts = normalized.split(",").filter(Boolean);
86
+ const result = new Set();
87
+
88
+ for (const part of parts) {
89
+ if (part.includes("-")) {
90
+ const [startRaw, endRaw] = part.split("-");
91
+ const start = Number(startRaw);
92
+ const end = Number(endRaw);
93
+ if (!Number.isInteger(start) || !Number.isInteger(end)) continue;
94
+ const from = Math.min(start, end);
95
+ const to = Math.max(start, end);
96
+ for (let i = from; i <= to; i += 1) {
97
+ if (i >= 1 && i <= max) result.add(i);
98
+ }
99
+ } else {
100
+ const index = Number(part);
101
+ if (Number.isInteger(index) && index >= 1 && index <= max) {
102
+ result.add(index);
103
+ }
104
+ }
105
+ }
106
+
107
+ return Array.from(result.values());
108
+ }
109
+
110
+ function detectPackageManager(projectRoot) {
111
+ if (fileExists(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
112
+ if (fileExists(path.join(projectRoot, "yarn.lock"))) return "yarn";
113
+ if (fileExists(path.join(projectRoot, "bun.lock")) || fileExists(path.join(projectRoot, "bun.lockb"))) {
114
+ return "bun";
115
+ }
116
+ return "npm";
117
+ }
118
+
119
+ function installDependencies(dependencies, devDependencies) {
120
+ const deps = dependencies || [];
121
+ const devDeps = devDependencies || [];
122
+ if (deps.length === 0 && devDeps.length === 0) return;
123
+ const projectRoot = process.cwd();
124
+ if (!fileExists(path.join(projectRoot, "package.json"))) {
125
+ console.warn("Skipping dependency install: package.json not found.");
126
+ return;
127
+ }
128
+ const pkgManager = detectPackageManager(projectRoot);
129
+ const installArgs = (items, isDev) => {
130
+ if (items.length === 0) return null;
131
+ if (pkgManager === "npm") {
132
+ return isDev ? ["install", "--save-dev", ...items] : ["install", ...items];
133
+ }
134
+ if (pkgManager === "yarn") {
135
+ return isDev ? ["add", "-D", ...items] : ["add", ...items];
136
+ }
137
+ if (pkgManager === "pnpm") {
138
+ return isDev ? ["add", "-D", ...items] : ["add", ...items];
139
+ }
140
+ if (pkgManager === "bun") {
141
+ return isDev ? ["add", "-d", ...items] : ["add", ...items];
142
+ }
143
+ return null;
144
+ };
145
+
146
+ if (deps.length > 0) {
147
+ console.log(`Installing dependencies with ${pkgManager}: ${deps.join(", ")}`);
148
+ const args = installArgs(deps, false);
149
+ const result = spawnSync(pkgManager, args, { stdio: "inherit" });
150
+ if (result.status !== 0) {
151
+ throw new Error(`Failed to install dependencies with ${pkgManager}.`);
152
+ }
153
+ }
154
+
155
+ if (devDeps.length > 0) {
156
+ console.log(`Installing dev dependencies with ${pkgManager}: ${devDeps.join(", ")}`);
157
+ const args = installArgs(devDeps, true);
158
+ const result = spawnSync(pkgManager, args, { stdio: "inherit" });
159
+ if (result.status !== 0) {
160
+ throw new Error(`Failed to install dev dependencies with ${pkgManager}.`);
161
+ }
162
+ }
163
+ }
164
+
165
+ function resolveTargetPath(targetPath) {
166
+ return targetPath.replace(/\\/g, "/");
167
+ }
168
+
169
+ function resolveAliasPath(alias, projectRoot) {
170
+ if (!alias || typeof alias !== "string") return null;
171
+ if (!alias.startsWith("@/")) return null;
172
+ const relPath = alias.slice(2);
173
+ const directPath = path.join(projectRoot, relPath);
174
+ if (fileExists(directPath)) return relPath.replace(/\\/g, "/");
175
+ const srcPath = path.join(projectRoot, "src", relPath);
176
+ if (fileExists(srcPath)) return path.join("src", relPath).replace(/\\/g, "/");
177
+ return null;
178
+ }
179
+
180
+ function detectComponentsRoot(projectRoot) {
181
+ const configPath = path.join(projectRoot, "components.json");
182
+ if (fileExists(configPath)) {
183
+ const config = readJson(configPath);
184
+ const aliasRoot = resolveAliasPath(config?.aliases?.components, projectRoot);
185
+ if (aliasRoot) return aliasRoot;
186
+ }
187
+
188
+ const candidates = [
189
+ "src/components",
190
+ "components",
191
+ "src/app/components",
192
+ "app/components",
193
+ ];
194
+ const matches = candidates.filter((candidate) =>
195
+ fileExists(path.join(projectRoot, candidate))
196
+ );
197
+
198
+ if (matches.length === 1) return matches[0];
199
+
200
+ return matches.length ? matches : null;
201
+ }
202
+
203
+ async function promptForComponentsRoot(projectRoot) {
204
+ const detection = detectComponentsRoot(projectRoot);
205
+ if (typeof detection === "string") return detection;
206
+
207
+ const defaults = ["components", "src/components", "app/components", "src/app/components"];
208
+ const options = detection && Array.isArray(detection) ? detection : defaults;
209
+
210
+ console.log("\nSelect a components directory:");
211
+ options.forEach((option, index) => {
212
+ console.log(` ${index + 1}) ${option}`);
213
+ });
214
+ const answer = await prompt("Enter a number (default 1): ");
215
+ const selection = parseSelection(answer || "1", options.length)[0] || 1;
216
+ const chosen = options[selection - 1] || options[0];
217
+ return chosen;
218
+ }
219
+
220
+ function resolveComponentTarget(targetPath, componentsRoot) {
221
+ if (!targetPath.startsWith("components/")) {
222
+ return resolveTargetPath(targetPath);
223
+ }
224
+ const suffix = targetPath.slice("components/".length);
225
+ const resolved = path.join(componentsRoot, suffix).replace(/\\/g, "/");
226
+ return resolved;
227
+ }
228
+
229
+ async function fetchRegistryIndex() {
230
+ const registryUrl = `${REGISTRY_BASE}/registry.json`;
231
+ const data = await fetchJson(registryUrl);
232
+ return data?.items || [];
233
+ }
234
+
235
+ function isComponentInstalled(item, componentsRoot) {
236
+ if (!item?.files?.length) return false;
237
+ return item.files.every((file) => {
238
+ const target = file.target || file.path;
239
+ const resolvedTarget = resolveComponentTarget(target, componentsRoot);
240
+ return fileExists(path.join(process.cwd(), resolvedTarget));
241
+ });
242
+ }
243
+
244
+ async function addComponent(
245
+ name,
246
+ componentsRoot,
247
+ seen = new Set(),
248
+ dependencies = new Set(),
249
+ devDependencies = new Set()
250
+ ) {
251
+ if (seen.has(name)) return;
252
+ seen.add(name);
253
+
44
254
  const registryUrl = `${REGISTRY_BASE}/${name}.json`;
45
- const item = await fetchJson(registryUrl);
255
+ let item;
256
+ try {
257
+ item = await fetchJson(registryUrl);
258
+ } catch (error) {
259
+ if (String(error.message).includes("404")) {
260
+ console.error(`Error: Component "${name}" not found in the registry.`);
261
+ console.error("Run `npx @brxndxndiaz/ui add` to select from available components.");
262
+ return;
263
+ }
264
+ throw error;
265
+ }
46
266
  if (!item?.files?.length) {
47
267
  throw new Error(`Registry item has no files: ${registryUrl}`);
48
268
  }
49
269
 
270
+ if (isComponentInstalled(item, componentsRoot)) {
271
+ console.log(`Skipped ${name}: already installed.`);
272
+ return;
273
+ }
274
+
275
+ if (item.registryDependencies && item.registryDependencies.length > 0) {
276
+ for (const dependency of item.registryDependencies) {
277
+ await addComponent(
278
+ dependency,
279
+ componentsRoot,
280
+ seen,
281
+ dependencies,
282
+ devDependencies
283
+ );
284
+ }
285
+ }
286
+ if (Array.isArray(item.dependencies)) {
287
+ item.dependencies.forEach((dep) => dependencies.add(dep));
288
+ }
289
+ if (Array.isArray(item.devDependencies)) {
290
+ item.devDependencies.forEach((dep) => devDependencies.add(dep));
291
+ }
292
+ if (Array.isArray(item.peerDependencies)) {
293
+ item.peerDependencies.forEach((dep) => dependencies.add(dep));
294
+ }
295
+
50
296
  for (const file of item.files) {
51
297
  const targetPath = file.target || file.path;
298
+ const resolvedTarget = resolveComponentTarget(targetPath, componentsRoot);
52
299
  let content = file.content;
53
300
  if (!content) {
54
301
  if (!SOURCE_BASE) {
@@ -60,11 +307,107 @@ async function addComponent(name) {
60
307
  const sourceUrl = `${SOURCE_BASE}/${sourcePath}`;
61
308
  content = await fetchText(sourceUrl);
62
309
  }
63
- await writeFile(targetPath, content);
64
- console.log(`Added ${targetPath}`);
310
+ await writeFile(resolvedTarget, content);
311
+ console.log(`Added ${resolvedTarget}`);
65
312
  }
66
313
  }
67
314
 
315
+ async function promptForRegistryComponents(componentsRoot) {
316
+ const registryIndex = await fetchRegistryIndex();
317
+ if (!registryIndex.length) {
318
+ throw new Error("Failed to fetch registry index.");
319
+ }
320
+
321
+ const available = registryIndex.filter((item) => !isComponentInstalled(item, componentsRoot));
322
+ if (!available.length) {
323
+ console.log("All available components are already installed.");
324
+ process.exit(0);
325
+ }
326
+
327
+ console.log("\nWhich components would you like to add?");
328
+ available.forEach((item, index) => {
329
+ console.log(` ${index + 1}) ${item.name}`);
330
+ });
331
+ console.log("Enter numbers separated by commas (or 'all'):");
332
+ const answer = await prompt("> ");
333
+ const selections = parseSelection(answer, available.length);
334
+ if (!selections.length) {
335
+ console.log("No components selected. Exiting.");
336
+ process.exit(1);
337
+ }
338
+ return selections.map((index) => available[index - 1].name);
339
+ }
340
+
341
+ function resolveTailwindCssPath(projectRoot) {
342
+ const candidates = [
343
+ "src/app/globals.css",
344
+ "app/globals.css",
345
+ "src/styles/globals.css",
346
+ "styles/globals.css",
347
+ ];
348
+ const match = candidates.find((candidate) =>
349
+ fileExists(path.join(projectRoot, candidate))
350
+ );
351
+ return match || candidates[0];
352
+ }
353
+
354
+ function resolveUtilsAlias(projectRoot) {
355
+ const candidates = ["src/lib/utils", "lib/utils"];
356
+ const match = candidates.find((candidate) =>
357
+ fileExists(path.join(projectRoot, `${candidate}.ts`)) ||
358
+ fileExists(path.join(projectRoot, `${candidate}.tsx`))
359
+ );
360
+ if (match?.startsWith("src/")) return `@/${match.slice(4)}`;
361
+ return match ? `@/${match}` : "@/lib/utils";
362
+ }
363
+
364
+ async function initProject() {
365
+ const projectRoot = process.cwd();
366
+ const configPath = path.join(projectRoot, "components.json");
367
+ if (fileExists(configPath)) {
368
+ console.log("components.json already exists. Skipping init.");
369
+ return;
370
+ }
371
+
372
+ const componentsRoot = await promptForComponentsRoot(projectRoot);
373
+ const tailwindCss = resolveTailwindCssPath(projectRoot);
374
+ const utilsAlias = resolveUtilsAlias(projectRoot);
375
+ const componentsAlias = componentsRoot.startsWith("src/")
376
+ ? `@/${componentsRoot.slice(4)}`
377
+ : `@/${componentsRoot}`;
378
+
379
+ const config = {
380
+ $schema: "https://ui.shadcn.com/schema.json",
381
+ style: "base-nova",
382
+ rsc: true,
383
+ tsx: true,
384
+ tailwind: {
385
+ config: "",
386
+ css: tailwindCss,
387
+ baseColor: "neutral",
388
+ cssVariables: true,
389
+ prefix: "",
390
+ },
391
+ iconLibrary: "lucide",
392
+ rtl: false,
393
+ aliases: {
394
+ components: componentsAlias,
395
+ utils: utilsAlias,
396
+ ui: componentsAlias,
397
+ lib: "@/lib",
398
+ hooks: "@/hooks",
399
+ },
400
+ menuColor: "default",
401
+ menuAccent: "subtle",
402
+ registries: {
403
+ "@brxndxndiaz/ui": "https://ui.brndndiaz.dev/r/{name}.json",
404
+ },
405
+ };
406
+
407
+ await writeFile("components.json", `${JSON.stringify(config, null, 2)}\n`);
408
+ console.log("Created components.json");
409
+ }
410
+
68
411
  async function main() {
69
412
  const [, , command, arg] = process.argv;
70
413
  if (!command || command === "-h" || command === "--help") {
@@ -72,12 +415,33 @@ async function main() {
72
415
  return;
73
416
  }
74
417
 
75
- if (command !== "add" || !arg) {
418
+ if (command === "init") {
419
+ await initProject();
420
+ return;
421
+ }
422
+
423
+ if (command !== "add") {
76
424
  printHelp();
77
425
  process.exit(1);
78
426
  }
79
427
 
80
- await addComponent(arg);
428
+ const projectRoot = process.cwd();
429
+ const componentsRoot = await promptForComponentsRoot(projectRoot);
430
+
431
+ let components = [];
432
+ if (arg) {
433
+ components = [arg];
434
+ } else {
435
+ components = await promptForRegistryComponents(componentsRoot);
436
+ }
437
+
438
+ const seen = new Set();
439
+ const dependencies = new Set();
440
+ const devDependencies = new Set();
441
+ for (const component of components) {
442
+ await addComponent(component, componentsRoot, seen, dependencies, devDependencies);
443
+ }
444
+ installDependencies(Array.from(dependencies), Array.from(devDependencies));
81
445
  }
82
446
 
83
447
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brxndxndiaz/ui",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "CLI for the brxndxndiaz UI registry.",
6
6
  "scripts": {