basuicn 0.1.6 → 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.
@@ -0,0 +1,850 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // scripts/ui-cli.ts
27
+ var import_fs = __toESM(require("fs"), 1);
28
+ var import_path = __toESM(require("path"), 1);
29
+ var import_child_process = require("child_process");
30
+ var import_readline = __toESM(require("readline"), 1);
31
+ var VERSION = "0.1.6";
32
+ var REGISTRY_LOCAL = "./registry.json";
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}`);
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
+ };
66
+ var validateRegistry = (data) => {
67
+ if (!data || typeof data !== "object") return false;
68
+ const reg = data;
69
+ return "components" in reg && typeof reg.components === "object" && reg.components !== null;
70
+ };
71
+ var getRegistry = async (isLocal) => {
72
+ if (isLocal && import_fs.default.existsSync(REGISTRY_LOCAL)) {
73
+ log("Using local registry...");
74
+ try {
75
+ const data = JSON.parse(import_fs.default.readFileSync(REGISTRY_LOCAL, "utf-8"));
76
+ if (!validateRegistry(data)) {
77
+ error('Invalid local registry format \u2014 missing "components" field.');
78
+ process.exit(1);
79
+ }
80
+ return data;
81
+ } catch (err) {
82
+ error(`Failed to parse local registry: ${err instanceof Error ? err.message : err}`);
83
+ process.exit(1);
84
+ }
85
+ }
86
+ log("Fetching registry from remote...");
87
+ try {
88
+ const response = await fetch(REGISTRY_REMOTE);
89
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
90
+ const data = await response.json();
91
+ if (!validateRegistry(data)) {
92
+ error('Invalid remote registry format \u2014 missing "components" field.');
93
+ process.exit(1);
94
+ }
95
+ return data;
96
+ } catch (err) {
97
+ const message = err instanceof Error ? err.message : String(err);
98
+ error(`Cannot fetch registry: ${message}`);
99
+ process.exit(1);
100
+ }
101
+ };
102
+ var installNpmPackages = (packages, cwd, dev = false) => {
103
+ if (packages.length === 0) return;
104
+ const pkgJsonPath = import_path.default.join(cwd, "package.json");
105
+ let toInstall = packages;
106
+ if (import_fs.default.existsSync(pkgJsonPath)) {
107
+ const pkg = JSON.parse(import_fs.default.readFileSync(pkgJsonPath, "utf-8"));
108
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
109
+ toInstall = packages.filter((p) => !allDeps[p]);
110
+ }
111
+ if (toInstall.length === 0) return;
112
+ log(`Installing: ${c.bold}${toInstall.join(", ")}${c.reset}...`);
113
+ const flag = dev ? "--save-dev" : "--save";
114
+ try {
115
+ (0, import_child_process.execSync)(`npm install ${toInstall.join(" ")} ${flag}`, { stdio: "inherit", cwd });
116
+ } catch (err) {
117
+ error(`Failed to install packages: ${toInstall.join(", ")}. ${err instanceof Error ? err.message : ""}`);
118
+ }
119
+ };
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
+ ];
134
+ var VITE_CONFIG_TEMPLATE = `import { defineConfig } from 'vite';
135
+ import tailwindcss from '@tailwindcss/vite';
136
+ import react from '@vitejs/plugin-react';
137
+ import path from 'path';
138
+
139
+ export default defineConfig({
140
+ plugins: [tailwindcss(), react()],
141
+ resolve: {
142
+ alias: {
143
+ '@': path.resolve(__dirname, './src'),
144
+ '@lib': path.resolve(__dirname, './src/lib'),
145
+ '@components': path.resolve(__dirname, './src/components'),
146
+ '@assets': path.resolve(__dirname, './src/assets'),
147
+ '@pages': path.resolve(__dirname, './src/pages'),
148
+ '@styles': path.resolve(__dirname, './src/styles'),
149
+ },
150
+ },
151
+ });
152
+ `;
153
+ var TSCONFIG_PATHS = {
154
+ "@/*": ["./src/*"],
155
+ "@lib/*": ["./src/lib/*"],
156
+ "@components/*": ["./src/components/*"],
157
+ "@assets/*": ["./src/assets/*"],
158
+ "@pages/*": ["./src/pages/*"],
159
+ "@styles/*": ["./src/styles/*"]
160
+ };
161
+ var setupViteConfig = (cwd) => {
162
+ installNpmPackages(VITE_DEV_PACKAGES, cwd, true);
163
+ const configTs = import_path.default.join(cwd, "vite.config.ts");
164
+ const configJs = import_path.default.join(cwd, "vite.config.js");
165
+ if (!import_fs.default.existsSync(configTs) && !import_fs.default.existsSync(configJs)) {
166
+ import_fs.default.writeFileSync(configTs, VITE_CONFIG_TEMPLATE);
167
+ ok("Created vite.config.ts.");
168
+ return;
169
+ }
170
+ const existingPath = import_fs.default.existsSync(configTs) ? configTs : configJs;
171
+ let content = import_fs.default.readFileSync(existingPath, "utf-8");
172
+ const missingImports = [];
173
+ if (!content.includes("@tailwindcss/vite")) missingImports.push("import tailwindcss from '@tailwindcss/vite';");
174
+ if (!content.includes("@vitejs/plugin-react")) missingImports.push("import react from '@vitejs/plugin-react';");
175
+ if (!content.includes("from 'path'") && !content.includes('from "path"')) missingImports.push("import path from 'path';");
176
+ const missingPlugins = [];
177
+ if (!content.includes("tailwindcss()")) missingPlugins.push("tailwindcss()");
178
+ if (!content.includes("react()") && !content.includes("react({")) missingPlugins.push("react()");
179
+ const hasAlias = content.includes("alias:") || content.includes("'@'") || content.includes('"@"');
180
+ if (missingImports.length === 0 && missingPlugins.length === 0 && hasAlias) {
181
+ ok("vite.config already configured \u2014 skipping.");
182
+ return;
183
+ }
184
+ if (missingImports.length > 0) {
185
+ const importBlock = missingImports.join("\n");
186
+ const allImports = [...content.matchAll(/^import\s.+$/gm)];
187
+ if (allImports.length > 0) {
188
+ const last = allImports[allImports.length - 1];
189
+ const pos = last.index + last[0].length;
190
+ content = content.slice(0, pos) + "\n" + importBlock + content.slice(pos);
191
+ } else {
192
+ content = importBlock + "\n" + content;
193
+ }
194
+ }
195
+ if (missingPlugins.length > 0) {
196
+ const match = content.match(/plugins:\s*\[/);
197
+ if (match && match.index !== void 0) {
198
+ const pos = match.index + match[0].length;
199
+ const after = content.slice(pos);
200
+ const pluginLines = missingPlugins.map((p) => `
201
+ ${p},`).join("");
202
+ const needsNewline = after.length > 0 && after[0] !== "\n" && after[0] !== "\r";
203
+ content = content.slice(0, pos) + pluginLines + (needsNewline ? "\n " : "") + after;
204
+ }
205
+ }
206
+ if (!hasAlias) {
207
+ const aliasBlock = [
208
+ " resolve: {",
209
+ " alias: {",
210
+ " '@': path.resolve(__dirname, './src'),",
211
+ " '@lib': path.resolve(__dirname, './src/lib'),",
212
+ " '@components': path.resolve(__dirname, './src/components'),",
213
+ " '@assets': path.resolve(__dirname, './src/assets'),",
214
+ " '@pages': path.resolve(__dirname, './src/pages'),",
215
+ " '@styles': path.resolve(__dirname, './src/styles'),",
216
+ " },",
217
+ " },"
218
+ ].join("\n");
219
+ const pluginsStart = content.search(/plugins:\s*\[/);
220
+ if (pluginsStart !== -1) {
221
+ let depth = 0;
222
+ let foundStart = false;
223
+ for (let i = pluginsStart; i < content.length; i++) {
224
+ if (content[i] === "[") {
225
+ depth++;
226
+ foundStart = true;
227
+ }
228
+ if (content[i] === "]") depth--;
229
+ if (foundStart && depth === 0) {
230
+ let lineEnd = content.indexOf("\n", i);
231
+ if (lineEnd === -1) lineEnd = content.length;
232
+ content = content.slice(0, lineEnd + 1) + aliasBlock + "\n" + content.slice(lineEnd + 1);
233
+ break;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ import_fs.default.writeFileSync(existingPath, content);
239
+ ok(`Updated ${import_path.default.basename(existingPath)} with Tailwind + path aliases.`);
240
+ };
241
+ var setupTsConfig = (cwd) => {
242
+ const candidates = ["tsconfig.app.json", "tsconfig.json"];
243
+ for (const candidate of candidates) {
244
+ const configPath = import_path.default.join(cwd, candidate);
245
+ if (!import_fs.default.existsSync(configPath)) continue;
246
+ const raw = import_fs.default.readFileSync(configPath, "utf-8");
247
+ if (raw.includes('"@/*"') || raw.includes("'@/*'")) {
248
+ ok(`${candidate} already has path aliases \u2014 skipping.`);
249
+ return;
250
+ }
251
+ try {
252
+ const stripped = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[\s,{[\]])\/\/[^\n]*/g, "$1");
253
+ const parsed = JSON.parse(stripped);
254
+ if (!parsed.compilerOptions) parsed.compilerOptions = {};
255
+ parsed.compilerOptions.baseUrl = ".";
256
+ parsed.compilerOptions.paths = TSCONFIG_PATHS;
257
+ import_fs.default.writeFileSync(configPath, JSON.stringify(parsed, null, 2));
258
+ ok(`Added path aliases to ${candidate}.`);
259
+ } catch (err) {
260
+ warn(`Could not auto-patch ${candidate}: ${err instanceof Error ? err.message : err}`);
261
+ warn("Add these to compilerOptions manually:");
262
+ console.log('\n "baseUrl": ".",');
263
+ console.log(' "paths": {');
264
+ for (const [alias, targets] of Object.entries(TSCONFIG_PATHS)) {
265
+ console.log(` "${alias}": ["${targets[0]}"],`);
266
+ }
267
+ console.log(" }");
268
+ console.log("");
269
+ }
270
+ return;
271
+ }
272
+ const newConfig = { compilerOptions: { baseUrl: ".", paths: TSCONFIG_PATHS } };
273
+ import_fs.default.writeFileSync(import_path.default.join(cwd, "tsconfig.json"), JSON.stringify(newConfig, null, 2));
274
+ ok("Created tsconfig.json with path aliases.");
275
+ };
276
+ var ensureCore = (registry, cwd, options = {}) => {
277
+ const core = registry.core;
278
+ if (!core) return;
279
+ installNpmPackages(core.dependencies, cwd);
280
+ for (const file of core.files) {
281
+ const targetPath = import_path.default.join(cwd, file.path);
282
+ const targetDir = import_path.default.dirname(targetPath);
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;
332
+ }
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;
345
+ }
346
+ changed = true;
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)}.`);
377
+ };
378
+ var addComponent = (name, registry, cwd, options, added = /* @__PURE__ */ new Set()) => {
379
+ if (added.has(name)) return;
380
+ added.add(name);
381
+ const component = registry.components[name];
382
+ if (!component) {
383
+ error(`Component "${name}" not found. Run '${c.cyan}basuicn list${c.reset}' to see available components.`);
384
+ return;
385
+ }
386
+ log(`Adding: ${c.bold}${name}${c.reset}...`);
387
+ ensureCore(registry, cwd);
388
+ installNpmPackages(component.dependencies, cwd);
389
+ if (component.internalDependencies) {
390
+ for (const dep of component.internalDependencies) {
391
+ if (registry.components[dep]) {
392
+ addComponent(dep, registry, cwd, options, added);
393
+ }
394
+ }
395
+ }
396
+ for (const file of component.files) {
397
+ const targetPath = import_path.default.join(cwd, file.path);
398
+ const targetDir = import_path.default.dirname(targetPath);
399
+ if (!import_fs.default.existsSync(targetDir)) import_fs.default.mkdirSync(targetDir, { recursive: true });
400
+ if (import_fs.default.existsSync(targetPath) && !options.force) {
401
+ warn(`Skipped (exists): ${file.path} \u2014 use ${c.cyan}--force${c.reset} to overwrite`);
402
+ continue;
403
+ }
404
+ import_fs.default.writeFileSync(targetPath, file.content);
405
+ ok(`Created: ${file.path}`);
406
+ }
407
+ };
408
+ var removeComponent = (name, registry, cwd) => {
409
+ const component = registry.components[name];
410
+ if (!component) {
411
+ error(`Component "${name}" not found.`);
412
+ return;
413
+ }
414
+ log(`Removing: ${c.bold}${name}${c.reset}...`);
415
+ for (const file of component.files) {
416
+ const targetPath = import_path.default.join(cwd, file.path);
417
+ if (import_fs.default.existsSync(targetPath)) {
418
+ import_fs.default.unlinkSync(targetPath);
419
+ ok(`Deleted: ${file.path}`);
420
+ }
421
+ }
422
+ for (const file of component.files) {
423
+ const targetDir = import_path.default.dirname(import_path.default.join(cwd, file.path));
424
+ try {
425
+ if (import_fs.default.existsSync(targetDir) && import_fs.default.readdirSync(targetDir).length === 0) {
426
+ import_fs.default.rmdirSync(targetDir);
427
+ ok(`Removed empty dir: ${import_path.default.relative(cwd, targetDir)}`);
428
+ }
429
+ } catch (err) {
430
+ warn(`Could not remove directory: ${err instanceof Error ? err.message : err}`);
431
+ }
432
+ }
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
+ };
563
+ var main = async () => {
564
+ const args = process.argv.slice(2);
565
+ if (args.includes("--version") || args.includes("-v")) {
566
+ console.log(`basuicn v${VERSION}`);
567
+ return;
568
+ }
569
+ const isLocal = args.includes("--local");
570
+ const isForce = args.includes("--force");
571
+ const isHelp = args.includes("--help") || args.includes("-h");
572
+ const filteredArgs = args.filter((a) => !a.startsWith("--") && a !== "-h" && a !== "-v");
573
+ const command = filteredArgs[0];
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
+ }
583
+ const cwd = getTargetProjectDir();
584
+ const registry = await getRegistry(isLocal);
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
+ }
597
+ case "add": {
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);
621
+ }
622
+ const cnPath = import_path.default.join(cwd, "src/lib/utils/cn.ts");
623
+ if (!import_fs.default.existsSync(cnPath)) {
624
+ log("Project not initialized \u2014 running init first...");
625
+ setupViteConfig(cwd);
626
+ setupTsConfig(cwd);
627
+ installNpmPackages(RUNTIME_PACKAGES, cwd);
628
+ ensureCore(registry, cwd, { force: true });
629
+ patchMainTsx(cwd);
630
+ console.log("");
631
+ }
632
+ for (const name of names) {
633
+ addComponent(name, registry, cwd, { force: isForce });
634
+ patchMainTsxComponent(cwd, name);
635
+ }
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}`);
652
+ break;
653
+ }
654
+ case "remove": {
655
+ if (componentNames.length === 0) {
656
+ error(`Usage: ${c.cyan}npx basuicn remove <component-name>${c.reset}`);
657
+ return;
658
+ }
659
+ if (!isForce) {
660
+ const yes = await confirm(`Remove ${componentNames.join(", ")}?`);
661
+ if (!yes) {
662
+ log("Cancelled.");
663
+ return;
664
+ }
665
+ }
666
+ for (const name of componentNames) {
667
+ removeComponent(name, registry, cwd);
668
+ }
669
+ console.log("");
670
+ ok(`${c.bold}Done!${c.reset}`);
671
+ break;
672
+ }
673
+ case "list": {
674
+ const components = Object.keys(registry.components).sort();
675
+ console.log(`
676
+ ${c.bold}Available components (${components.length}):${c.reset}
677
+ `);
678
+ const installed = [];
679
+ const available = [];
680
+ for (const k of components) {
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);
686
+ }
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("");
695
+ }
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
+ }
703
+ }
704
+ console.log("");
705
+ break;
706
+ }
707
+ case "diff": {
708
+ if (componentNames.length === 0) {
709
+ error(`Usage: ${c.cyan}npx basuicn diff <component-name>${c.reset}`);
710
+ return;
711
+ }
712
+ for (const name of componentNames) {
713
+ const component = registry.components[name];
714
+ if (!component) {
715
+ error(`Component "${name}" not found.`);
716
+ continue;
717
+ }
718
+ let hasDiff = false;
719
+ console.log(`
720
+ ${c.bold}[diff] ${name}${c.reset}`);
721
+ for (const file of component.files) {
722
+ const targetPath = import_path.default.join(cwd, file.path);
723
+ if (!import_fs.default.existsSync(targetPath)) {
724
+ console.log(` ${c.green}+ [new file]${c.reset} ${file.path}`);
725
+ hasDiff = true;
726
+ continue;
727
+ }
728
+ const localContent = import_fs.default.readFileSync(targetPath, "utf-8");
729
+ if (localContent === file.content) continue;
730
+ hasDiff = true;
731
+ console.log(`
732
+ ${c.yellow}~${c.reset} ${file.path}`);
733
+ const localLines = localContent.split("\n");
734
+ const remoteLines = file.content.split("\n");
735
+ const maxLen = Math.max(localLines.length, remoteLines.length);
736
+ let shownLines = 0;
737
+ for (let i = 0; i < maxLen; i++) {
738
+ if (localLines[i] !== 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}`);
741
+ shownLines++;
742
+ if (shownLines >= 20) {
743
+ const remaining = maxLen - i - 1;
744
+ if (remaining > 0) console.log(` ${c.dim}... and ${remaining} more lines${c.reset}`);
745
+ break;
746
+ }
747
+ }
748
+ }
749
+ }
750
+ if (!hasDiff) ok(`${name}: already up to date.`);
751
+ }
752
+ break;
753
+ }
754
+ case "doctor": {
755
+ console.log(`
756
+ ${c.bold}Project Health Check${c.reset}
757
+ `);
758
+ let issues = 0;
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}`);
763
+ issues++;
764
+ }
765
+ };
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
+ }
802
+ const pkgPath = import_path.default.join(cwd, "package.json");
803
+ if (import_fs.default.existsSync(pkgPath)) {
804
+ const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
805
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
806
+ for (const dep of RUNTIME_PACKAGES) {
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}`);
811
+ }
812
+ } else {
813
+ check(false, "package.json found", "run: npm init -y");
814
+ }
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");
825
+ const tsCandidates = ["tsconfig.app.json", "tsconfig.json"];
826
+ const hasAlias = tsCandidates.some((f) => {
827
+ const p = import_path.default.join(cwd, f);
828
+ if (!import_fs.default.existsSync(p)) return false;
829
+ const content = import_fs.default.readFileSync(p, "utf-8");
830
+ return content.includes('"@/*"') || content.includes("'@/*'");
831
+ });
832
+ check(hasAlias, "TypeScript path aliases (@/*)", "run: npx basuicn init");
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"));
834
+ check(hasViteConfig, "vite.config.ts / vite.config.js", "run: npx basuicn init");
835
+ console.log("");
836
+ if (issues === 0) {
837
+ ok(`${c.bold}All checks passed!${c.reset} Project is healthy.`);
838
+ } else {
839
+ warn(`${c.bold}${issues} issue(s) found.${c.reset} Run ${c.cyan}npx basuicn init${c.reset} to fix most issues.`);
840
+ }
841
+ break;
842
+ }
843
+ default: {
844
+ error(`Unknown command: "${command}"`);
845
+ console.log(` Run ${c.cyan}npx basuicn --help${c.reset} to see available commands.
846
+ `);
847
+ }
848
+ }
849
+ };
850
+ main();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "basuicn",
3
3
  "private": false,
