basuicn 0.1.4 → 0.1.5
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/package.json +16 -3
- package/scripts/ui-cli.ts +327 -121
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basuicn",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"basuicn": "./dist/ui-cli.cjs"
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"test:coverage": "vitest run --coverage",
|
|
24
24
|
"registry:build": "npx -y tsx scripts/build-registry.ts",
|
|
25
25
|
"theme:sync": "npx -y tsx scripts/generate-theme-css.ts",
|
|
26
|
-
"ui:add": "npx -y tsx scripts/ui-cli.ts add"
|
|
26
|
+
"ui:add": "npx -y tsx scripts/ui-cli.ts add",
|
|
27
|
+
"storybook": "storybook dev -p 6006",
|
|
28
|
+
"build-storybook": "storybook build"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@babel/core": "^7.29.0",
|
|
@@ -81,7 +83,18 @@
|
|
|
81
83
|
"unified": "^11.0.5",
|
|
82
84
|
"vite": "^8.0.1",
|
|
83
85
|
"vitest": "^4.1.2",
|
|
84
|
-
"zod": "^4.3.6"
|
|
86
|
+
"zod": "^4.3.6",
|
|
87
|
+
"storybook": "^10.3.4",
|
|
88
|
+
"@storybook/react-vite": "^10.3.4",
|
|
89
|
+
"@chromatic-com/storybook": "^5.1.1",
|
|
90
|
+
"@storybook/addon-vitest": "^10.3.4",
|
|
91
|
+
"@storybook/addon-a11y": "^10.3.4",
|
|
92
|
+
"@storybook/addon-docs": "^10.3.4",
|
|
93
|
+
"@storybook/addon-onboarding": "^10.3.4",
|
|
94
|
+
"eslint-plugin-storybook": "^10.3.4",
|
|
95
|
+
"playwright": "^1.59.1",
|
|
96
|
+
"@vitest/browser-playwright": "^4.1.2",
|
|
97
|
+
"@vitest/coverage-v8": "^4.1.2"
|
|
85
98
|
},
|
|
86
99
|
"dependencies": {
|
|
87
100
|
"keen-slider": "^6.8.6"
|
package/scripts/ui-cli.ts
CHANGED
|
@@ -2,17 +2,56 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
+
import readline from 'readline';
|
|
5
6
|
|
|
7
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.4';
|
|
6
10
|
const REGISTRY_LOCAL = './registry.json';
|
|
7
11
|
const REGISTRY_REMOTE = 'https://raw.githubusercontent.com/huy14032003/ui-component/main/registry.json';
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
13
|
+
// ─── Colors (ANSI) ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const c = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m',
|
|
21
|
+
red: '\x1b[31m',
|
|
22
|
+
cyan: '\x1b[36m',
|
|
23
|
+
magenta: '\x1b[35m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
gray: '\x1b[90m',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const log = (msg: string) => console.log(`${c.cyan}▸${c.reset} ${msg}`);
|
|
29
|
+
const ok = (msg: string) => console.log(`${c.green}✔${c.reset} ${msg}`);
|
|
30
|
+
const warn = (msg: string) => console.warn(`${c.yellow}⚠${c.reset} ${msg}`);
|
|
31
|
+
const error = (msg: string) => console.error(`${c.red}✖${c.reset} ${msg}`);
|
|
12
32
|
|
|
13
33
|
const getTargetProjectDir = () => process.cwd();
|
|
14
34
|
|
|
15
|
-
// ───
|
|
35
|
+
// ─── Interactive prompt ──────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const ask = (question: string): Promise<string> => {
|
|
38
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
rl.question(`${c.cyan}?${c.reset} ${question} `, (answer) => {
|
|
41
|
+
rl.close();
|
|
42
|
+
resolve(answer.trim());
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const confirm = async (question: string, defaultYes = true): Promise<boolean> => {
|
|
48
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
49
|
+
const answer = await ask(`${question} ${c.dim}(${hint})${c.reset}`);
|
|
50
|
+
if (!answer) return defaultYes;
|
|
51
|
+
return answer.toLowerCase().startsWith('y');
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
16
55
|
|
|
17
56
|
interface RegistryFile { path: string; content: string }
|
|
18
57
|
interface RegistryComponent {
|
|
@@ -80,7 +119,7 @@ const installNpmPackages = (packages: string[], cwd: string, dev = false) => {
|
|
|
80
119
|
|
|
81
120
|
if (toInstall.length === 0) return;
|
|
82
121
|
|
|
83
|
-
log(`Installing: ${toInstall.join(', ')}...`);
|
|
122
|
+
log(`Installing: ${c.bold}${toInstall.join(', ')}${c.reset}...`);
|
|
84
123
|
const flag = dev ? '--save-dev' : '--save';
|
|
85
124
|
try {
|
|
86
125
|
execSync(`npm install ${toInstall.join(' ')} ${flag}`, { stdio: 'inherit', cwd });
|
|
@@ -89,9 +128,8 @@ const installNpmPackages = (packages: string[], cwd: string, dev = false) => {
|
|
|
89
128
|
}
|
|
90
129
|
};
|
|
91
130
|
|
|
92
|
-
// ─── Packages
|
|
131
|
+
// ─── Packages ─────────────────────────────────────────────────────────────────
|
|
93
132
|
|
|
94
|
-
/** Build/dev tooling installed as devDependencies */
|
|
95
133
|
const VITE_DEV_PACKAGES = [
|
|
96
134
|
'tailwindcss',
|
|
97
135
|
'@tailwindcss/vite',
|
|
@@ -99,10 +137,6 @@ const VITE_DEV_PACKAGES = [
|
|
|
99
137
|
'@types/node',
|
|
100
138
|
];
|
|
101
139
|
|
|
102
|
-
/**
|
|
103
|
-
* Runtime packages every project using these components needs.
|
|
104
|
-
* Installed as regular dependencies.
|
|
105
|
-
*/
|
|
106
140
|
const RUNTIME_PACKAGES = [
|
|
107
141
|
'@base-ui/react',
|
|
108
142
|
'tailwind-variants',
|
|
@@ -112,7 +146,7 @@ const RUNTIME_PACKAGES = [
|
|
|
112
146
|
'lucide-react',
|
|
113
147
|
];
|
|
114
148
|
|
|
115
|
-
// ─── Vite config
|
|
149
|
+
// ─── Vite config ──────────────────────────────────────────────────────────────
|
|
116
150
|
|
|
117
151
|
const VITE_CONFIG_TEMPLATE = `import { defineConfig } from 'vite';
|
|
118
152
|
import tailwindcss from '@tailwindcss/vite';
|
|
@@ -151,7 +185,7 @@ const setupViteConfig = (cwd: string) => {
|
|
|
151
185
|
|
|
152
186
|
if (!fs.existsSync(configTs) && !fs.existsSync(configJs)) {
|
|
153
187
|
fs.writeFileSync(configTs, VITE_CONFIG_TEMPLATE);
|
|
154
|
-
|
|
188
|
+
ok('Created vite.config.ts.');
|
|
155
189
|
return;
|
|
156
190
|
}
|
|
157
191
|
|
|
@@ -170,7 +204,7 @@ const setupViteConfig = (cwd: string) => {
|
|
|
170
204
|
const hasAlias = content.includes('alias:') || content.includes("'@'") || content.includes('"@"');
|
|
171
205
|
|
|
172
206
|
if (missingImports.length === 0 && missingPlugins.length === 0 && hasAlias) {
|
|
173
|
-
|
|
207
|
+
ok('vite.config already configured — skipping.');
|
|
174
208
|
return;
|
|
175
209
|
}
|
|
176
210
|
|
|
@@ -229,10 +263,10 @@ const setupViteConfig = (cwd: string) => {
|
|
|
229
263
|
}
|
|
230
264
|
|
|
231
265
|
fs.writeFileSync(existingPath, content);
|
|
232
|
-
|
|
266
|
+
ok(`Updated ${path.basename(existingPath)} with Tailwind + path aliases.`);
|
|
233
267
|
};
|
|
234
268
|
|
|
235
|
-
// ─── tsconfig
|
|
269
|
+
// ─── tsconfig ─────────────────────────────────────────────────────────────────
|
|
236
270
|
|
|
237
271
|
const setupTsConfig = (cwd: string) => {
|
|
238
272
|
const candidates = ['tsconfig.app.json', 'tsconfig.json'];
|
|
@@ -244,7 +278,7 @@ const setupTsConfig = (cwd: string) => {
|
|
|
244
278
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
245
279
|
|
|
246
280
|
if (raw.includes('"@/*"') || raw.includes("'@/*'")) {
|
|
247
|
-
|
|
281
|
+
ok(`${candidate} already has path aliases — skipping.`);
|
|
248
282
|
return;
|
|
249
283
|
}
|
|
250
284
|
|
|
@@ -257,7 +291,7 @@ const setupTsConfig = (cwd: string) => {
|
|
|
257
291
|
parsed.compilerOptions.baseUrl = '.';
|
|
258
292
|
parsed.compilerOptions.paths = TSCONFIG_PATHS;
|
|
259
293
|
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2));
|
|
260
|
-
|
|
294
|
+
ok(`Added path aliases to ${candidate}.`);
|
|
261
295
|
} catch (err) {
|
|
262
296
|
warn(`Could not auto-patch ${candidate}: ${err instanceof Error ? err.message : err}`);
|
|
263
297
|
warn('Add these to compilerOptions manually:');
|
|
@@ -274,15 +308,11 @@ const setupTsConfig = (cwd: string) => {
|
|
|
274
308
|
|
|
275
309
|
const newConfig = { compilerOptions: { baseUrl: '.', paths: TSCONFIG_PATHS } };
|
|
276
310
|
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify(newConfig, null, 2));
|
|
277
|
-
|
|
311
|
+
ok('Created tsconfig.json with path aliases.');
|
|
278
312
|
};
|
|
279
313
|
|
|
280
|
-
// ─── Core files
|
|
314
|
+
// ─── Core files ───────────────────────────────────────────────────────────────
|
|
281
315
|
|
|
282
|
-
/**
|
|
283
|
-
* Copy core files (cn.ts, index.css, ThemeProvider, themes.ts) from registry.
|
|
284
|
-
* Pass force=true on `init` to always overwrite — keeps core up to date.
|
|
285
|
-
*/
|
|
286
316
|
const ensureCore = (
|
|
287
317
|
registry: { core?: { dependencies: string[]; files: RegistryFile[] } },
|
|
288
318
|
cwd: string,
|
|
@@ -300,21 +330,17 @@ const ensureCore = (
|
|
|
300
330
|
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
301
331
|
|
|
302
332
|
if (fs.existsSync(targetPath) && !options.force) {
|
|
303
|
-
log(`Core file exists (skipping): ${file.path}`);
|
|
333
|
+
log(`Core file exists (skipping): ${c.dim}${file.path}${c.reset}`);
|
|
304
334
|
continue;
|
|
305
335
|
}
|
|
306
336
|
|
|
307
337
|
fs.writeFileSync(targetPath, file.content);
|
|
308
|
-
|
|
338
|
+
ok(`${fs.existsSync(targetPath) ? 'Updated' : 'Created'} core file: ${file.path}`);
|
|
309
339
|
}
|
|
310
340
|
};
|
|
311
341
|
|
|
312
342
|
// ─── main.tsx patching ────────────────────────────────────────────────────────
|
|
313
343
|
|
|
314
|
-
/**
|
|
315
|
-
* Components that need additional setup in the main entry file.
|
|
316
|
-
* Key = component name in registry, value = import + JSX to inject.
|
|
317
|
-
*/
|
|
318
344
|
const MAIN_PATCH_COMPONENTS: Record<string, { import: string; jsx: string }> = {
|
|
319
345
|
toast: {
|
|
320
346
|
import: "import { Toaster } from '@/components/ui/toast/Toaster';",
|
|
@@ -333,7 +359,6 @@ const findMainFile = (cwd: string): string | null => {
|
|
|
333
359
|
};
|
|
334
360
|
|
|
335
361
|
const insertImport = (content: string, importLine: string): string => {
|
|
336
|
-
// Don't add duplicate
|
|
337
362
|
if (content.includes(importLine)) return content;
|
|
338
363
|
const allImports = [...content.matchAll(/^import\s.+$/gm)];
|
|
339
364
|
if (allImports.length > 0) {
|
|
@@ -344,14 +369,6 @@ const insertImport = (content: string, importLine: string): string => {
|
|
|
344
369
|
return importLine + '\n' + content;
|
|
345
370
|
};
|
|
346
371
|
|
|
347
|
-
/**
|
|
348
|
-
* Patches the main entry file to:
|
|
349
|
-
* 1. Import src/styles/index.css (theme variables + Tailwind)
|
|
350
|
-
* 2. Import ThemeProvider
|
|
351
|
-
* 3. Wrap <App /> with <ThemeProvider>
|
|
352
|
-
*
|
|
353
|
-
* Safe to call multiple times — skips sections that are already set up.
|
|
354
|
-
*/
|
|
355
372
|
const patchMainTsx = (cwd: string) => {
|
|
356
373
|
const mainPath = findMainFile(cwd);
|
|
357
374
|
if (!mainPath) {
|
|
@@ -362,11 +379,9 @@ const patchMainTsx = (cwd: string) => {
|
|
|
362
379
|
let content = fs.readFileSync(mainPath, 'utf-8');
|
|
363
380
|
let changed = false;
|
|
364
381
|
|
|
365
|
-
// 1. Ensure styles/index.css is imported
|
|
366
382
|
const cssImportLine = "import './styles/index.css';";
|
|
367
383
|
const hasCssImport = content.includes('styles/index.css') || content.includes('index.css');
|
|
368
384
|
if (!hasCssImport) {
|
|
369
|
-
// Insert at top before other imports
|
|
370
385
|
const firstImport = content.match(/^import\s/m);
|
|
371
386
|
if (firstImport?.index !== undefined) {
|
|
372
387
|
content = content.slice(0, firstImport.index) + cssImportLine + '\n' + content.slice(firstImport.index);
|
|
@@ -375,12 +390,10 @@ const patchMainTsx = (cwd: string) => {
|
|
|
375
390
|
}
|
|
376
391
|
changed = true;
|
|
377
392
|
} else if (!content.includes('styles/index.css')) {
|
|
378
|
-
// Has some CSS import but not our theme CSS — add it alongside
|
|
379
393
|
content = insertImport(content, cssImportLine);
|
|
380
394
|
changed = true;
|
|
381
395
|
}
|
|
382
396
|
|
|
383
|
-
// 2. ThemeProvider
|
|
384
397
|
if (!content.includes('ThemeProvider')) {
|
|
385
398
|
content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
|
|
386
399
|
|
|
@@ -395,16 +408,12 @@ const patchMainTsx = (cwd: string) => {
|
|
|
395
408
|
|
|
396
409
|
if (changed) {
|
|
397
410
|
fs.writeFileSync(mainPath, content);
|
|
398
|
-
|
|
411
|
+
ok(`Patched ${path.relative(cwd, mainPath)}.`);
|
|
399
412
|
} else {
|
|
400
|
-
|
|
413
|
+
ok(`${path.relative(cwd, mainPath)} already configured — skipping.`);
|
|
401
414
|
}
|
|
402
415
|
};
|
|
403
416
|
|
|
404
|
-
/**
|
|
405
|
-
* Injects a component's bootstrap JSX (e.g. <Toaster />) into the main entry file.
|
|
406
|
-
* Places it inside <ThemeProvider> after <App />, falls back to right after <App />.
|
|
407
|
-
*/
|
|
408
417
|
const patchMainTsxComponent = (cwd: string, componentName: string) => {
|
|
409
418
|
const patch = MAIN_PATCH_COMPONENTS[componentName];
|
|
410
419
|
if (!patch) return;
|
|
@@ -413,7 +422,6 @@ const patchMainTsxComponent = (cwd: string, componentName: string) => {
|
|
|
413
422
|
if (!mainPath) return;
|
|
414
423
|
|
|
415
424
|
let content = fs.readFileSync(mainPath, 'utf-8');
|
|
416
|
-
// Check by component tag name (e.g. "Toaster")
|
|
417
425
|
const tagName = patch.jsx.match(/<(\w+)/)?.[1];
|
|
418
426
|
if (tagName && content.includes(`<${tagName}`)) return;
|
|
419
427
|
|
|
@@ -431,10 +439,10 @@ const patchMainTsxComponent = (cwd: string, componentName: string) => {
|
|
|
431
439
|
if (fallback !== content) fs.writeFileSync(mainPath, fallback);
|
|
432
440
|
}
|
|
433
441
|
|
|
434
|
-
|
|
442
|
+
ok(`Added <${tagName}> to ${path.relative(cwd, mainPath)}.`);
|
|
435
443
|
};
|
|
436
444
|
|
|
437
|
-
// ─── Component add/remove
|
|
445
|
+
// ─── Component add/remove ─────────────────────────────────────────────────────
|
|
438
446
|
|
|
439
447
|
const addComponent = (
|
|
440
448
|
name: string,
|
|
@@ -448,11 +456,11 @@ const addComponent = (
|
|
|
448
456
|
|
|
449
457
|
const component = registry.components[name];
|
|
450
458
|
if (!component) {
|
|
451
|
-
error(`Component "${name}" not found. Run 'list' to see available components.`);
|
|
459
|
+
error(`Component "${name}" not found. Run '${c.cyan}basuicn list${c.reset}' to see available components.`);
|
|
452
460
|
return;
|
|
453
461
|
}
|
|
454
462
|
|
|
455
|
-
log(`Adding: ${name}...`);
|
|
463
|
+
log(`Adding: ${c.bold}${name}${c.reset}...`);
|
|
456
464
|
|
|
457
465
|
ensureCore(registry as Parameters<typeof ensureCore>[0], cwd);
|
|
458
466
|
installNpmPackages(component.dependencies, cwd);
|
|
@@ -472,12 +480,12 @@ const addComponent = (
|
|
|
472
480
|
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
473
481
|
|
|
474
482
|
if (fs.existsSync(targetPath) && !options.force) {
|
|
475
|
-
warn(`Skipped (exists): ${file.path} — use --force to overwrite`);
|
|
483
|
+
warn(`Skipped (exists): ${file.path} — use ${c.cyan}--force${c.reset} to overwrite`);
|
|
476
484
|
continue;
|
|
477
485
|
}
|
|
478
486
|
|
|
479
487
|
fs.writeFileSync(targetPath, file.content);
|
|
480
|
-
|
|
488
|
+
ok(`Created: ${file.path}`);
|
|
481
489
|
}
|
|
482
490
|
};
|
|
483
491
|
|
|
@@ -492,13 +500,13 @@ const removeComponent = (
|
|
|
492
500
|
return;
|
|
493
501
|
}
|
|
494
502
|
|
|
495
|
-
log(`Removing: ${name}...`);
|
|
503
|
+
log(`Removing: ${c.bold}${name}${c.reset}...`);
|
|
496
504
|
|
|
497
505
|
for (const file of component.files) {
|
|
498
506
|
const targetPath = path.join(cwd, file.path);
|
|
499
507
|
if (fs.existsSync(targetPath)) {
|
|
500
508
|
fs.unlinkSync(targetPath);
|
|
501
|
-
|
|
509
|
+
ok(`Deleted: ${file.path}`);
|
|
502
510
|
}
|
|
503
511
|
}
|
|
504
512
|
|
|
@@ -507,7 +515,7 @@ const removeComponent = (
|
|
|
507
515
|
try {
|
|
508
516
|
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length === 0) {
|
|
509
517
|
fs.rmdirSync(targetDir);
|
|
510
|
-
|
|
518
|
+
ok(`Removed empty dir: ${path.relative(cwd, targetDir)}`);
|
|
511
519
|
}
|
|
512
520
|
} catch (err) {
|
|
513
521
|
warn(`Could not remove directory: ${err instanceof Error ? err.message : err}`);
|
|
@@ -515,16 +523,169 @@ const removeComponent = (
|
|
|
515
523
|
}
|
|
516
524
|
};
|
|
517
525
|
|
|
526
|
+
// ─── Help texts ───────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
const HELP_MAIN = `
|
|
529
|
+
${c.bold}${c.cyan}basuicn${c.reset} ${c.dim}v${VERSION}${c.reset} — Modern React UI Component CLI
|
|
530
|
+
|
|
531
|
+
${c.bold}USAGE${c.reset}
|
|
532
|
+
${c.cyan}npx basuicn${c.reset} ${c.green}<command>${c.reset} ${c.dim}[options]${c.reset}
|
|
533
|
+
|
|
534
|
+
${c.bold}COMMANDS${c.reset}
|
|
535
|
+
${c.green}init${c.reset} Initialize project: install deps, copy core files, patch entry
|
|
536
|
+
${c.green}add${c.reset} ${c.dim}<name...>${c.reset} Add component(s) to your project
|
|
537
|
+
${c.green}update${c.reset} ${c.dim}<name...>${c.reset} Update component(s) to latest registry version
|
|
538
|
+
${c.green}diff${c.reset} ${c.dim}<name...>${c.reset} Show diff between local and registry version
|
|
539
|
+
${c.green}remove${c.reset} ${c.dim}<name...>${c.reset} Remove component(s) from your project
|
|
540
|
+
${c.green}list${c.reset} List all available components
|
|
541
|
+
${c.green}doctor${c.reset} Check project health and configuration
|
|
542
|
+
|
|
543
|
+
${c.bold}OPTIONS${c.reset}
|
|
544
|
+
${c.cyan}--force${c.reset} Overwrite existing files when adding/updating
|
|
545
|
+
${c.cyan}--local${c.reset} Use local registry.json instead of remote
|
|
546
|
+
${c.cyan}--help, -h${c.reset} Show help (use with a command for detailed help)
|
|
547
|
+
${c.cyan}--version, -v${c.reset} Show version
|
|
548
|
+
|
|
549
|
+
${c.bold}QUICK START${c.reset}
|
|
550
|
+
${c.dim}$${c.reset} npx basuicn init
|
|
551
|
+
${c.dim}$${c.reset} npx basuicn add button input card
|
|
552
|
+
${c.dim}$${c.reset} npx basuicn add toast
|
|
553
|
+
|
|
554
|
+
${c.bold}EXAMPLES${c.reset}
|
|
555
|
+
${c.dim}$${c.reset} npx basuicn add dialog --force ${c.dim}# Overwrite existing dialog${c.reset}
|
|
556
|
+
${c.dim}$${c.reset} npx basuicn diff button ${c.dim}# See what changed since last update${c.reset}
|
|
557
|
+
${c.dim}$${c.reset} npx basuicn doctor ${c.dim}# Diagnose missing deps/config${c.reset}
|
|
558
|
+
|
|
559
|
+
${c.dim}Documentation: https://github.com/huy14032003/ui-component${c.reset}
|
|
560
|
+
`;
|
|
561
|
+
|
|
562
|
+
const HELP_COMMANDS: Record<string, string> = {
|
|
563
|
+
init: `
|
|
564
|
+
${c.bold}basuicn init${c.reset}
|
|
565
|
+
|
|
566
|
+
Initialize your project for basuicn components.
|
|
567
|
+
|
|
568
|
+
${c.bold}What it does:${c.reset}
|
|
569
|
+
1. Installs runtime dependencies (@base-ui/react, tailwind-variants, etc.)
|
|
570
|
+
2. Sets up vite.config.ts with Tailwind CSS + path aliases
|
|
571
|
+
3. Patches tsconfig.json with path aliases (@/*, @lib/*, etc.)
|
|
572
|
+
4. Copies core files (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
|
|
573
|
+
5. Wraps your <App /> with <ThemeProvider> in the main entry
|
|
574
|
+
|
|
575
|
+
${c.bold}Usage:${c.reset}
|
|
576
|
+
${c.dim}$${c.reset} npx basuicn init
|
|
577
|
+
${c.dim}$${c.reset} npx basuicn init --local ${c.dim}# Use local registry${c.reset}
|
|
578
|
+
`,
|
|
579
|
+
add: `
|
|
580
|
+
${c.bold}basuicn add${c.reset} ${c.dim}<name...>${c.reset}
|
|
581
|
+
|
|
582
|
+
Add one or more components to your project.
|
|
583
|
+
|
|
584
|
+
${c.bold}Options:${c.reset}
|
|
585
|
+
${c.cyan}--force${c.reset} Overwrite existing component files
|
|
586
|
+
|
|
587
|
+
${c.bold}Features:${c.reset}
|
|
588
|
+
• Auto-runs init if project hasn't been set up
|
|
589
|
+
• Resolves internal dependencies (e.g., dialog depends on button)
|
|
590
|
+
• Installs required npm packages automatically
|
|
591
|
+
• Patches main entry for components that need it (e.g., toast)
|
|
592
|
+
|
|
593
|
+
${c.bold}Usage:${c.reset}
|
|
594
|
+
${c.dim}$${c.reset} npx basuicn add button
|
|
595
|
+
${c.dim}$${c.reset} npx basuicn add button input card dialog
|
|
596
|
+
${c.dim}$${c.reset} npx basuicn add toast --force
|
|
597
|
+
|
|
598
|
+
${c.bold}Interactive:${c.reset}
|
|
599
|
+
${c.dim}$${c.reset} npx basuicn add ${c.dim}# Prompts to select components${c.reset}
|
|
600
|
+
`,
|
|
601
|
+
update: `
|
|
602
|
+
${c.bold}basuicn update${c.reset} ${c.dim}<name...>${c.reset}
|
|
603
|
+
|
|
604
|
+
Update component(s) to the latest registry version.
|
|
605
|
+
Equivalent to ${c.cyan}add --force${c.reset}.
|
|
606
|
+
|
|
607
|
+
${c.bold}Usage:${c.reset}
|
|
608
|
+
${c.dim}$${c.reset} npx basuicn update button
|
|
609
|
+
${c.dim}$${c.reset} npx basuicn update button card dialog
|
|
610
|
+
`,
|
|
611
|
+
remove: `
|
|
612
|
+
${c.bold}basuicn remove${c.reset} ${c.dim}<name...>${c.reset}
|
|
613
|
+
|
|
614
|
+
Remove component(s) from your project.
|
|
615
|
+
Deletes component files and cleans up empty directories.
|
|
616
|
+
|
|
617
|
+
${c.bold}Usage:${c.reset}
|
|
618
|
+
${c.dim}$${c.reset} npx basuicn remove button
|
|
619
|
+
${c.dim}$${c.reset} npx basuicn remove dialog drawer sheet
|
|
620
|
+
`,
|
|
621
|
+
diff: `
|
|
622
|
+
${c.bold}basuicn diff${c.reset} ${c.dim}<name...>${c.reset}
|
|
623
|
+
|
|
624
|
+
Show differences between your local component files and the registry version.
|
|
625
|
+
Useful to see what has changed before running update.
|
|
626
|
+
|
|
627
|
+
${c.bold}Usage:${c.reset}
|
|
628
|
+
${c.dim}$${c.reset} npx basuicn diff button
|
|
629
|
+
${c.dim}$${c.reset} npx basuicn diff button card
|
|
630
|
+
`,
|
|
631
|
+
list: `
|
|
632
|
+
${c.bold}basuicn list${c.reset}
|
|
633
|
+
|
|
634
|
+
Show all available components in the registry.
|
|
635
|
+
Displays internal dependencies for each component.
|
|
636
|
+
|
|
637
|
+
${c.bold}Usage:${c.reset}
|
|
638
|
+
${c.dim}$${c.reset} npx basuicn list
|
|
639
|
+
`,
|
|
640
|
+
doctor: `
|
|
641
|
+
${c.bold}basuicn doctor${c.reset}
|
|
642
|
+
|
|
643
|
+
Run a health check on your project configuration.
|
|
644
|
+
|
|
645
|
+
${c.bold}Checks:${c.reset}
|
|
646
|
+
• Core files exist (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
|
|
647
|
+
• ThemeProvider + CSS import in main entry
|
|
648
|
+
• Runtime packages installed
|
|
649
|
+
• Dev packages installed
|
|
650
|
+
• Tailwind CSS configured
|
|
651
|
+
• TypeScript path aliases
|
|
652
|
+
• Vite config present
|
|
653
|
+
|
|
654
|
+
${c.bold}Usage:${c.reset}
|
|
655
|
+
${c.dim}$${c.reset} npx basuicn doctor
|
|
656
|
+
`,
|
|
657
|
+
};
|
|
658
|
+
|
|
518
659
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
519
660
|
|
|
520
661
|
const main = async () => {
|
|
521
662
|
const args = process.argv.slice(2);
|
|
663
|
+
|
|
664
|
+
// Version flag
|
|
665
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
666
|
+
console.log(`basuicn v${VERSION}`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
522
670
|
const isLocal = args.includes('--local');
|
|
523
671
|
const isForce = args.includes('--force');
|
|
524
|
-
const
|
|
672
|
+
const isHelp = args.includes('--help') || args.includes('-h');
|
|
673
|
+
const filteredArgs = args.filter((a) => !a.startsWith('--') && a !== '-h' && a !== '-v');
|
|
525
674
|
const command = filteredArgs[0];
|
|
526
675
|
const componentNames = filteredArgs.slice(1);
|
|
527
676
|
|
|
677
|
+
// Help for specific command
|
|
678
|
+
if (isHelp && command && HELP_COMMANDS[command]) {
|
|
679
|
+
console.log(HELP_COMMANDS[command]);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// General help
|
|
684
|
+
if (isHelp || !command) {
|
|
685
|
+
console.log(HELP_MAIN);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
528
689
|
const cwd = getTargetProjectDir();
|
|
529
690
|
const registry = await getRegistry(isLocal);
|
|
530
691
|
|
|
@@ -535,17 +696,43 @@ const main = async () => {
|
|
|
535
696
|
setupViteConfig(cwd);
|
|
536
697
|
setupTsConfig(cwd);
|
|
537
698
|
installNpmPackages(RUNTIME_PACKAGES, cwd);
|
|
538
|
-
// force=true so init always refreshes core files to latest version
|
|
539
699
|
ensureCore(registry, cwd, { force: true });
|
|
540
700
|
patchMainTsx(cwd);
|
|
541
|
-
log('
|
|
701
|
+
console.log('');
|
|
702
|
+
ok(`${c.bold}Initialization complete!${c.reset} Run ${c.cyan}npx basuicn add <component>${c.reset} to get started.`);
|
|
542
703
|
break;
|
|
543
704
|
}
|
|
544
705
|
|
|
545
706
|
case 'add': {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
707
|
+
let names = componentNames;
|
|
708
|
+
|
|
709
|
+
// Interactive mode: no component names provided
|
|
710
|
+
if (names.length === 0) {
|
|
711
|
+
const all = Object.keys(registry.components).sort();
|
|
712
|
+
console.log(`\n${c.bold}Available components (${all.length}):${c.reset}`);
|
|
713
|
+
|
|
714
|
+
// Group by category
|
|
715
|
+
const categories: Record<string, string[]> = {};
|
|
716
|
+
for (const name of all) {
|
|
717
|
+
const prefix = name.includes('-') ? name.split('-')[0] : 'general';
|
|
718
|
+
if (!categories[prefix]) categories[prefix] = [];
|
|
719
|
+
categories[prefix].push(name);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Print in columns
|
|
723
|
+
const cols = 4;
|
|
724
|
+
for (let i = 0; i < all.length; i += cols) {
|
|
725
|
+
const row = all.slice(i, i + cols).map(n => n.padEnd(20)).join('');
|
|
726
|
+
console.log(` ${c.dim}${row}${c.reset}`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
console.log('');
|
|
730
|
+
const answer = await ask(`Which components to add? ${c.dim}(space-separated, or "all")${c.reset}`);
|
|
731
|
+
if (!answer) {
|
|
732
|
+
log('No components selected.');
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
names = answer === 'all' ? all : answer.split(/[\s,]+/).filter(Boolean);
|
|
549
736
|
}
|
|
550
737
|
|
|
551
738
|
// Auto-init if project hasn't been initialized yet
|
|
@@ -557,108 +744,147 @@ const main = async () => {
|
|
|
557
744
|
installNpmPackages(RUNTIME_PACKAGES, cwd);
|
|
558
745
|
ensureCore(registry, cwd, { force: true });
|
|
559
746
|
patchMainTsx(cwd);
|
|
747
|
+
console.log('');
|
|
560
748
|
}
|
|
561
749
|
|
|
562
|
-
for (const name of
|
|
750
|
+
for (const name of names) {
|
|
563
751
|
addComponent(name, registry, cwd, { force: isForce });
|
|
564
752
|
patchMainTsxComponent(cwd, name);
|
|
565
753
|
}
|
|
566
|
-
log('
|
|
754
|
+
console.log('');
|
|
755
|
+
ok(`${c.bold}Done!${c.reset} Added ${names.length} component(s).`);
|
|
567
756
|
break;
|
|
568
757
|
}
|
|
569
758
|
|
|
570
759
|
case 'update': {
|
|
571
760
|
if (componentNames.length === 0) {
|
|
572
|
-
error(
|
|
761
|
+
error(`Usage: ${c.cyan}npx basuicn update <component-name> [...]${c.reset}`);
|
|
762
|
+
console.log(` Run ${c.cyan}npx basuicn update --help${c.reset} for details.`);
|
|
573
763
|
return;
|
|
574
764
|
}
|
|
575
765
|
for (const name of componentNames) {
|
|
576
|
-
log(`Updating: ${name}...`);
|
|
766
|
+
log(`Updating: ${c.bold}${name}${c.reset}...`);
|
|
577
767
|
addComponent(name, registry, cwd, { force: true });
|
|
578
768
|
}
|
|
579
|
-
log('
|
|
769
|
+
console.log('');
|
|
770
|
+
ok(`${c.bold}Update complete.${c.reset}`);
|
|
580
771
|
break;
|
|
581
772
|
}
|
|
582
773
|
|
|
583
774
|
case 'remove': {
|
|
584
775
|
if (componentNames.length === 0) {
|
|
585
|
-
error(
|
|
776
|
+
error(`Usage: ${c.cyan}npx basuicn remove <component-name>${c.reset}`);
|
|
586
777
|
return;
|
|
587
778
|
}
|
|
779
|
+
|
|
780
|
+
if (!isForce) {
|
|
781
|
+
const yes = await confirm(`Remove ${componentNames.join(', ')}?`);
|
|
782
|
+
if (!yes) {
|
|
783
|
+
log('Cancelled.');
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
588
788
|
for (const name of componentNames) {
|
|
589
789
|
removeComponent(name, registry, cwd);
|
|
590
790
|
}
|
|
591
|
-
log('
|
|
791
|
+
console.log('');
|
|
792
|
+
ok(`${c.bold}Done!${c.reset}`);
|
|
592
793
|
break;
|
|
593
794
|
}
|
|
594
795
|
|
|
595
796
|
case 'list': {
|
|
596
797
|
const components = Object.keys(registry.components).sort();
|
|
597
|
-
log(
|
|
798
|
+
console.log(`\n${c.bold}Available components (${components.length}):${c.reset}\n`);
|
|
799
|
+
|
|
800
|
+
const installed: string[] = [];
|
|
801
|
+
const available: string[] = [];
|
|
802
|
+
|
|
598
803
|
for (const k of components) {
|
|
599
804
|
const comp = registry.components[k];
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
|
|
805
|
+
const firstFile = comp.files[0];
|
|
806
|
+
const isInstalled = firstFile && fs.existsSync(path.join(cwd, firstFile.path));
|
|
807
|
+
if (isInstalled) installed.push(k);
|
|
808
|
+
else available.push(k);
|
|
603
809
|
}
|
|
810
|
+
|
|
811
|
+
if (installed.length > 0) {
|
|
812
|
+
console.log(` ${c.green}Installed (${installed.length}):${c.reset}`);
|
|
813
|
+
for (const k of installed) {
|
|
814
|
+
const deps = registry.components[k].internalDependencies?.filter(Boolean);
|
|
815
|
+
const depStr = deps?.length ? ` ${c.dim}→ ${deps.join(', ')}${c.reset}` : '';
|
|
816
|
+
console.log(` ${c.green}●${c.reset} ${k}${depStr}`);
|
|
817
|
+
}
|
|
818
|
+
console.log('');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (available.length > 0) {
|
|
822
|
+
console.log(` ${c.dim}Available (${available.length}):${c.reset}`);
|
|
823
|
+
for (const k of available) {
|
|
824
|
+
const deps = registry.components[k].internalDependencies?.filter(Boolean);
|
|
825
|
+
const depStr = deps?.length ? ` ${c.dim}→ ${deps.join(', ')}${c.reset}` : '';
|
|
826
|
+
console.log(` ${c.dim}○${c.reset} ${k}${depStr}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
console.log('');
|
|
604
830
|
break;
|
|
605
831
|
}
|
|
606
832
|
|
|
607
833
|
case 'diff': {
|
|
608
834
|
if (componentNames.length === 0) {
|
|
609
|
-
error(
|
|
835
|
+
error(`Usage: ${c.cyan}npx basuicn diff <component-name>${c.reset}`);
|
|
610
836
|
return;
|
|
611
837
|
}
|
|
612
838
|
for (const name of componentNames) {
|
|
613
839
|
const component = registry.components[name];
|
|
614
840
|
if (!component) {
|
|
615
|
-
error(`Component "${name}" not found
|
|
841
|
+
error(`Component "${name}" not found.`);
|
|
616
842
|
continue;
|
|
617
843
|
}
|
|
618
844
|
let hasDiff = false;
|
|
619
|
-
console.log(`\n[diff] ${name}`);
|
|
845
|
+
console.log(`\n${c.bold}[diff] ${name}${c.reset}`);
|
|
620
846
|
for (const file of component.files) {
|
|
621
847
|
const targetPath = path.join(cwd, file.path);
|
|
622
848
|
if (!fs.existsSync(targetPath)) {
|
|
623
|
-
console.log(` + [new file] ${file.path}`);
|
|
849
|
+
console.log(` ${c.green}+ [new file]${c.reset} ${file.path}`);
|
|
624
850
|
hasDiff = true;
|
|
625
851
|
continue;
|
|
626
852
|
}
|
|
627
853
|
const localContent = fs.readFileSync(targetPath, 'utf-8');
|
|
628
854
|
if (localContent === file.content) continue;
|
|
629
855
|
hasDiff = true;
|
|
630
|
-
console.log(`\n
|
|
856
|
+
console.log(`\n ${c.yellow}~${c.reset} ${file.path}`);
|
|
631
857
|
const localLines = localContent.split('\n');
|
|
632
858
|
const remoteLines = file.content.split('\n');
|
|
633
859
|
const maxLen = Math.max(localLines.length, remoteLines.length);
|
|
634
860
|
let shownLines = 0;
|
|
635
861
|
for (let i = 0; i < maxLen; i++) {
|
|
636
862
|
if (localLines[i] !== remoteLines[i]) {
|
|
637
|
-
if (localLines[i] !== undefined) console.log(` - ${localLines[i]}`);
|
|
638
|
-
if (remoteLines[i] !== undefined) console.log(` + ${remoteLines[i]}`);
|
|
863
|
+
if (localLines[i] !== undefined) console.log(` ${c.red}- ${localLines[i]}${c.reset}`);
|
|
864
|
+
if (remoteLines[i] !== undefined) console.log(` ${c.green}+ ${remoteLines[i]}${c.reset}`);
|
|
639
865
|
shownLines++;
|
|
640
866
|
if (shownLines >= 20) {
|
|
641
867
|
const remaining = maxLen - i - 1;
|
|
642
|
-
if (remaining > 0) console.log(` ... and ${remaining} more lines`);
|
|
868
|
+
if (remaining > 0) console.log(` ${c.dim}... and ${remaining} more lines${c.reset}`);
|
|
643
869
|
break;
|
|
644
870
|
}
|
|
645
871
|
}
|
|
646
872
|
}
|
|
647
873
|
}
|
|
648
|
-
if (!hasDiff)
|
|
874
|
+
if (!hasDiff) ok(`${name}: already up to date.`);
|
|
649
875
|
}
|
|
650
876
|
break;
|
|
651
877
|
}
|
|
652
878
|
|
|
653
879
|
case 'doctor': {
|
|
654
|
-
log(
|
|
880
|
+
console.log(`\n${c.bold}Project Health Check${c.reset}\n`);
|
|
655
881
|
let issues = 0;
|
|
656
|
-
const check = (
|
|
657
|
-
console.log(
|
|
658
|
-
if (!
|
|
882
|
+
const check = (passed: boolean, msg: string, fix?: string) => {
|
|
883
|
+
console.log(` ${passed ? `${c.green}✔${c.reset}` : `${c.red}✖${c.reset}`} ${msg}`);
|
|
884
|
+
if (!passed) { if (fix) console.log(` ${c.dim}→ ${fix}${c.reset}`); issues++; }
|
|
659
885
|
};
|
|
660
886
|
|
|
661
|
-
//
|
|
887
|
+
// Core files
|
|
662
888
|
check(fs.existsSync(path.join(cwd, 'src/lib/utils/cn.ts')),
|
|
663
889
|
'src/lib/utils/cn.ts', 'run: npx basuicn init');
|
|
664
890
|
check(fs.existsSync(path.join(cwd, 'src/lib/theme/themes.ts')),
|
|
@@ -668,7 +894,7 @@ const main = async () => {
|
|
|
668
894
|
check(fs.existsSync(path.join(cwd, 'src/styles/index.css')),
|
|
669
895
|
'src/styles/index.css (theme variables)', 'run: npx basuicn init');
|
|
670
896
|
|
|
671
|
-
//
|
|
897
|
+
// Main entry
|
|
672
898
|
const mainPath = findMainFile(cwd);
|
|
673
899
|
if (mainPath) {
|
|
674
900
|
const mainContent = fs.readFileSync(mainPath, 'utf-8');
|
|
@@ -680,7 +906,7 @@ const main = async () => {
|
|
|
680
906
|
check(false, 'main entry file (src/main.tsx)', 'create src/main.tsx');
|
|
681
907
|
}
|
|
682
908
|
|
|
683
|
-
//
|
|
909
|
+
// Runtime packages
|
|
684
910
|
const pkgPath = path.join(cwd, 'package.json');
|
|
685
911
|
if (fs.existsSync(pkgPath)) {
|
|
686
912
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
@@ -688,7 +914,6 @@ const main = async () => {
|
|
|
688
914
|
for (const dep of RUNTIME_PACKAGES) {
|
|
689
915
|
check(!!allDeps[dep], `package: ${dep}`, `run: npm install ${dep}`);
|
|
690
916
|
}
|
|
691
|
-
// Dev packages
|
|
692
917
|
for (const dep of VITE_DEV_PACKAGES) {
|
|
693
918
|
check(!!allDeps[dep], `package (dev): ${dep}`, `run: npm install -D ${dep}`);
|
|
694
919
|
}
|
|
@@ -696,14 +921,14 @@ const main = async () => {
|
|
|
696
921
|
check(false, 'package.json found', 'run: npm init -y');
|
|
697
922
|
}
|
|
698
923
|
|
|
699
|
-
//
|
|
924
|
+
// Config files
|
|
700
925
|
const hasTailwindInCss = (() => {
|
|
701
926
|
const candidates = ['src/styles/index.css', 'src/index.css', 'src/App.css'];
|
|
702
927
|
return candidates.some(f => {
|
|
703
928
|
const p = path.join(cwd, f);
|
|
704
929
|
if (!fs.existsSync(p)) return false;
|
|
705
|
-
const
|
|
706
|
-
return
|
|
930
|
+
const content = fs.readFileSync(p, 'utf-8');
|
|
931
|
+
return content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'");
|
|
707
932
|
});
|
|
708
933
|
})();
|
|
709
934
|
check(hasTailwindInCss, '@import "tailwindcss" in CSS', 'run: npx basuicn init');
|
|
@@ -712,8 +937,8 @@ const main = async () => {
|
|
|
712
937
|
const hasAlias = tsCandidates.some(f => {
|
|
713
938
|
const p = path.join(cwd, f);
|
|
714
939
|
if (!fs.existsSync(p)) return false;
|
|
715
|
-
const
|
|
716
|
-
return
|
|
940
|
+
const content = fs.readFileSync(p, 'utf-8');
|
|
941
|
+
return content.includes('"@/*"') || content.includes("'@/*'");
|
|
717
942
|
});
|
|
718
943
|
check(hasAlias, 'TypeScript path aliases (@/*)', 'run: npx basuicn init');
|
|
719
944
|
|
|
@@ -724,35 +949,16 @@ const main = async () => {
|
|
|
724
949
|
|
|
725
950
|
console.log('');
|
|
726
951
|
if (issues === 0) {
|
|
727
|
-
|
|
952
|
+
ok(`${c.bold}All checks passed!${c.reset} Project is healthy.`);
|
|
728
953
|
} else {
|
|
729
|
-
warn(`${issues} issue(s) found. Run
|
|
954
|
+
warn(`${c.bold}${issues} issue(s) found.${c.reset} Run ${c.cyan}npx basuicn init${c.reset} to fix most issues.`);
|
|
730
955
|
}
|
|
731
956
|
break;
|
|
732
957
|
}
|
|
733
958
|
|
|
734
959
|
default: {
|
|
735
|
-
|
|
736
|
-
basuicn
|
|
737
|
-
|
|
738
|
-
Commands:
|
|
739
|
-
init Set up project: install deps, copy core files, patch main entry
|
|
740
|
-
add <name> [--force] Add component(s) to your project
|
|
741
|
-
update <name> Update component(s) to latest registry version
|
|
742
|
-
diff <name> Show diff between local and registry version
|
|
743
|
-
remove <name> Remove component(s) from your project
|
|
744
|
-
list List all available components
|
|
745
|
-
doctor Check project health and configuration
|
|
746
|
-
|
|
747
|
-
Options:
|
|
748
|
-
--local Use local registry.json instead of remote
|
|
749
|
-
--force Overwrite existing files when adding
|
|
750
|
-
|
|
751
|
-
Quick start:
|
|
752
|
-
npx basuicn init
|
|
753
|
-
npx basuicn add button
|
|
754
|
-
npx basuicn add toast
|
|
755
|
-
`);
|
|
960
|
+
error(`Unknown command: "${command}"`);
|
|
961
|
+
console.log(` Run ${c.cyan}npx basuicn --help${c.reset} to see available commands.\n`);
|
|
756
962
|
}
|
|
757
963
|
}
|
|
758
964
|
};
|