basuicn 0.1.5 → 0.1.7

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/dist/ui-cli.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ #!/usr/bin/env node
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -26,12 +27,42 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
27
  var import_fs = __toESM(require("fs"), 1);
27
28
  var import_path = __toESM(require("path"), 1);
28
29
  var import_child_process = require("child_process");
30
+ var import_readline = __toESM(require("readline"), 1);
31
+ var VERSION = "0.1.6";
29
32
  var REGISTRY_LOCAL = "./registry.json";
30
- var REGISTRY_REMOTE = "https://raw.githubusercontent.com/huy14032003/ui-component/main/registry.json";
31
- var log = (msg) => console.log(`[basuicn] ${msg}`);
32
- var warn = (msg) => console.warn(`[basuicn] WARN: ${msg}`);
33
- var error = (msg) => console.error(`[basuicn] ERROR: ${msg}`);
33
+ var REGISTRY_REMOTE = "https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json";
34
+ var c = {
35
+ reset: "\x1B[0m",
36
+ bold: "\x1B[1m",
37
+ dim: "\x1B[2m",
38
+ green: "\x1B[32m",
39
+ yellow: "\x1B[33m",
40
+ red: "\x1B[31m",
41
+ cyan: "\x1B[36m",
42
+ magenta: "\x1B[35m",
43
+ blue: "\x1B[34m",
44
+ gray: "\x1B[90m"
45
+ };
46
+ var log = (msg) => console.log(`${c.cyan}\u25B8${c.reset} ${msg}`);
47
+ var ok = (msg) => console.log(`${c.green}\u2714${c.reset} ${msg}`);
48
+ var warn = (msg) => console.warn(`${c.yellow}\u26A0${c.reset} ${msg}`);
49
+ var error = (msg) => console.error(`${c.red}\u2716${c.reset} ${msg}`);
34
50
  var getTargetProjectDir = () => process.cwd();
