@brxndxndiaz/ui 0.1.1 → 0.1.2

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,218 @@ 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) {
120
+ if (!dependencies || dependencies.length === 0) return;
121
+ const projectRoot = process.cwd();
122
+ if (!fileExists(path.join(projectRoot, "package.json"))) {
123
+ console.warn("Skipping dependency install: package.json not found.");
124
+ return;
125
+ }
126
+ const pkgManager = detectPackageManager(projectRoot);
127
+ const args =
128
+ pkgManager === "yarn"
129
+ ? ["add", ...dependencies]
130
+ : pkgManager === "pnpm" || pkgManager === "bun"
131
+ ? ["add", ...dependencies]
132
+ : ["install", ...dependencies];
133
+ const result = spawnSync(pkgManager, args, { stdio: "inherit" });
134
+ if (result.status !== 0) {
135
+ throw new Error(`Failed to install dependencies with ${pkgManager}.`);
136
+ }
137
+ }
138
+
139
+ function resolveTargetPath(targetPath) {
140
+ return targetPath.replace(/\\/g, "/");
141
+ }
142
+
143
+ function resolveAliasPath(alias, projectRoot) {
144
+ if (!alias || typeof alias !== "string") return null;
145
+ if (!alias.startsWith("@/")) return null;
146
+ const relPath = alias.slice(2);
147
+ const directPath = path.join(projectRoot, relPath);
148
+ if (fileExists(directPath)) return relPath.replace(/\\/g, "/");
149
+ const srcPath = path.join(projectRoot, "src", relPath);
150
+ if (fileExists(srcPath)) return path.join("src", relPath).replace(/\\/g, "/");
151
+ return null;
152
+ }
153
+
154
+ function detectComponentsRoot(projectRoot) {
155
+ const configPath = path.join(projectRoot, "components.json");
156
+ if (fileExists(configPath)) {
157
+ const config = readJson(configPath);
158
+ const aliasRoot = resolveAliasPath(config?.aliases?.components, projectRoot);
159
+ if (aliasRoot) return aliasRoot;
160
+ }
161
+
162
+ const candidates = [
163
+ "src/components",
164
+ "components",
165
+ "src/app/components",
166
+ "app/components",
167
+ ];
168
+ const matches = candidates.filter((candidate) =>
169
+ fileExists(path.join(projectRoot, candidate))
170
+ );
171
+
172
+ if (matches.length === 1) return matches[0];
173
+
174
+ return matches.length ? matches : null;
175
+ }
176
+
177
+ async function promptForComponentsRoot(projectRoot) {
178
+ const detection = detectComponentsRoot(projectRoot);
179
+ if (typeof detection === "string") return detection;
180
+
181
+ const defaults = ["components", "src/components", "app/components", "src/app/components"];
182
+ const options = detection && Array.isArray(detection) ? detection : defaults;
183
+
184
+ console.log("\nSelect a components directory:");
185
+ options.forEach((option, index) => {
186
+ console.log(` ${index + 1}) ${option}`);
187
+ });
188
+ const answer = await prompt("Enter a number (default 1): ");
189
+ const selection = parseSelection(answer || "1", options.length)[0] || 1;
190
+ const chosen = options[selection - 1] || options[0];
191
+ return chosen;
192
+ }
193
+
194
+ function resolveComponentTarget(targetPath, componentsRoot) {
195
+ if (!targetPath.startsWith("components/")) {
196
+ return resolveTargetPath(targetPath);
197
+ }
198
+ const suffix = targetPath.slice("components/".length);
199
+ const resolved = path.join(componentsRoot, suffix).replace(/\\/g, "/");
200
+ return resolved;
201
+ }
202
+
203
+ async function fetchRegistryIndex() {
204
+ const registryUrl = `${REGISTRY_BASE}/registry.json`;
205
+ const data = await fetchJson(registryUrl);
206
+ return data?.items || [];
207
+ }
208
+
209
+ function isComponentInstalled(item, componentsRoot) {
210
+ if (!item?.files?.length) return false;
211
+ return item.files.every((file) => {
212
+ const target = file.target || file.path;
213
+ const resolvedTarget = resolveComponentTarget(target, componentsRoot);
214
+ return fileExists(path.join(process.cwd(), resolvedTarget));
215
+ });
216
+ }
217
+
218
+ async function addComponent(
219
+ name,
220
+ componentsRoot,
221
+ seen = new Set(),
222
+ dependencies = new Set()
223
+ ) {
224
+ if (seen.has(name)) return;
225
+ seen.add(name);
226
+
44
227
  const registryUrl = `${REGISTRY_BASE}/${name}.json`;
45
- const item = await fetchJson(registryUrl);
228
+ let item;
229
+ try {
230
+ item = await fetchJson(registryUrl);
231
+ } catch (error) {
232
+ if (String(error.message).includes("404")) {
233
+ console.error(`Error: Component "${name}" not found in the registry.`);
234
+ console.error("Run `npx @brxndxndiaz/ui add` to select from available components.");
235
+ return;
236
+ }
237
+ throw error;
238
+ }
46
239
  if (!item?.files?.length) {
47
240
  throw new Error(`Registry item has no files: ${registryUrl}`);
48
241
  }
49
242
 
243
+ if (isComponentInstalled(item, componentsRoot)) {
244
+ console.log(`Skipped ${name}: already installed.`);
245
+ return;
246
+ }
247
+
248
+ if (item.registryDependencies && item.registryDependencies.length > 0) {
249
+ for (const dependency of item.registryDependencies) {
250
+ await addComponent(dependency, componentsRoot, seen, dependencies);
251
+ }
252
+ }
253
+ if (item.dependencies && item.dependencies.length > 0) {
254
+ item.dependencies.forEach((dep) => dependencies.add(dep));
255
+ }
256
+
50
257
  for (const file of item.files) {
51
258
  const targetPath = file.target || file.path;
259
+ const resolvedTarget = resolveComponentTarget(targetPath, componentsRoot);
52
260
  let content = file.content;
53
261
  if (!content) {
54
262
  if (!SOURCE_BASE) {
@@ -60,11 +268,107 @@ async function addComponent(name) {
60
268
  const sourceUrl = `${SOURCE_BASE}/${sourcePath}`;
61
269
  content = await fetchText(sourceUrl);
62
270
  }
63
- await writeFile(targetPath, content);
64
- console.log(`Added ${targetPath}`);
271
+ await writeFile(resolvedTarget, content);
272
+ console.log(`Added ${resolvedTarget}`);
65
273
  }
66
274
  }
67
275
 
276
+ async function promptForRegistryComponents(componentsRoot) {
277
+ const registryIndex = await fetchRegistryIndex();
278
+ if (!registryIndex.length) {
279
+ throw new Error("Failed to fetch registry index.");
280
+ }
281
+
282
+ const available = registryIndex.filter((item) => !isComponentInstalled(item, componentsRoot));
283
+ if (!available.length) {
284
+ console.log("All available components are already installed.");
285
+ process.exit(0);
286
+ }
287
+
288
+ console.log("\nWhich components would you like to add?");
289
+ available.forEach((item, index) => {
290
+ console.log(` ${index + 1}) ${item.name}`);
291
+ });
292
+ console.log("Enter numbers separated by commas (or 'all'):");
293
+ const answer = await prompt("> ");
294
+ const selections = parseSelection(answer, available.length);
295
+ if (!selections.length) {
296
+ console.log("No components selected. Exiting.");
297
+ process.exit(1);
298
+ }
299
+ return selections.map((index) => available[index - 1].name);
300
+ }
301
+
302
+ function resolveTailwindCssPath(projectRoot) {
303
+ const candidates = [
304
+ "src/app/globals.css",
305
+ "app/globals.css",
306
+ "src/styles/globals.css",
307
+ "styles/globals.css",
308
+ ];
309
+ const match = candidates.find((candidate) =>
310
+ fileExists(path.join(projectRoot, candidate))
311
+ );
312
+ return match || candidates[0];
313
+ }
314
+
315
+ function resolveUtilsAlias(projectRoot) {
316
+ const candidates = ["src/lib/utils", "lib/utils"];
317
+ const match = candidates.find((candidate) =>
318
+ fileExists(path.join(projectRoot, `${candidate}.ts`)) ||
319
+ fileExists(path.join(projectRoot, `${candidate}.tsx`))
320
+ );
321
+ if (match?.startsWith("src/")) return `@/${match.slice(4)}`;
322
+ return match ? `@/${match}` : "@/lib/utils";
323
+ }
324
+
325
+ async function initProject() {
326
+ const projectRoot = process.cwd();
327
+ const configPath = path.join(projectRoot, "components.json");
328
+ if (fileExists(configPath)) {
329
+ console.log("components.json already exists. Skipping init.");
330
+ return;
331
+ }
332
+
333
+ const componentsRoot = await promptForComponentsRoot(projectRoot);
334
+ const tailwindCss = resolveTailwindCssPath(projectRoot);
335
+ const utilsAlias = resolveUtilsAlias(projectRoot);
336
+ const componentsAlias = componentsRoot.startsWith("src/")
337
+ ? `@/${componentsRoot.slice(4)}`
338
+ : `@/${componentsRoot}`;
339
+
340
+ const config = {
341
+ $schema: "https://ui.shadcn.com/schema.json",
342
+ style: "base-nova",
343
+ rsc: true,
344
+ tsx: true,
345
+ tailwind: {
346
+ config: "",
347
+ css: tailwindCss,
348
+ baseColor: "neutral",
349
+ cssVariables: true,
350
+ prefix: "",
351
+ },
352
+ iconLibrary: "lucide",
353
+ rtl: false,
354
+ aliases: {
355
+ components: componentsAlias,
356
+ utils: utilsAlias,
357
+ ui: componentsAlias,
358
+ lib: "@/lib",
359
+ hooks: "@/hooks",
360
+ },
361
+ menuColor: "default",
362
+ menuAccent: "subtle",
363
+ registries: {
364
+ "@brxndxndiaz/ui": "https://ui.brndndiaz.dev/r/{name}.json",
365
+ },
366
+ };
367
+
368
+ await writeFile("components.json", `${JSON.stringify(config, null, 2)}\n`);
369
+ console.log("Created components.json");
370
+ }
371
+
68
372
  async function main() {
69
373
  const [, , command, arg] = process.argv;
70
374
  if (!command || command === "-h" || command === "--help") {
@@ -72,12 +376,32 @@ async function main() {
72
376
  return;
73
377
  }
74
378
 
75
- if (command !== "add" || !arg) {
379
+ if (command === "init") {
380
+ await initProject();
381
+ return;
382
+ }
383
+
384
+ if (command !== "add") {
76
385
  printHelp();
77
386
  process.exit(1);
78
387
  }
79
388
 
80
- await addComponent(arg);
389
+ const projectRoot = process.cwd();
390
+ const componentsRoot = await promptForComponentsRoot(projectRoot);
391
+
392
+ let components = [];
393
+ if (arg) {
394
+ components = [arg];
395
+ } else {
396
+ components = await promptForRegistryComponents(componentsRoot);
397
+ }
398
+
399
+ const seen = new Set();
400
+ const dependencies = new Set();
401
+ for (const component of components) {
402
+ await addComponent(component, componentsRoot, seen, dependencies);
403
+ }
404
+ installDependencies(Array.from(dependencies));
81
405
  }
82
406
 
83
407
  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.2",
4
4
  "private": false,
5
5
  "description": "CLI for the brxndxndiaz UI registry.",
6
6
  "scripts": {