4
- "version": "0.1.6",
4
+ "version": "0.1.7",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "basuicn": "./dist/ui-cli.cjs"
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  "dev": "vite",
17
17
  "build": "tsc -b && vite build",
18
- "build:cli": "npx -y esbuild scripts/ui-cli.ts --bundle --platform=node --outfile=dist/ui-cli.cjs --format=cjs --packages=external",
18
+ "build:cli": "node scripts/build-cli.mjs",
19
19
  "lint": "eslint .",
20
20
  "preview": "vite preview",
21
21
  "test": "vitest",
@@ -0,0 +1,13 @@
1
+ import { build } from 'esbuild';
2
+
3
+ await build({
4
+ entryPoints: ['scripts/ui-cli.ts'],
5
+ bundle: true,
6
+ platform: 'node',
7
+ outfile: 'dist/ui-cli.cjs',
8
+ format: 'cjs',
9
+ packages: 'external',
10
+ banner: { js: '#!/usr/bin/env node' },
11
+ });
12
+
13
+ console.log('✔ CLI built → dist/ui-cli.cjs');
package/scripts/ui-cli.ts CHANGED
@@ -6,7 +6,7 @@ import readline from 'readline';
6
6
 
7
7
  // ─── Constants ────────────────────────────────────────────────────────────────
8
8
 
9
- const VERSION = '0.1.6';
9
+ const VERSION = '0.1.7';
10
10
  const REGISTRY_LOCAL = './registry.json';
11
11
  const REGISTRY_REMOTE = 'https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json';
12
12