51
+ var ask = (question) => {
52
+ const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
53
+ return new Promise((resolve) => {
54
+ rl.question(`${c.cyan}?${c.reset} ${question} `, (answer) => {
55
+ rl.close();
56
+ resolve(answer.trim());
57
+ });
58
+ });
59
+ };
60
+ var confirm = async (question, defaultYes = true) => {
61
+ const hint = defaultYes ? "Y/n" : "y/N";
62
+ const answer = await ask(`${question} ${c.dim}(${hint})${c.reset}`);
63
+ if (!answer) return defaultYes;
64
+ return answer.toLowerCase().startsWith("y");
65
+ };
35
66
  var validateRegistry = (data) => {
36
67
  if (!data || typeof data !== "object") return false;
37
68
  const reg = data;
@@ -78,7 +109,7 @@ var installNpmPackages = (packages, cwd, dev = false) => {
78
109
  toInstall = packages.filter((p) => !allDeps[p]);
79
110
  }
80
111
  if (toInstall.length === 0) return;
81
- log(`Installing: ${toInstall.join(", ")}...`);
112
+ log(`Installing: ${c.bold}${toInstall.join(", ")}${c.reset}...`);
82
113
  const flag = dev ? "--save-dev" : "--save";
83
114
  try {
84
115
  (0, import_child_process.execSync)(`npm install ${toInstall.join(" ")} ${flag}`, { stdio: "inherit", cwd });
@@ -86,22 +117,27 @@ var installNpmPackages = (packages, cwd, dev = false) => {
86
117
  error(`Failed to install packages: ${toInstall.join(", ")}. ${err instanceof Error ? err.message : ""}`);
87
118
  }
88
119
  };
89
- var VITE_DEV_PACKAGES = ["tailwindcss", "@tailwindcss/vite", "@vitejs/plugin-react", "vite-plugin-babel", "babel-plugin-react-compiler", "@types/node"];
90
- var RUNTIME_PACKAGES = ["@base-ui/react", "tailwind-variants", "clsx", "tailwind-merge", "tailwindcss-animate"];
120
+ var VITE_DEV_PACKAGES = [
121
+ "tailwindcss",
122
+ "@tailwindcss/vite",
123
+ "@vitejs/plugin-react",
124
+ "@types/node"
125
+ ];
126
+ var RUNTIME_PACKAGES = [
127
+ "@base-ui/react",
128
+ "tailwind-variants",
129
+ "clsx",
130
+ "tailwind-merge",
131
+ "tailwindcss-animate",
132
+ "lucide-react"
133
+ ];
91
134
  var VITE_CONFIG_TEMPLATE = `import { defineConfig } from 'vite';
92
135
  import tailwindcss from '@tailwindcss/vite';
93
136
  import react from '@vitejs/plugin-react';
94
- import babel from 'vite-plugin-babel';
95
- import { reactCompilerPreset } from 'babel-plugin-react-compiler';
96
137
  import path from 'path';
97
138
 
98
- // https://vite.dev/config/
99
139
  export default defineConfig({
100
- plugins: [
101
- tailwindcss(),
102
- react(),
103
- babel({ presets: [reactCompilerPreset()] }),
104
- ],
140
+ plugins: [tailwindcss(), react()],
105
141
  resolve: {
106
142
  alias: {
107
143
  '@': path.resolve(__dirname, './src'),
@@ -128,7 +164,7 @@ var setupViteConfig = (cwd) => {
128
164
  const configJs = import_path.default.join(cwd, "vite.config.js");
129
165
  if (!import_fs.default.existsSync(configTs) && !import_fs.default.existsSync(configJs)) {
130
166
  import_fs.default.writeFileSync(configTs, VITE_CONFIG_TEMPLATE);
131
- log("Created vite.config.ts with Tailwind + React Compiler setup.");
167
+ ok("Created vite.config.ts.");
132
168
  return;
133
169
  }
134
170
  const existingPath = import_fs.default.existsSync(configTs) ? configTs : configJs;
@@ -136,16 +172,13 @@ var setupViteConfig = (cwd) => {
136
172
  const missingImports = [];
137
173
  if (!content.includes("@tailwindcss/vite")) missingImports.push("import tailwindcss from '@tailwindcss/vite';");
138
174
  if (!content.includes("@vitejs/plugin-react")) missingImports.push("import react from '@vitejs/plugin-react';");
139
- if (!content.includes("vite-plugin-babel")) missingImports.push("import babel from 'vite-plugin-babel';");
140
- if (!content.includes("babel-plugin-react-compiler")) missingImports.push("import { reactCompilerPreset } from 'babel-plugin-react-compiler';");
141
175
  if (!content.includes("from 'path'") && !content.includes('from "path"')) missingImports.push("import path from 'path';");
142
176
  const missingPlugins = [];
143
177
  if (!content.includes("tailwindcss()")) missingPlugins.push("tailwindcss()");
144
178
  if (!content.includes("react()") && !content.includes("react({")) missingPlugins.push("react()");
145
- if (!content.includes("reactCompilerPreset")) missingPlugins.push("babel({ presets: [reactCompilerPreset()] })");
146
- const hasAlias = content.includes("alias:") || content.includes("alias(") || content.includes("'@'") || content.includes('"@"');
179
+ const hasAlias = content.includes("alias:") || content.includes("'@'") || content.includes('"@"');
147
180
  if (missingImports.length === 0 && missingPlugins.length === 0 && hasAlias) {
148
- log("vite.config already configured \u2014 skipping.");
181
+ ok("vite.config already configured \u2014 skipping.");
149
182
  return;
150
183
  }
151
184
  if (missingImports.length > 0) {
@@ -203,29 +236,7 @@ var setupViteConfig = (cwd) => {
203
236
  }
204
237
  }
205
238
  import_fs.default.writeFileSync(existingPath, content);
206
- log(`Updated ${import_path.default.basename(existingPath)} with Tailwind + React Compiler + path aliases.`);
207
- };
208
- var ensureTailwindCss = (cwd) => {
209
- const candidates = ["src/index.css", "src/App.css", "src/main.css"];
210
- for (const cssFile of candidates) {
211
- const cssPath = import_path.default.join(cwd, cssFile);
212
- if (import_fs.default.existsSync(cssPath)) {
213
- const content = import_fs.default.readFileSync(cssPath, "utf-8");
214
- if (!content.includes('@import "tailwindcss"') && !content.includes("@import 'tailwindcss'")) {
215
- import_fs.default.writeFileSync(cssPath, `@import "tailwindcss";
216
-
217
- ${content}`);
218
- log(`Added @import "tailwindcss" to ${cssFile}`);
219
- } else {
220
- log(`${cssFile} already imports Tailwind \u2014 skipping.`);
221
- }
222
- return;
223
- }
224
- }
225
- const srcDir = import_path.default.join(cwd, "src");
226
- if (!import_fs.default.existsSync(srcDir)) import_fs.default.mkdirSync(srcDir, { recursive: true });
227
- import_fs.default.writeFileSync(import_path.default.join(srcDir, "index.css"), '@import "tailwindcss";\n');
228
- log('Created src/index.css with @import "tailwindcss"');
239
+ ok(`Updated ${import_path.default.basename(existingPath)} with Tailwind + path aliases.`);
229
240
  };
230
241
  var setupTsConfig = (cwd) => {
231
242
  const candidates = ["tsconfig.app.json", "tsconfig.json"];
@@ -234,7 +245,7 @@ var setupTsConfig = (cwd) => {
234
245
  if (!import_fs.default.existsSync(configPath)) continue;
235
246
  const raw = import_fs.default.readFileSync(configPath, "utf-8");
236
247
  if (raw.includes('"@/*"') || raw.includes("'@/*'")) {
237
- log(`${candidate} already has path aliases \u2014 skipping.`);
248
+ ok(`${candidate} already has path aliases \u2014 skipping.`);
238
249
  return;
239
250
  }
240
251
  try {
@@ -244,7 +255,7 @@ var setupTsConfig = (cwd) => {
244
255
  parsed.compilerOptions.baseUrl = ".";
245
256
  parsed.compilerOptions.paths = TSCONFIG_PATHS;
246
257
  import_fs.default.writeFileSync(configPath, JSON.stringify(parsed, null, 2));
247
- log(`Added path aliases to ${candidate}.`);
258
+ ok(`Added path aliases to ${candidate}.`);
248
259
  } catch (err) {
249
260
  warn(`Could not auto-patch ${candidate}: ${err instanceof Error ? err.message : err}`);
250
261
  warn("Add these to compilerOptions manually:");
@@ -260,33 +271,119 @@ var setupTsConfig = (cwd) => {
260
271
  }
261
272
  const newConfig = { compilerOptions: { baseUrl: ".", paths: TSCONFIG_PATHS } };
262
273
  import_fs.default.writeFileSync(import_path.default.join(cwd, "tsconfig.json"), JSON.stringify(newConfig, null, 2));
263
- log("Created tsconfig.json with path aliases.");
274
+ ok("Created tsconfig.json with path aliases.");
264
275
  };
265
- var ensureCore = (registry, cwd) => {
276
+ var ensureCore = (registry, cwd, options = {}) => {
266
277
  const core = registry.core;
267
278
  if (!core) return;
268
279
  installNpmPackages(core.dependencies, cwd);
269
280
  for (const file of core.files) {
270
281
  const targetPath = import_path.default.join(cwd, file.path);
271
282
  const targetDir = import_path.default.dirname(targetPath);
272
- if (!import_fs.default.existsSync(targetDir)) {
273
- import_fs.default.mkdirSync(targetDir, { recursive: true });
283
+ if (!import_fs.default.existsSync(targetDir)) import_fs.default.mkdirSync(targetDir, { recursive: true });
284
+ if (import_fs.default.existsSync(targetPath) && !options.force) {
285
+ log(`Core file exists (skipping): ${c.dim}${file.path}${c.reset}`);
286
+ continue;
287
+ }
288
+ import_fs.default.writeFileSync(targetPath, file.content);
289
+ ok(`${import_fs.default.existsSync(targetPath) ? "Updated" : "Created"} core file: ${file.path}`);
290
+ }
291
+ };
292
+ var MAIN_PATCH_COMPONENTS = {
293
+ toast: {
294
+ import: "import { Toaster } from '@/components/ui/toast/Toaster';",
295
+ jsx: '<Toaster position="top-center" expand={true} richColors />'
296
+ }
297
+ };
298
+ var MAIN_CANDIDATES = ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx"];
299
+ var findMainFile = (cwd) => {
300
+ for (const c2 of MAIN_CANDIDATES) {
301
+ const p = import_path.default.join(cwd, c2);
302
+ if (import_fs.default.existsSync(p)) return p;
303
+ }
304
+ return null;
305
+ };
306
+ var insertImport = (content, importLine) => {
307
+ if (content.includes(importLine)) return content;
308
+ const allImports = [...content.matchAll(/^import\s.+$/gm)];
309
+ if (allImports.length > 0) {
310
+ const last = allImports[allImports.length - 1];
311
+ const pos = last.index + last[0].length;
312
+ return content.slice(0, pos) + "\n" + importLine + content.slice(pos);
313
+ }
314
+ return importLine + "\n" + content;
315
+ };
316
+ var patchMainTsx = (cwd) => {
317
+ const mainPath = findMainFile(cwd);
318
+ if (!mainPath) {
319
+ warn("Could not find entry file (src/main.tsx). Skipping main entry setup.");
320
+ return;
321
+ }
322
+ let content = import_fs.default.readFileSync(mainPath, "utf-8");
323
+ let changed = false;
324
+ const cssImportLine = "import './styles/index.css';";
325
+ const hasCssImport = content.includes("styles/index.css") || content.includes("index.css");
326
+ if (!hasCssImport) {
327
+ const firstImport = content.match(/^import\s/m);
328
+ if (firstImport?.index !== void 0) {
329
+ content = content.slice(0, firstImport.index) + cssImportLine + "\n" + content.slice(firstImport.index);
330
+ } else {
331
+ content = cssImportLine + "\n" + content;
274
332
  }
275
- if (!import_fs.default.existsSync(targetPath)) {
276
- import_fs.default.writeFileSync(targetPath, file.content);
277
- log(`Created core file: ${file.path}`);
333
+ changed = true;
334
+ } else if (!content.includes("styles/index.css")) {
335
+ content = insertImport(content, cssImportLine);
336
+ changed = true;
337
+ }
338
+ if (!content.includes("ThemeProvider")) {
339
+ content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
340
+ const wrapped = content.replace(/(<App\s*\/>)/g, "<ThemeProvider>\n $1\n </ThemeProvider>");
341
+ if (wrapped === content) {
342
+ warn("Could not locate <App /> in entry file \u2014 add <ThemeProvider> wrapper manually.");
343
+ } else {
344
+ content = wrapped;
278
345
  }
346
+ changed = true;
279
347
  }
348
+ if (changed) {
349
+ import_fs.default.writeFileSync(mainPath, content);
350
+ ok(`Patched ${import_path.default.relative(cwd, mainPath)}.`);
351
+ } else {
352
+ ok(`${import_path.default.relative(cwd, mainPath)} already configured \u2014 skipping.`);
353
+ }
354
+ };
355
+ var patchMainTsxComponent = (cwd, componentName) => {
356
+ const patch = MAIN_PATCH_COMPONENTS[componentName];
357
+ if (!patch) return;
358
+ const mainPath = findMainFile(cwd);
359
+ if (!mainPath) return;
360
+ let content = import_fs.default.readFileSync(mainPath, "utf-8");
361
+ const tagName = patch.jsx.match(/<(\w+)/)?.[1];
362
+ if (tagName && content.includes(`<${tagName}`)) return;
363
+ content = insertImport(content, patch.import);
364
+ const withProvider = content.replace(
365
+ /(<App\s*\/>)(\s*\n\s*<\/ThemeProvider>)/,
366
+ `$1
367
+ ${patch.jsx}$2`
368
+ );
369
+ if (withProvider !== content) {
370
+ import_fs.default.writeFileSync(mainPath, withProvider);
371
+ } else {
372
+ const fallback = content.replace(/(<App\s*\/>)/, `$1
373
+ ${patch.jsx}`);
374
+ if (fallback !== content) import_fs.default.writeFileSync(mainPath, fallback);
375
+ }
376
+ ok(`Added <${tagName}> to ${import_path.default.relative(cwd, mainPath)}.`);
280
377
  };
281
378
  var addComponent = (name, registry, cwd, options, added = /* @__PURE__ */ new Set()) => {
282
379
  if (added.has(name)) return;
283
380
  added.add(name);
284
381
  const component = registry.components[name];
285
382
  if (!component) {
286
- error(`Component "${name}" not found. Run 'list' to see available components.`);
383
+ error(`Component "${name}" not found. Run '${c.cyan}basuicn list${c.reset}' to see available components.`);
287
384
  return;
288
385
  }
289
- log(`Adding: ${name}...`);
386
+ log(`Adding: ${c.bold}${name}${c.reset}...`);
290
387
  ensureCore(registry, cwd);
291
388
  installNpmPackages(component.dependencies, cwd);
292
389
  if (component.internalDependencies) {
@@ -299,15 +396,13 @@ var addComponent = (name, registry, cwd, options, added = /* @__PURE__ */ new Se
299
396
  for (const file of component.files) {
300
397
  const targetPath = import_path.default.join(cwd, file.path);
301
398
  const targetDir = import_path.default.dirname(targetPath);
302
- if (!import_fs.default.existsSync(targetDir)) {
303
- import_fs.default.mkdirSync(targetDir, { recursive: true });
304
- }
399
+ if (!import_fs.default.existsSync(targetDir)) import_fs.default.mkdirSync(targetDir, { recursive: true });
305
400
  if (import_fs.default.existsSync(targetPath) && !options.force) {
306
- warn(`Skipped (exists): ${file.path} \u2014 use --force to overwrite`);
401
+ warn(`Skipped (exists): ${file.path} \u2014 use ${c.cyan}--force${c.reset} to overwrite`);
307
402
  continue;
308
403
  }
309
404
  import_fs.default.writeFileSync(targetPath, file.content);
310
- log(`Created: ${file.path}`);
405
+ ok(`Created: ${file.path}`);
311
406
  }
312
407
  };
313
408
  var removeComponent = (name, registry, cwd) => {
@@ -316,12 +411,12 @@ var removeComponent = (name, registry, cwd) => {
316
411
  error(`Component "${name}" not found.`);
317
412
  return;
318
413
  }
319
- log(`Removing: ${name}...`);
414
+ log(`Removing: ${c.bold}${name}${c.reset}...`);
320
415
  for (const file of component.files) {
321
416
  const targetPath = import_path.default.join(cwd, file.path);
322
417
  if (import_fs.default.existsSync(targetPath)) {
323
418
  import_fs.default.unlinkSync(targetPath);
324
- log(`Deleted: ${file.path}`);
419
+ ok(`Deleted: ${file.path}`);
325
420
  }
326
421
  }
327
422
  for (const file of component.files) {
@@ -329,107 +424,304 @@ var removeComponent = (name, registry, cwd) => {
329
424
  try {
330
425
  if (import_fs.default.existsSync(targetDir) && import_fs.default.readdirSync(targetDir).length === 0) {
331
426
  import_fs.default.rmdirSync(targetDir);
332
- log(`Removed empty dir: ${import_path.default.relative(cwd, targetDir)}`);
427
+ ok(`Removed empty dir: ${import_path.default.relative(cwd, targetDir)}`);
333
428
  }
334
429
  } catch (err) {
335
430
  warn(`Could not remove directory: ${err instanceof Error ? err.message : err}`);
336
431
  }
337
432
  }
338
433
  };
434
+ var HELP_MAIN = `
435
+ ${c.bold}${c.cyan}basuicn${c.reset} ${c.dim}v${VERSION}${c.reset} \u2014 Modern React UI Component CLI
436
+
437
+ ${c.bold}USAGE${c.reset}
438
+ ${c.cyan}npx basuicn${c.reset} ${c.green}<command>${c.reset} ${c.dim}[options]${c.reset}
439
+
440
+ ${c.bold}COMMANDS${c.reset}
441
+ ${c.green}init${c.reset} Initialize project: install deps, copy core files, patch entry
442
+ ${c.green}add${c.reset} ${c.dim}<name...>${c.reset} Add component(s) to your project
443
+ ${c.green}update${c.reset} ${c.dim}<name...>${c.reset} Update component(s) to latest registry version
444
+ ${c.green}diff${c.reset} ${c.dim}<name...>${c.reset} Show diff between local and registry version
445
+ ${c.green}remove${c.reset} ${c.dim}<name...>${c.reset} Remove component(s) from your project
446
+ ${c.green}list${c.reset} List all available components
447
+ ${c.green}doctor${c.reset} Check project health and configuration
448
+
449
+ ${c.bold}OPTIONS${c.reset}
450
+ ${c.cyan}--force${c.reset} Overwrite existing files when adding/updating
451
+ ${c.cyan}--local${c.reset} Use local registry.json instead of remote
452
+ ${c.cyan}--help, -h${c.reset} Show help (use with a command for detailed help)
453
+ ${c.cyan}--version, -v${c.reset} Show version
454
+
455
+ ${c.bold}QUICK START${c.reset}
456
+ ${c.dim}$${c.reset} npx basuicn init
457
+ ${c.dim}$${c.reset} npx basuicn add button input card
458
+ ${c.dim}$${c.reset} npx basuicn add toast
459
+
460
+ ${c.bold}EXAMPLES${c.reset}
461
+ ${c.dim}$${c.reset} npx basuicn add dialog --force ${c.dim}# Overwrite existing dialog${c.reset}
462
+ ${c.dim}$${c.reset} npx basuicn diff button ${c.dim}# See what changed since last update${c.reset}
463
+ ${c.dim}$${c.reset} npx basuicn doctor ${c.dim}# Diagnose missing deps/config${c.reset}
464
+
465
+ ${c.dim}Documentation: https://github.com/Basuicn/basuicn-core${c.reset}
466
+ `;
467
+ var HELP_COMMANDS = {
468
+ init: `
469
+ ${c.bold}basuicn init${c.reset}
470
+
471
+ Initialize your project for basuicn components.
472
+
473
+ ${c.bold}What it does:${c.reset}
474
+ 1. Installs runtime dependencies (@base-ui/react, tailwind-variants, etc.)
475
+ 2. Sets up vite.config.ts with Tailwind CSS + path aliases
476
+ 3. Patches tsconfig.json with path aliases (@/*, @lib/*, etc.)
477
+ 4. Copies core files (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
478
+ 5. Wraps your <App /> with <ThemeProvider> in the main entry
479
+
480
+ ${c.bold}Usage:${c.reset}
481
+ ${c.dim}$${c.reset} npx basuicn init
482
+ ${c.dim}$${c.reset} npx basuicn init --local ${c.dim}# Use local registry${c.reset}
483
+ `,
484
+ add: `
485
+ ${c.bold}basuicn add${c.reset} ${c.dim}<name...>${c.reset}
486
+
487
+ Add one or more components to your project.
488
+
489
+ ${c.bold}Options:${c.reset}
490
+ ${c.cyan}--force${c.reset} Overwrite existing component files
491
+
492
+ ${c.bold}Features:${c.reset}
493
+ \u2022 Auto-runs init if project hasn't been set up
494
+ \u2022 Resolves internal dependencies (e.g., dialog depends on button)
495
+ \u2022 Installs required npm packages automatically
496
+ \u2022 Patches main entry for components that need it (e.g., toast)
497
+
498
+ ${c.bold}Usage:${c.reset}
499
+ ${c.dim}$${c.reset} npx basuicn add button
500
+ ${c.dim}$${c.reset} npx basuicn add button input card dialog
501
+ ${c.dim}$${c.reset} npx basuicn add toast --force
502
+
503
+ ${c.bold}Interactive:${c.reset}
504
+ ${c.dim}$${c.reset} npx basuicn add ${c.dim}# Prompts to select components${c.reset}
505
+ `,
506
+ update: `
507
+ ${c.bold}basuicn update${c.reset} ${c.dim}<name...>${c.reset}
508
+
509
+ Update component(s) to the latest registry version.
510
+ Equivalent to ${c.cyan}add --force${c.reset}.
511
+
512
+ ${c.bold}Usage:${c.reset}
513
+ ${c.dim}$${c.reset} npx basuicn update button
514
+ ${c.dim}$${c.reset} npx basuicn update button card dialog
515
+ `,
516
+ remove: `
517
+ ${c.bold}basuicn remove${c.reset} ${c.dim}<name...>${c.reset}
518
+
519
+ Remove component(s) from your project.
520
+ Deletes component files and cleans up empty directories.
521
+
522
+ ${c.bold}Usage:${c.reset}
523
+ ${c.dim}$${c.reset} npx basuicn remove button
524
+ ${c.dim}$${c.reset} npx basuicn remove dialog drawer sheet
525
+ `,
526
+ diff: `
527
+ ${c.bold}basuicn diff${c.reset} ${c.dim}<name...>${c.reset}
528
+
529
+ Show differences between your local component files and the registry version.
530
+ Useful to see what has changed before running update.
531
+
532
+ ${c.bold}Usage:${c.reset}
533
+ ${c.dim}$${c.reset} npx basuicn diff button
534
+ ${c.dim}$${c.reset} npx basuicn diff button card
535
+ `,
536
+ list: `
537
+ ${c.bold}basuicn list${c.reset}
538
+
539
+ Show all available components in the registry.
540
+ Displays internal dependencies for each component.
541
+
542
+ ${c.bold}Usage:${c.reset}
543
+ ${c.dim}$${c.reset} npx basuicn list
544
+ `,
545
+ doctor: `
546
+ ${c.bold}basuicn doctor${c.reset}
547
+
548
+ Run a health check on your project configuration.
549
+
550
+ ${c.bold}Checks:${c.reset}
551
+ \u2022 Core files exist (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
552
+ \u2022 ThemeProvider + CSS import in main entry
553
+ \u2022 Runtime packages installed
554
+ \u2022 Dev packages installed
555
+ \u2022 Tailwind CSS configured
556
+ \u2022 TypeScript path aliases
557
+ \u2022 Vite config present
558
+
559
+ ${c.bold}Usage:${c.reset}
560
+ ${c.dim}$${c.reset} npx basuicn doctor
561
+ `
562
+ };
339
563
  var main = async () => {
340
564
  const args = process.argv.slice(2);
565
+ if (args.includes("--version") || args.includes("-v")) {
566
+ console.log(`basuicn v${VERSION}`);
567
+ return;
568
+ }
341
569
  const isLocal = args.includes("--local");
342
570
  const isForce = args.includes("--force");
343
- const filteredArgs = args.filter((a) => !a.startsWith("--"));
571
+ const isHelp = args.includes("--help") || args.includes("-h");
572
+ const filteredArgs = args.filter((a) => !a.startsWith("--") && a !== "-h" && a !== "-v");
344
573
  const command = filteredArgs[0];
345
574
  const componentNames = filteredArgs.slice(1);
575
+ if (isHelp && command && HELP_COMMANDS[command]) {
576
+ console.log(HELP_COMMANDS[command]);
577
+ return;
578
+ }
579
+ if (isHelp || !command) {
580
+ console.log(HELP_MAIN);
581
+ return;
582
+ }
346
583
  const cwd = getTargetProjectDir();
347
584
  const registry = await getRegistry(isLocal);
348
585
  switch (command) {
586
+ case "init": {
587
+ log("Initializing project...");
588
+ setupViteConfig(cwd);
589
+ setupTsConfig(cwd);
590
+ installNpmPackages(RUNTIME_PACKAGES, cwd);
591
+ ensureCore(registry, cwd, { force: true });
592
+ patchMainTsx(cwd);
593
+ console.log("");
594
+ ok(`${c.bold}Initialization complete!${c.reset} Run ${c.cyan}npx basuicn add <component>${c.reset} to get started.`);
595
+ break;
596
+ }
349
597
  case "add": {
350
- if (componentNames.length === 0) {
351
- error("Usage: npx basuicn add <component-name> [--force]");
352
- return;
598
+ let names = componentNames;
599
+ if (names.length === 0) {
600
+ const all = Object.keys(registry.components).sort();
601
+ console.log(`
602
+ ${c.bold}Available components (${all.length}):${c.reset}`);
603
+ const categories = {};
604
+ for (const name of all) {
605
+ const prefix = name.includes("-") ? name.split("-")[0] : "general";
606
+ if (!categories[prefix]) categories[prefix] = [];
607
+ categories[prefix].push(name);
608
+ }
609
+ const cols = 4;
610
+ for (let i = 0; i < all.length; i += cols) {
611
+ const row = all.slice(i, i + cols).map((n) => n.padEnd(20)).join("");
612
+ console.log(` ${c.dim}${row}${c.reset}`);
613
+ }
614
+ console.log("");
615
+ const answer = await ask(`Which components to add? ${c.dim}(space-separated, or "all")${c.reset}`);
616
+ if (!answer) {
617
+ log("No components selected.");
618
+ return;
619
+ }
620
+ names = answer === "all" ? all : answer.split(/[\s,]+/).filter(Boolean);
353
621
  }
354
622
  const cnPath = import_path.default.join(cwd, "src/lib/utils/cn.ts");
355
623
  if (!import_fs.default.existsSync(cnPath)) {
356
624
  log("Project not initialized \u2014 running init first...");
357
625
  setupViteConfig(cwd);
358
626
  setupTsConfig(cwd);
359
- ensureTailwindCss(cwd);
360
627
  installNpmPackages(RUNTIME_PACKAGES, cwd);
628
+ ensureCore(registry, cwd, { force: true });
629
+ patchMainTsx(cwd);
630
+ console.log("");
361
631
  }
362
- for (const name of componentNames) {
632
+ for (const name of names) {
363
633
  addComponent(name, registry, cwd, { force: isForce });
634
+ patchMainTsxComponent(cwd, name);
364
635
  }
365
- log("Done!");
636
+ console.log("");
637
+ ok(`${c.bold}Done!${c.reset} Added ${names.length} component(s).`);
638
+ break;
639
+ }
640
+ case "update": {
641
+ if (componentNames.length === 0) {
642
+ error(`Usage: ${c.cyan}npx basuicn update <component-name> [...]${c.reset}`);
643
+ console.log(` Run ${c.cyan}npx basuicn update --help${c.reset} for details.`);
644
+ return;
645
+ }
646
+ for (const name of componentNames) {
647
+ log(`Updating: ${c.bold}${name}${c.reset}...`);
648
+ addComponent(name, registry, cwd, { force: true });
649
+ }
650
+ console.log("");
651
+ ok(`${c.bold}Update complete.${c.reset}`);
366
652
  break;
367
653
  }
368
654
  case "remove": {
369
655
  if (componentNames.length === 0) {
370
- error("Usage: npx basuicn remove <component-name>");
656
+ error(`Usage: ${c.cyan}npx basuicn remove <component-name>${c.reset}`);
371
657
  return;
372
658
  }
659
+ if (!isForce) {
660
+ const yes = await confirm(`Remove ${componentNames.join(", ")}?`);
661
+ if (!yes) {
662
+ log("Cancelled.");
663
+ return;
664
+ }
665
+ }
373
666
  for (const name of componentNames) {
374
667
  removeComponent(name, registry, cwd);
375
668
  }
376
- log("Done!");
669
+ console.log("");
670
+ ok(`${c.bold}Done!${c.reset}`);
377
671
  break;
378
672
  }
379
673
  case "list": {
380
674
  const components = Object.keys(registry.components).sort();
381
- log(`Available components (${components.length}):`);
675
+ console.log(`
676
+ ${c.bold}Available components (${components.length}):${c.reset}
677
+ `);
678
+ const installed = [];
679
+ const available = [];
382
680
  for (const k of components) {
383
- const deps = registry.components[k].internalDependencies;
384
- const depStr = deps?.length ? ` (requires: ${deps.join(", ")})` : "";
385
- console.log(` - ${k}${depStr}`);
681
+ const comp = registry.components[k];
682
+ const firstFile = comp.files[0];
683
+ const isInstalled = firstFile && import_fs.default.existsSync(import_path.default.join(cwd, firstFile.path));
684
+ if (isInstalled) installed.push(k);
685
+ else available.push(k);
386
686
  }
387
- break;
388
- }
389
- case "init": {
390
- setupViteConfig(cwd);
391
- setupTsConfig(cwd);
392
- ensureTailwindCss(cwd);
393
- installNpmPackages(RUNTIME_PACKAGES, cwd);
394
- ensureCore(registry, cwd);
395
- log("Initialization complete.");
396
- break;
397
- }
398
- case "tailwind": {
399
- console.log("\n--- Copy to tailwind.config.ts / tailwind.config.js ---\n");
400
- console.log("// See README_CLI.md for full theme config");
401
- break;
402
- }
403
- case "update": {
404
- if (componentNames.length === 0) {
405
- error("Usage: npx basuicn update <component-name> [...]");
406
- return;
687
+ if (installed.length > 0) {
688
+ console.log(` ${c.green}Installed (${installed.length}):${c.reset}`);
689
+ for (const k of installed) {
690
+ const deps = registry.components[k].internalDependencies?.filter(Boolean);
691
+ const depStr = deps?.length ? ` ${c.dim}\u2192 ${deps.join(", ")}${c.reset}` : "";
692
+ console.log(` ${c.green}\u25CF${c.reset} ${k}${depStr}`);
693
+ }
694
+ console.log("");
407
695
  }
408
- for (const name of componentNames) {
409
- log(`Updating: ${name}...`);
410
- addComponent(name, registry, cwd, { force: true });
696
+ if (available.length > 0) {
697
+ console.log(` ${c.dim}Available (${available.length}):${c.reset}`);
698
+ for (const k of available) {
699
+ const deps = registry.components[k].internalDependencies?.filter(Boolean);
700
+ const depStr = deps?.length ? ` ${c.dim}\u2192 ${deps.join(", ")}${c.reset}` : "";
701
+ console.log(` ${c.dim}\u25CB${c.reset} ${k}${depStr}`);
702
+ }
411
703
  }
412
- log("Update complete.");
704
+ console.log("");
413
705
  break;
414
706
  }
415
707
  case "diff": {
416
708
  if (componentNames.length === 0) {
417
- error("Usage: npx basuicn diff <component-name>");
709
+ error(`Usage: ${c.cyan}npx basuicn diff <component-name>${c.reset}`);
418
710
  return;
419
711
  }
420
712
  for (const name of componentNames) {
421
713
  const component = registry.components[name];
422
714
  if (!component) {
423
- error(`Component "${name}" not found. Run 'list' to see available components.`);
715
+ error(`Component "${name}" not found.`);
424
716
  continue;
425
717
  }
426
718
  let hasDiff = false;
427
719
  console.log(`
428
- [diff] ${name}`);
720
+ ${c.bold}[diff] ${name}${c.reset}`);
429
721
  for (const file of component.files) {
430
722
  const targetPath = import_path.default.join(cwd, file.path);
431
723
  if (!import_fs.default.existsSync(targetPath)) {
432
- console.log(` + [new file] ${file.path}`);
724
+ console.log(` ${c.green}+ [new file]${c.reset} ${file.path}`);
433
725
  hasDiff = true;
434
726
  continue;
435
727
  }
@@ -437,94 +729,120 @@ var main = async () => {
437
729
  if (localContent === file.content) continue;
438
730
  hasDiff = true;
439
731
  console.log(`
440
- ~ ${file.path}`);
732
+ ${c.yellow}~${c.reset} ${file.path}`);
441
733
  const localLines = localContent.split("\n");
442
734
  const remoteLines = file.content.split("\n");
443
735
  const maxLen = Math.max(localLines.length, remoteLines.length);
444
736
  let shownLines = 0;
445
737
  for (let i = 0; i < maxLen; i++) {
446
738
  if (localLines[i] !== remoteLines[i]) {
447
- if (localLines[i] !== void 0) console.log(` - ${localLines[i]}`);
448
- if (remoteLines[i] !== void 0) console.log(` + ${remoteLines[i]}`);
739
+ if (localLines[i] !== void 0) console.log(` ${c.red}- ${localLines[i]}${c.reset}`);
740
+ if (remoteLines[i] !== void 0) console.log(` ${c.green}+ ${remoteLines[i]}${c.reset}`);
449
741
  shownLines++;
450
742
  if (shownLines >= 20) {
451
743
  const remaining = maxLen - i - 1;
452
- if (remaining > 0) console.log(` ... and ${remaining} more lines`);
744
+ if (remaining > 0) console.log(` ${c.dim}... and ${remaining} more lines${c.reset}`);
453
745
  break;
454
746
  }
455
747
  }
456
748
  }
457
749
  }
458
- if (!hasDiff) log(`${name}: already up to date.`);
750
+ if (!hasDiff) ok(`${name}: already up to date.`);
459
751
  }
460
752
  break;
461
753
  }
462
754
  case "doctor": {
463
- log("Running project health check...\n");
755
+ console.log(`
756
+ ${c.bold}Project Health Check${c.reset}
757
+ `);
464
758
  let issues = 0;
465
- const check = (ok, msg, fix) => {
466
- const icon = ok ? " \u2713" : " \u2717";
467
- console.log(`${icon} ${msg}`);
468
- if (!ok) {
469
- if (fix) console.log(` \u2192 ${fix}`);
759
+ const check = (passed, msg, fix) => {
760
+ console.log(` ${passed ? `${c.green}\u2714${c.reset}` : `${c.red}\u2716${c.reset}`} ${msg}`);
761
+ if (!passed) {
762
+ if (fix) console.log(` ${c.dim}\u2192 ${fix}${c.reset}`);
470
763
  issues++;
471
764
  }
472
765
  };
473
- const cnPath = import_path.default.join(cwd, "src/lib/utils/cn.ts");
474
- check(import_fs.default.existsSync(cnPath), "src/lib/utils/cn.ts", "run: npx basuicn init");
766
+ check(
767
+ import_fs.default.existsSync(import_path.default.join(cwd, "src/lib/utils/cn.ts")),
768
+ "src/lib/utils/cn.ts",
769
+ "run: npx basuicn init"
770
+ );
771
+ check(
772
+ import_fs.default.existsSync(import_path.default.join(cwd, "src/lib/theme/themes.ts")),
773
+ "src/lib/theme/themes.ts",
774
+ "run: npx basuicn init"
775
+ );
776
+ check(
777
+ import_fs.default.existsSync(import_path.default.join(cwd, "src/lib/theme/ThemeProvider.tsx")),
778
+ "src/lib/theme/ThemeProvider.tsx",
779
+ "run: npx basuicn init"
780
+ );
781
+ check(
782
+ import_fs.default.existsSync(import_path.default.join(cwd, "src/styles/index.css")),
783
+ "src/styles/index.css (theme variables)",
784
+ "run: npx basuicn init"
785
+ );
786
+ const mainPath = findMainFile(cwd);
787
+ if (mainPath) {
788
+ const mainContent = import_fs.default.readFileSync(mainPath, "utf-8");
789
+ check(
790
+ mainContent.includes("ThemeProvider"),
791
+ "ThemeProvider in main entry",
792
+ "run: npx basuicn init"
793
+ );
794
+ check(
795
+ mainContent.includes("styles/index.css") || mainContent.includes("index.css"),
796
+ "CSS import in main entry",
797
+ "run: npx basuicn init"
798
+ );
799
+ } else {
800
+ check(false, "main entry file (src/main.tsx)", "create src/main.tsx");
801
+ }
475
802
  const pkgPath = import_path.default.join(cwd, "package.json");
476
803
  if (import_fs.default.existsSync(pkgPath)) {
477
804
  const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
478
805
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
479
806
  for (const dep of RUNTIME_PACKAGES) {
480
- check(!!allDeps[dep], `dependency: ${dep}`, `run: npm install ${dep}`);
807
+ check(!!allDeps[dep], `package: ${dep}`, `run: npm install ${dep}`);
808
+ }
809
+ for (const dep of VITE_DEV_PACKAGES) {
810
+ check(!!allDeps[dep], `package (dev): ${dep}`, `run: npm install -D ${dep}`);
481
811
  }
482
812
  } else {
483
813
  check(false, "package.json found", "run: npm init -y");
484
814
  }
485
- const cssCandidates = ["src/index.css", "src/App.css", "src/main.css", "src/styles/index.css"];
486
- const hasTailwind = cssCandidates.some((f) => {
487
- const p = import_path.default.join(cwd, f);
488
- if (!import_fs.default.existsSync(p)) return false;
489
- const c = import_fs.default.readFileSync(p, "utf-8");
490
- return c.includes('@import "tailwindcss"') || c.includes("@import 'tailwindcss'");
491
- });
492
- check(hasTailwind, "Tailwind CSS @import in CSS file", "run: npx basuicn init");
815
+ const hasTailwindInCss = (() => {
816
+ const candidates = ["src/styles/index.css", "src/index.css", "src/App.css"];
817
+ return candidates.some((f) => {
818
+ const p = import_path.default.join(cwd, f);
819
+ if (!import_fs.default.existsSync(p)) return false;
820
+ const content = import_fs.default.readFileSync(p, "utf-8");
821
+ return content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'");
822
+ });
823
+ })();
824
+ check(hasTailwindInCss, '@import "tailwindcss" in CSS', "run: npx basuicn init");
493
825
  const tsCandidates = ["tsconfig.app.json", "tsconfig.json"];
494
826
  const hasAlias = tsCandidates.some((f) => {
495
827
  const p = import_path.default.join(cwd, f);
496
828
  if (!import_fs.default.existsSync(p)) return false;
497
- const c = import_fs.default.readFileSync(p, "utf-8");
498
- return c.includes('"@/*"') || c.includes("'@/*'");
829
+ const content = import_fs.default.readFileSync(p, "utf-8");
830
+ return content.includes('"@/*"') || content.includes("'@/*'");
499
831
  });
500
832
  check(hasAlias, "TypeScript path aliases (@/*)", "run: npx basuicn init");
501
833
  const hasViteConfig = import_fs.default.existsSync(import_path.default.join(cwd, "vite.config.ts")) || import_fs.default.existsSync(import_path.default.join(cwd, "vite.config.js"));
502
834
  check(hasViteConfig, "vite.config.ts / vite.config.js", "run: npx basuicn init");
503
835
  console.log("");
504
836
  if (issues === 0) {
505
- log("All checks passed! Project is healthy.");
837
+ ok(`${c.bold}All checks passed!${c.reset} Project is healthy.`);
506
838
  } else {
507
- warn(`${issues} issue(s) found. Run 'npx basuicn init' to fix most issues.`);
839
+ warn(`${c.bold}${issues} issue(s) found.${c.reset} Run ${c.cyan}npx basuicn init${c.reset} to fix most issues.`);
508
840
  }
509
841
  break;
510
842
  }
511
843
  default: {
512
- console.log(`
513
- basuicn \u2014 UI Component CLI
514
-
515
- Commands:
516
- init Initialize project (install core deps + files)
517
- add <name> [--force] Add component(s) to your project
518
- update <name> Update component(s) to the latest registry version
519
- diff <name> Show diff between local and registry version
520
- remove <name> Remove component(s) from your project
521
- list List all available components
522
- doctor Check project health and configuration
523
- tailwind Show Tailwind config instructions
524
-
525
- Options:
526
- --local Use local registry.json instead of remote
527
- --force Overwrite existing files when adding
844
+ error(`Unknown command: "${command}"`);
845
+ console.log(` Run ${c.cyan}npx basuicn --help${c.reset} to see available commands.
528
846
  `);
529
847
  }
530
848
  }