bearnie 0.1.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,13 +61,16 @@ This will:
61
61
  - Set up the `src/components/bearnie` directory
62
62
  - Create the `cn()` utility function
63
63
  - Install `clsx`, `tailwind-merge`, and `tailwindcss` dependencies
64
+ - Add the `@/*` path alias to `tsconfig.json`
65
+ - Wire `@tailwindcss/vite` into your Astro config (simple configs only — you get a hint otherwise)
66
+ - Ask for a base color and an accent color (see Themes below) and install the result to `src/styles/bearnie.css`
64
67
 
65
68
  Projects created with `create-bearnie` already include `bearnie.json` and can skip init.
66
69
 
67
70
  **Options:**
68
71
 
69
72
  - `-y, --yes` - Skip confirmation prompts and use defaults
70
- - `-c, --cwd <path>` - Set the working directory (defaults to current directory)
73
+ - `--cwd <path>` - Set the working directory (defaults to current directory)
71
74
 
72
75
  ### `add`
73
76
 
@@ -90,11 +93,14 @@ npx bearnie add barrel
90
93
  npx bearnie add
91
94
  ```
92
95
 
96
+ If a file already exists, `add` asks before overwriting it (existing files are kept if you decline).
97
+
93
98
  **Options:**
94
99
 
95
- - `-y, --yes` - Skip confirmation prompts
100
+ - `-y, --yes` - Skip confirmation prompts and overwrite existing files
96
101
  - `-a, --all` - Add all available components
97
- - `-c, --cwd <path>` - Set the working directory
102
+ - `-o, --overwrite` - Overwrite existing files without asking
103
+ - `--cwd <path>` - Set the working directory
98
104
 
99
105
  ### `list`
100
106
 
@@ -114,6 +120,69 @@ Components are grouped by category, including **Theme** (`styles`) and **Meta**
114
120
 
115
121
  - `--json` - Output as JSON
116
122
 
123
+ ### `diff`
124
+
125
+ See how your installed components differ from the current registry — useful after Bearnie ships fixes.
126
+
127
+ ```bash
128
+ # Check all installed components
129
+ npx bearnie diff
130
+
131
+ # Check specific components
132
+ npx bearnie diff button dialog
133
+
134
+ # Just list changed files without the full diff
135
+ npx bearnie diff --name-only
136
+ ```
137
+
138
+ **Options:**
139
+
140
+ - `--name-only` - Only show which files changed, not the diff
141
+ - `--cwd <path>` - Set the working directory
142
+
143
+ ### `update`
144
+
145
+ Pull the latest registry version of your installed components. Shows what will change and asks for confirmation first — updating overwrites local edits to those files, so run `diff` first if you've customized components.
146
+
147
+ ```bash
148
+ # Update everything that drifted
149
+ npx bearnie update
150
+
151
+ # Update specific components
152
+ npx bearnie update button dialog
153
+
154
+ # Skip the confirmation prompt
155
+ npx bearnie update --yes
156
+ ```
157
+
158
+ Any new npm dependencies the updated components need are installed automatically.
159
+
160
+ **Options:**
161
+
162
+ - `-y, --yes` - Skip confirmation prompt
163
+ - `--cwd <path>` - Set the working directory
164
+
165
+ ## Themes
166
+
167
+ Themes combine a **base color** (the grays used for backgrounds, text, and borders) with an **accent color** (buttons, focus rings, active states), both from Tailwind's official palette:
168
+
169
+ - **Bases:** `neutral` (default), `slate`, `gray`, `zinc`, `stone`, `mauve`, `olive`, `mist`, `taupe`
170
+ - **Accents:** neutral default, `red`, `rose`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`
171
+
172
+ Every combination is a registry entry: `styles-blue` (neutral base, blue accent), `styles-slate` (slate base, neutral accent), `styles-slate-blue`, and so on. They all install the same `bearnie.css` file, so every component works with every theme.
173
+
174
+ `init` and `create-bearnie` ask for base and accent. Switch later with:
175
+
176
+ ```bash
177
+ npx bearnie add styles-slate-blue --overwrite
178
+ ```
179
+
180
+ Switching records the theme in `bearnie.json`, so `diff` and `update` compare your CSS against the right palette.
181
+
182
+ ## Package Managers
183
+
184
+ The CLI detects your package manager from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`) and uses it for all dependency installs. No configuration needed.
185
+
117
186
  ## Configuration
118
187
 
119
188
  After running `init`, a `bearnie.json` file is created in your project root:
@@ -187,17 +256,25 @@ BEARNIE_REGISTRY_URL=http://localhost:4321/registry bearnie add button
187
256
 
188
257
  ## Available Components
189
258
 
190
- **Form:** button, input, textarea, label, checkbox, radio, select, switch
259
+ **Form:** button, button-group, checkbox, combobox, file-upload, input, input-group, input-otp, label, radio, select, slider, switch, textarea, toggle, toggle-group
260
+
261
+ **Layout:** aspect-ratio, card, scroll-area, separator
262
+
263
+ **Navigation:** breadcrumb, command, context-menu, dropdown-menu, menubar, pagination, sidebar, stepper, tabs, tree
264
+
265
+ **Feedback:** alert, empty, progress, skeleton, spinner, toast
266
+
267
+ **Disclosure:** accordion, alert-dialog, collapsible, dialog, popover, sheet
191
268
 
192
- **Layout:** card, separator, scroll-area, aspect-ratio
269
+ **Display:** avatar, badge, carousel, hover-card, icon, kbd, table, tooltip
193
270
 
194
- **Navigation:** breadcrumb, tabs, dropdown-menu
271
+ **Theme:** styles, theme-toggle
195
272
 
196
- **Feedback:** alert, badge, progress, skeleton, spinner, toast, tooltip
273
+ **Meta:** barrel
197
274
 
198
- **Disclosure:** accordion, collapsible, dialog, popover
275
+ Shared utilities (`cn`, `focus-trap`, and the `ui-runtime-*` modules) are installed automatically as dependencies of the components that need them.
199
276
 
200
- **Display:** avatar, table
277
+ Run `npx bearnie list` for the always-current list.
201
278
 
202
279
  ## Usage Examples
203
280
 
@@ -1,6 +1,7 @@
1
1
  interface AddOptions {
2
2
  yes?: boolean;
3
3
  all?: boolean;
4
+ overwrite?: boolean;
4
5
  cwd: string;
5
6
  }
6
7
  export declare function add(components: string[], options: AddOptions): Promise<void>;
@@ -0,0 +1,6 @@
1
+ interface DiffOptions {
2
+ cwd: string;
3
+ nameOnly?: boolean;
4
+ }
5
+ export declare function diff(components: string[], options: DiffOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,6 @@
1
+ interface UpdateOptions {
2
+ cwd: string;
3
+ yes?: boolean;
4
+ }
5
+ export declare function update(components: string[], options: UpdateOptions): Promise<void>;
6
+ export {};
package/dist/index.js CHANGED
@@ -5,14 +5,14 @@ import { readFileSync } from "fs";
5
5
  import { dirname, join } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
- import chalk5 from "chalk";
8
+ import chalk7 from "chalk";
9
9
 
10
10
  // src/commands/init.ts
11
11
  import chalk2 from "chalk";
12
12
  import ora from "ora";
13
13
  import prompts from "prompts";
14
- import path2 from "path";
15
- import fs2 from "fs-extra";
14
+ import path4 from "path";
15
+ import fs4 from "fs-extra";
16
16
  import { execa } from "execa";
17
17
 
18
18
  // src/utils/config.ts
@@ -23,8 +23,20 @@ var DEFAULT_CONFIG = {
23
23
  utilsDir: "src/utils",
24
24
  stylesDir: "src/styles",
25
25
  tailwindConfig: "tailwind.config.mjs",
26
- typescript: true
26
+ typescript: true,
27
+ theme: "default"
27
28
  };
29
+ function themeEntryName(theme) {
30
+ return theme === "default" ? "styles" : `styles-${theme}`;
31
+ }
32
+ function composeThemeName(base, accent) {
33
+ if (base === "neutral") return accent;
34
+ if (accent === "default") return base;
35
+ return `${base}-${accent}`;
36
+ }
37
+ function isThemeEntry(name) {
38
+ return name === "styles" || name.startsWith("styles-");
39
+ }
28
40
  var CONFIG_FILE = "bearnie.json";
29
41
  async function getProjectConfig(cwd) {
30
42
  const configPath = path.join(cwd, CONFIG_FILE);
@@ -74,6 +86,83 @@ function resolveInstallPath(cwd, config, filePath, componentType) {
74
86
  return path.join(cwd, config.componentsDir, filePath);
75
87
  }
76
88
 
89
+ // src/utils/registry.ts
90
+ import fs2 from "fs-extra";
91
+ import path2 from "path";
92
+ var REGISTRY_URL = process.env.BEARNIE_REGISTRY_URL || "https://bearnie.dev/registry";
93
+ var REGISTRY_PATH = process.env.BEARNIE_REGISTRY_PATH;
94
+ var REGISTRY_INDEX_URL = `${REGISTRY_URL}/index.json`;
95
+ async function getRegistryIndex() {
96
+ if (REGISTRY_PATH) {
97
+ const indexPath = path2.join(REGISTRY_PATH, "index.json");
98
+ if (await fs2.pathExists(indexPath)) {
99
+ return fs2.readJson(indexPath);
100
+ }
101
+ throw new Error(`Registry index not found at: ${indexPath}`);
102
+ }
103
+ const response = await fetch(REGISTRY_INDEX_URL);
104
+ if (!response.ok) {
105
+ throw new Error(`Failed to fetch registry index: ${response.statusText}`);
106
+ }
107
+ return response.json();
108
+ }
109
+ async function getComponent(name) {
110
+ if (REGISTRY_PATH) {
111
+ const componentPath = path2.join(REGISTRY_PATH, `${name}.json`);
112
+ if (await fs2.pathExists(componentPath)) {
113
+ return fs2.readJson(componentPath);
114
+ }
115
+ throw new Error(`Component "${name}" not found at: ${componentPath}`);
116
+ }
117
+ const url = `${REGISTRY_URL}/${name}.json`;
118
+ const response = await fetch(url);
119
+ if (!response.ok) {
120
+ throw new Error(`Component "${name}" not found in registry`);
121
+ }
122
+ return response.json();
123
+ }
124
+ async function resolveComponentDependencies(names, resolved = /* @__PURE__ */ new Set()) {
125
+ const result = [];
126
+ for (const name of names) {
127
+ if (resolved.has(name)) continue;
128
+ resolved.add(name);
129
+ const component = await getComponent(name);
130
+ if (component.registryDependencies?.length) {
131
+ const deps = await resolveComponentDependencies(
132
+ component.registryDependencies,
133
+ resolved
134
+ );
135
+ result.push(...deps);
136
+ }
137
+ result.push(name);
138
+ }
139
+ return result;
140
+ }
141
+
142
+ // src/utils/pm.ts
143
+ import fs3 from "fs-extra";
144
+ import path3 from "path";
145
+ function detectPackageManager(cwd) {
146
+ if (fs3.existsSync(path3.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
147
+ if (fs3.existsSync(path3.join(cwd, "bun.lock")) || fs3.existsSync(path3.join(cwd, "bun.lockb"))) {
148
+ return "bun";
149
+ }
150
+ if (fs3.existsSync(path3.join(cwd, "yarn.lock"))) return "yarn";
151
+ if (fs3.existsSync(path3.join(cwd, "package-lock.json"))) return "npm";
152
+ const userAgent = process.env.npm_config_user_agent ?? "";
153
+ if (userAgent.startsWith("pnpm")) return "pnpm";
154
+ if (userAgent.startsWith("yarn")) return "yarn";
155
+ if (userAgent.startsWith("bun")) return "bun";
156
+ return "npm";
157
+ }
158
+ function installCommand(pm, packages, dev = false) {
159
+ const devFlag = dev ? ["-D"] : [];
160
+ if (pm === "npm") {
161
+ return { command: "npm", args: ["install", ...devFlag, ...packages] };
162
+ }
163
+ return { command: pm, args: ["add", ...devFlag, ...packages] };
164
+ }
165
+
77
166
  // src/utils/ui.ts
78
167
  import chalk from "chalk";
79
168
  var brand = {
@@ -176,8 +265,65 @@ var promptsTheme = {
176
265
  };
177
266
 
178
267
  // src/commands/init.ts
268
+ async function ensureTsconfigPaths(cwd) {
269
+ const tsconfigPath = path4.join(cwd, "tsconfig.json");
270
+ if (!await fs4.pathExists(tsconfigPath)) {
271
+ await fs4.writeJson(
272
+ tsconfigPath,
273
+ {
274
+ extends: "astro/tsconfigs/strict",
275
+ compilerOptions: {
276
+ baseUrl: ".",
277
+ paths: { "@/*": ["./src/*"] }
278
+ }
279
+ },
280
+ { spaces: 2 }
281
+ );
282
+ return "done";
283
+ }
284
+ try {
285
+ const tsconfig = await fs4.readJson(tsconfigPath);
286
+ const compilerOptions = tsconfig.compilerOptions ?? {};
287
+ if (compilerOptions.paths?.["@/*"]) return "already";
288
+ tsconfig.compilerOptions = {
289
+ ...compilerOptions,
290
+ baseUrl: compilerOptions.baseUrl ?? ".",
291
+ paths: { ...compilerOptions.paths, "@/*": ["./src/*"] }
292
+ };
293
+ await fs4.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
294
+ return "done";
295
+ } catch {
296
+ return "manual";
297
+ }
298
+ }
299
+ async function wireTailwindPlugin(cwd) {
300
+ const candidates = [
301
+ "astro.config.mjs",
302
+ "astro.config.ts",
303
+ "astro.config.mts",
304
+ "astro.config.js"
305
+ ];
306
+ for (const name of candidates) {
307
+ const configPath = path4.join(cwd, name);
308
+ if (!await fs4.pathExists(configPath)) continue;
309
+ let content = await fs4.readFile(configPath, "utf-8");
310
+ if (content.includes("@tailwindcss/vite")) return "already";
311
+ if (/vite\s*:/.test(content) || !content.includes("defineConfig({")) {
312
+ return "manual";
313
+ }
314
+ content = content.replace(
315
+ "defineConfig({",
316
+ "defineConfig({\n vite: {\n plugins: [tailwindcss()],\n },"
317
+ );
318
+ content = `import tailwindcss from "@tailwindcss/vite";
319
+ ${content}`;
320
+ await fs4.writeFile(configPath, content);
321
+ return "done";
322
+ }
323
+ return "manual";
324
+ }
179
325
  async function init(options) {
180
- const cwd = path2.resolve(options.cwd);
326
+ const cwd = path4.resolve(options.cwd);
181
327
  print.logo();
182
328
  console.log(` ${messages.initStart()}`);
183
329
  print.newline();
@@ -188,8 +334,8 @@ async function init(options) {
188
334
  print.newline();
189
335
  process.exit(1);
190
336
  }
191
- const configPath = path2.join(cwd, CONFIG_FILE);
192
- if (await fs2.pathExists(configPath)) {
337
+ const configPath = path4.join(cwd, CONFIG_FILE);
338
+ if (await fs4.pathExists(configPath)) {
193
339
  print.warning(messages.alreadyInit());
194
340
  print.newline();
195
341
  const { overwrite } = options.yes ? { overwrite: true } : await prompts({
@@ -204,6 +350,7 @@ async function init(options) {
204
350
  process.exit(0);
205
351
  }
206
352
  }
353
+ const pm = detectPackageManager(cwd);
207
354
  const hasTailwind = await hasTailwindInstalled(cwd);
208
355
  if (!hasTailwind) {
209
356
  print.warning("Tailwind CSS isn't installed yet.");
@@ -219,17 +366,21 @@ async function init(options) {
219
366
  color: "green"
220
367
  }).start();
221
368
  try {
222
- await execa(
223
- "npm",
224
- ["install", "-D", "tailwindcss", "@tailwindcss/vite"],
225
- { cwd }
369
+ const { command, args } = installCommand(
370
+ pm,
371
+ ["tailwindcss", "@tailwindcss/vite"],
372
+ true
226
373
  );
374
+ await execa(command, args, { cwd });
227
375
  spinner2.succeed(brand.success("Tailwind CSS is ready"));
228
376
  } catch (error) {
229
377
  spinner2.fail("Couldn't install Tailwind CSS");
230
- print.hint(
231
- "Try manually: npm install -D tailwindcss @tailwindcss/vite"
378
+ const { command, args } = installCommand(
379
+ pm,
380
+ ["tailwindcss", "@tailwindcss/vite"],
381
+ true
232
382
  );
383
+ print.hint(`Try manually: ${command} ${args.join(" ")}`);
233
384
  }
234
385
  }
235
386
  }
@@ -238,6 +389,14 @@ async function init(options) {
238
389
  print.newline();
239
390
  console.log(` ${chalk2.bold("Where should things go?")}`);
240
391
  print.newline();
392
+ let themeBases = ["neutral"];
393
+ let themeAccents = ["default"];
394
+ try {
395
+ const index = await getRegistryIndex();
396
+ if (index.themeBases?.length) themeBases = index.themeBases;
397
+ if (index.themeAccents?.length) themeAccents = index.themeAccents;
398
+ } catch {
399
+ }
241
400
  const responses = await prompts([
242
401
  {
243
402
  type: "text",
@@ -250,11 +409,37 @@ async function init(options) {
250
409
  name: "utilsDir",
251
410
  message: "Utilities directory",
252
411
  initial: DEFAULT_CONFIG.utilsDir
253
- }
412
+ },
413
+ ...themeBases.length > 1 ? [
414
+ {
415
+ type: "select",
416
+ name: "themeBase",
417
+ message: "Base color (grays and surfaces)",
418
+ choices: themeBases.map((name) => ({
419
+ title: name,
420
+ value: name
421
+ })),
422
+ initial: 0
423
+ }
424
+ ] : [],
425
+ ...themeAccents.length > 1 ? [
426
+ {
427
+ type: "select",
428
+ name: "themeAccent",
429
+ message: "Accent color (buttons, focus rings)",
430
+ choices: themeAccents.map((name) => ({
431
+ title: name === "default" ? "default (neutral)" : name,
432
+ value: name
433
+ })),
434
+ initial: 0
435
+ }
436
+ ] : []
254
437
  ]);
438
+ const { themeBase, themeAccent, ...dirs } = responses;
255
439
  config = {
256
440
  ...config,
257
- ...responses
441
+ ...dirs,
442
+ theme: composeThemeName(themeBase ?? "neutral", themeAccent ?? "default")
258
443
  };
259
444
  }
260
445
  const spinner = ora({
@@ -262,15 +447,15 @@ async function init(options) {
262
447
  color: "green"
263
448
  }).start();
264
449
  try {
265
- await fs2.ensureDir(path2.join(cwd, config.componentsDir));
266
- await fs2.ensureDir(path2.join(cwd, config.utilsDir));
267
- await fs2.ensureDir(path2.join(cwd, config.stylesDir));
450
+ await fs4.ensureDir(path4.join(cwd, config.componentsDir));
451
+ await fs4.ensureDir(path4.join(cwd, config.utilsDir));
452
+ await fs4.ensureDir(path4.join(cwd, config.stylesDir));
268
453
  spinner.text = "Creating directories...";
269
454
  } catch (error) {
270
455
  spinner.fail("Couldn't create directories");
271
456
  process.exit(1);
272
457
  }
273
- const cnUtilContent = `import { type ClassValue, clsx } from "clsx";
458
+ const cnFallbackContent = `import { type ClassValue, clsx } from "clsx";
274
459
  import { twMerge } from "tailwind-merge";
275
460
 
276
461
  export function cn(...inputs: ClassValue[]) {
@@ -278,10 +463,17 @@ export function cn(...inputs: ClassValue[]) {
278
463
  }
279
464
  `;
280
465
  try {
281
- const utilPath = path2.join(cwd, config.utilsDir, "cn.ts");
282
- if (!await fs2.pathExists(utilPath)) {
283
- await fs2.writeFile(utilPath, cnUtilContent);
284
- const packageJson = await fs2.readJson(path2.join(cwd, "package.json"));
466
+ const utilPath = path4.join(cwd, config.utilsDir, "cn.ts");
467
+ if (!await fs4.pathExists(utilPath)) {
468
+ let cnContent = cnFallbackContent;
469
+ try {
470
+ const cnEntry = await getComponent("cn");
471
+ const cnFile = cnEntry.files.find((f) => f.name === "cn.ts");
472
+ if (cnFile) cnContent = cnFile.content;
473
+ } catch {
474
+ }
475
+ await fs4.writeFile(utilPath, cnContent);
476
+ const packageJson = await fs4.readJson(path4.join(cwd, "package.json"));
285
477
  const deps = {
286
478
  ...packageJson.dependencies,
287
479
  ...packageJson.devDependencies
@@ -291,7 +483,8 @@ export function cn(...inputs: ClassValue[]) {
291
483
  if (!("tailwind-merge" in deps)) toInstall.push("tailwind-merge");
292
484
  if (toInstall.length > 0) {
293
485
  spinner.text = "Installing utilities...";
294
- await execa("npm", ["install", ...toInstall], { cwd });
486
+ const { command, args } = installCommand(pm, toInstall);
487
+ await execa(command, args, { cwd });
295
488
  }
296
489
  }
297
490
  } catch (error) {
@@ -304,12 +497,56 @@ export function cn(...inputs: ClassValue[]) {
304
497
  spinner.fail("Couldn't save configuration");
305
498
  process.exit(1);
306
499
  }
500
+ const manualSteps = [];
501
+ const tsconfigResult = await ensureTsconfigPaths(cwd);
502
+ if (tsconfigResult === "done") {
503
+ print.step(`${brand.success("\u2713")} Added ${chalk2.cyan("@/*")} path alias to tsconfig.json`);
504
+ } else if (tsconfigResult === "manual") {
505
+ manualSteps.push(
506
+ `Add to tsconfig.json: ${chalk2.cyan(`"paths": { "@/*": ["./src/*"] }`)} under compilerOptions`
507
+ );
508
+ }
509
+ const tailwindResult = await wireTailwindPlugin(cwd);
510
+ if (tailwindResult === "done") {
511
+ print.step(`${brand.success("\u2713")} Added ${chalk2.cyan("@tailwindcss/vite")} to your Astro config`);
512
+ } else if (tailwindResult === "manual") {
513
+ manualSteps.push(
514
+ `Add ${chalk2.cyan("tailwindcss()")} from ${chalk2.cyan("@tailwindcss/vite")} to vite.plugins in your Astro config`
515
+ );
516
+ }
517
+ try {
518
+ const stylesEntry = await getComponent(themeEntryName(config.theme));
519
+ let stylesWritten = false;
520
+ for (const file of stylesEntry.files) {
521
+ const stylesPath = path4.join(cwd, config.stylesDir, file.name);
522
+ if (!await fs4.pathExists(stylesPath)) {
523
+ await fs4.ensureDir(path4.dirname(stylesPath));
524
+ await fs4.writeFile(stylesPath, file.content);
525
+ stylesWritten = true;
526
+ }
527
+ }
528
+ if (stylesWritten) {
529
+ const themeLabel = config.theme === "default" ? "" : ` (${config.theme} theme)`;
530
+ print.step(
531
+ `${brand.success("\u2713")} Added theme variables to ${chalk2.cyan(`${config.stylesDir}/bearnie.css`)}${themeLabel}`
532
+ );
533
+ manualSteps.push(
534
+ `Import the styles in your global CSS: ${chalk2.cyan(`@import "./bearnie.css";`)} (after ${chalk2.cyan(`@import "tailwindcss";`)})`
535
+ );
536
+ } else if (config.theme !== "default") {
537
+ manualSteps.push(
538
+ `bearnie.css already exists \u2014 switch to the ${config.theme} theme with ${chalk2.cyan(`npx bearnie add ${themeEntryName(config.theme)} --overwrite`)}`
539
+ );
540
+ }
541
+ } catch {
542
+ manualSteps.push(`Add theme variables: ${chalk2.cyan("npx bearnie add styles")}`);
543
+ }
307
544
  print.newline();
308
545
  console.log(` ${messages.initSuccess()}`);
309
546
  print.nextSteps([
547
+ ...manualSteps,
310
548
  `Add your first component: ${chalk2.cyan("npx bearnie add button")}`,
311
- `Browse all components: ${chalk2.cyan("npx bearnie list")}`,
312
- `Add CSS variables: ${chalk2.cyan("npx bearnie add styles")}`
549
+ `Browse all components: ${chalk2.cyan("npx bearnie list")}`
313
550
  ]);
314
551
  print.footer();
315
552
  }
@@ -318,70 +555,16 @@ export function cn(...inputs: ClassValue[]) {
318
555
  import chalk3 from "chalk";
319
556
  import ora2 from "ora";
320
557
  import prompts2 from "prompts";
321
- import path4 from "path";
322
- import fs4 from "fs-extra";
558
+ import path5 from "path";
559
+ import fs5 from "fs-extra";
323
560
  import { execa as execa2 } from "execa";
324
-
325
- // src/utils/registry.ts
326
- import fs3 from "fs-extra";
327
- import path3 from "path";
328
- var REGISTRY_URL = process.env.BEARNIE_REGISTRY_URL || "https://bearnie.dev/registry";
329
- var REGISTRY_PATH = process.env.BEARNIE_REGISTRY_PATH;
330
- var REGISTRY_INDEX_URL = `${REGISTRY_URL}/index.json`;
331
- async function getRegistryIndex() {
332
- if (REGISTRY_PATH) {
333
- const indexPath = path3.join(REGISTRY_PATH, "index.json");
334
- if (await fs3.pathExists(indexPath)) {
335
- return fs3.readJson(indexPath);
336
- }
337
- throw new Error(`Registry index not found at: ${indexPath}`);
338
- }
339
- const response = await fetch(REGISTRY_INDEX_URL);
340
- if (!response.ok) {
341
- throw new Error(`Failed to fetch registry index: ${response.statusText}`);
342
- }
343
- return response.json();
344
- }
345
- async function getComponent(name) {
346
- if (REGISTRY_PATH) {
347
- const componentPath = path3.join(REGISTRY_PATH, `${name}.json`);
348
- if (await fs3.pathExists(componentPath)) {
349
- return fs3.readJson(componentPath);
350
- }
351
- throw new Error(`Component "${name}" not found at: ${componentPath}`);
352
- }
353
- const url = `${REGISTRY_URL}/${name}.json`;
354
- const response = await fetch(url);
355
- if (!response.ok) {
356
- throw new Error(`Component "${name}" not found in registry`);
357
- }
358
- return response.json();
359
- }
360
- async function resolveComponentDependencies(names, resolved = /* @__PURE__ */ new Set()) {
361
- const result = [];
362
- for (const name of names) {
363
- if (resolved.has(name)) continue;
364
- resolved.add(name);
365
- const component = await getComponent(name);
366
- if (component.registryDependencies?.length) {
367
- const deps = await resolveComponentDependencies(
368
- component.registryDependencies,
369
- resolved
370
- );
371
- result.push(...deps);
372
- }
373
- result.push(name);
374
- }
375
- return result;
376
- }
377
-
378
- // src/commands/add.ts
379
561
  async function add(components, options) {
380
- const cwd = path4.resolve(options.cwd);
562
+ const cwd = path5.resolve(options.cwd);
381
563
  print.logo();
382
564
  console.log(` ${messages.addStart()}`);
383
565
  print.newline();
384
566
  let config = await getProjectConfig(cwd);
567
+ const hadConfig = config !== null;
385
568
  if (!config) {
386
569
  print.warning(
387
570
  `Project not initialized. Run ${chalk3.cyan("npx bearnie init")} first.`
@@ -435,8 +618,11 @@ async function add(components, options) {
435
618
  }
436
619
  selectedComponents = selected;
437
620
  } else {
438
- const availableNames = registryIndex.components.map((c) => c.name);
439
- const invalid = components.filter((c) => !availableNames.includes(c));
621
+ const availableNames = new Set(registryIndex.components.map((c) => c.name));
622
+ for (const theme of registryIndex.themes ?? []) {
623
+ availableNames.add(theme === "default" ? "styles" : `styles-${theme}`);
624
+ }
625
+ const invalid = components.filter((c) => !availableNames.has(c));
440
626
  if (invalid.length > 0) {
441
627
  print.error(messages.unknownComponent(invalid));
442
628
  print.hint(`Run ${chalk3.cyan("npx bearnie list")} to see what's available.`);
@@ -471,46 +657,99 @@ async function add(components, options) {
471
657
  const npmDevDeps = /* @__PURE__ */ new Set();
472
658
  const writtenFiles = [];
473
659
  const skippedFiles = [];
660
+ const plans = [];
474
661
  for (const componentName of allComponents) {
475
- const spinner = ora2({
476
- text: messages.installing(componentName),
477
- color: "green"
478
- }).start();
479
662
  try {
480
663
  const component = await getComponent(componentName);
481
664
  component.dependencies?.forEach((d) => npmDeps.add(d));
482
665
  component.devDependencies?.forEach((d) => npmDevDeps.add(d));
666
+ const files = [];
483
667
  for (const file of component.files) {
484
- const filePath = resolveInstallPath(
668
+ const absPath = resolveInstallPath(
485
669
  cwd,
486
670
  config,
487
671
  file.path,
488
672
  component.type
489
673
  );
490
- const exists = await fs4.pathExists(filePath);
491
- if (exists && !options.yes) {
492
- skippedFiles.push(file.path);
674
+ files.push({
675
+ registryPath: file.path,
676
+ absPath,
677
+ content: file.content,
678
+ exists: await fs5.pathExists(absPath)
679
+ });
680
+ }
681
+ plans.push({ component, files });
682
+ } catch (error) {
683
+ print.error(`Couldn't fetch ${componentName}`);
684
+ print.hint(`${error}`);
685
+ }
686
+ }
687
+ const existingFiles = plans.flatMap((p) => p.files.filter((f) => f.exists));
688
+ let overwrite = Boolean(options.yes || options.overwrite);
689
+ if (existingFiles.length > 0 && !overwrite) {
690
+ print.warning(
691
+ `${existingFiles.length} file${existingFiles.length > 1 ? "s" : ""} already exist${existingFiles.length > 1 ? "" : "s"}:`
692
+ );
693
+ existingFiles.slice(0, 5).forEach((f) => {
694
+ console.log(brand.muted(` ${f.registryPath}`));
695
+ });
696
+ if (existingFiles.length > 5) {
697
+ console.log(brand.muted(` ...and ${existingFiles.length - 5} more`));
698
+ }
699
+ print.newline();
700
+ const { confirmOverwrite } = await prompts2({
701
+ type: "confirm",
702
+ name: "confirmOverwrite",
703
+ message: "Overwrite existing files?",
704
+ initial: false
705
+ });
706
+ overwrite = Boolean(confirmOverwrite);
707
+ print.newline();
708
+ }
709
+ for (const { component, files } of plans) {
710
+ const spinner = ora2({
711
+ text: messages.installing(component.name),
712
+ color: "green"
713
+ }).start();
714
+ try {
715
+ for (const file of files) {
716
+ if (file.exists && !overwrite) {
717
+ skippedFiles.push(file.registryPath);
493
718
  continue;
494
719
  }
495
- await fs4.ensureDir(path4.dirname(filePath));
496
- await fs4.writeFile(filePath, file.content);
497
- writtenFiles.push(file.path);
720
+ await fs5.ensureDir(path5.dirname(file.absPath));
721
+ await fs5.writeFile(file.absPath, file.content);
722
+ writtenFiles.push(file.registryPath);
498
723
  }
499
- spinner.succeed(messages.installed(componentName));
724
+ spinner.succeed(messages.installed(component.name));
500
725
  } catch (error) {
501
- spinner.fail(`Couldn't add ${componentName}`);
726
+ spinner.fail(`Couldn't add ${component.name}`);
502
727
  print.hint(`${error}`);
503
728
  }
504
729
  }
730
+ if (hadConfig) {
731
+ const themeInstalled = selectedComponents.find(
732
+ (name) => isThemeEntry(name)
733
+ );
734
+ if (themeInstalled) {
735
+ const theme = themeInstalled === "styles" ? "default" : themeInstalled.replace(/^styles-/, "");
736
+ if (config.theme !== theme) {
737
+ config = { ...config, theme };
738
+ await saveProjectConfig(cwd, config);
739
+ print.step(`${brand.success("\u2713")} Theme set to ${chalk3.cyan(theme)} in bearnie.json`);
740
+ }
741
+ }
742
+ }
505
743
  const allDeps = [...npmDeps];
506
744
  const allDevDeps = [...npmDevDeps];
507
745
  if (allDeps.length > 0 || allDevDeps.length > 0) {
746
+ const pm = detectPackageManager(cwd);
508
747
  const depsSpinner = ora2({
509
748
  text: messages.installingDeps(),
510
749
  color: "green"
511
750
  }).start();
512
751
  try {
513
- const packageJson = await fs4.readJson(path4.join(cwd, "package.json"));
752
+ const packageJson = await fs5.readJson(path5.join(cwd, "package.json"));
514
753
  const existingDeps = {
515
754
  ...packageJson.dependencies,
516
755
  ...packageJson.devDependencies
@@ -518,12 +757,14 @@ async function add(components, options) {
518
757
  const newDeps = allDeps.filter((d) => !(d in existingDeps));
519
758
  const newDevDeps = allDevDeps.filter((d) => !(d in existingDeps));
520
759
  if (newDeps.length > 0) {
521
- await execa2("npm", ["install", ...newDeps], { cwd });
760
+ const { command, args } = installCommand(pm, newDeps);
761
+ await execa2(command, args, { cwd });
522
762
  }
523
763
  if (newDevDeps.length > 0) {
524
- await execa2("npm", ["install", "-D", ...newDevDeps], { cwd });
764
+ const { command, args } = installCommand(pm, newDevDeps, true);
765
+ await execa2(command, args, { cwd });
525
766
  }
526
- depsSpinner.succeed("Dependencies installed");
767
+ depsSpinner.succeed(`Dependencies installed ${brand.muted(`(${pm})`)}`);
527
768
  } catch {
528
769
  depsSpinner.fail("Some dependencies couldn't be installed");
529
770
  print.hint("You might need to install them manually.");
@@ -546,7 +787,7 @@ async function add(components, options) {
546
787
  console.log(
547
788
  ` ${brand.warning("\u26A0")} Skipped ${skippedFiles.length} existing file${skippedFiles.length > 1 ? "s" : ""}`
548
789
  );
549
- print.hint(`Use ${chalk3.cyan("--yes")} to overwrite.`);
790
+ print.hint(`Use ${chalk3.cyan("--overwrite")} to replace them.`);
550
791
  }
551
792
  print.newline();
552
793
  print.success();
@@ -630,14 +871,300 @@ async function list(options) {
630
871
  }
631
872
  }
632
873
 
874
+ // src/commands/diff.ts
875
+ import chalk5 from "chalk";
876
+ import ora4 from "ora";
877
+ import path6 from "path";
878
+ import { structuredPatch } from "diff";
879
+
880
+ // src/utils/installed.ts
881
+ import fs6 from "fs-extra";
882
+ async function getAllRegistryNames() {
883
+ const index = await getRegistryIndex();
884
+ const names = new Set(index.components.map((c) => c.name));
885
+ for (const utility of index.utilities ?? []) {
886
+ names.add(utility.name);
887
+ }
888
+ for (const theme of index.themes ?? []) {
889
+ names.add(themeEntryName(theme));
890
+ }
891
+ return [...names];
892
+ }
893
+ async function getEntryState(cwd, config, name) {
894
+ const entry = await getComponent(name);
895
+ const files = [];
896
+ let anyExists = false;
897
+ for (const file of entry.files) {
898
+ const absPath = resolveInstallPath(cwd, config, file.path, entry.type);
899
+ const exists = await fs6.pathExists(absPath);
900
+ let status = "missing";
901
+ let localContent;
902
+ if (exists) {
903
+ anyExists = true;
904
+ localContent = await fs6.readFile(absPath, "utf-8");
905
+ status = localContent === file.content ? "unchanged" : "modified";
906
+ }
907
+ files.push({
908
+ path: file.path,
909
+ absPath,
910
+ status,
911
+ localContent,
912
+ registryContent: file.content
913
+ });
914
+ }
915
+ if (!anyExists) return null;
916
+ return {
917
+ entry,
918
+ files,
919
+ hasChanges: files.some((f) => f.status !== "unchanged")
920
+ };
921
+ }
922
+ async function getInstalledEntries(cwd, config, names) {
923
+ const allNames = await getAllRegistryNames();
924
+ if (names?.length) {
925
+ const unknown = names.filter((n) => !allNames.includes(n));
926
+ if (unknown.length > 0) {
927
+ throw new Error(`Unknown components: ${unknown.join(", ")}`);
928
+ }
929
+ }
930
+ const activeTheme = themeEntryName(config.theme ?? "default");
931
+ const targets = names?.length ? names : allNames.filter((name) => !isThemeEntry(name) || name === activeTheme);
932
+ const installed = [];
933
+ for (const name of targets) {
934
+ const state = await getEntryState(cwd, config, name);
935
+ if (state) installed.push(state);
936
+ }
937
+ return installed;
938
+ }
939
+
940
+ // src/commands/diff.ts
941
+ function printFileDiff(filePath, local, registry) {
942
+ const patch = structuredPatch(filePath, filePath, local, registry, "", "", {
943
+ context: 3
944
+ });
945
+ for (const hunk of patch.hunks) {
946
+ console.log(
947
+ brand.info(
948
+ ` @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`
949
+ )
950
+ );
951
+ for (const line of hunk.lines) {
952
+ if (line.startsWith("+")) {
953
+ console.log(` ${chalk5.green(line)}`);
954
+ } else if (line.startsWith("-")) {
955
+ console.log(` ${chalk5.red(line)}`);
956
+ } else {
957
+ console.log(` ${chalk5.dim(line)}`);
958
+ }
959
+ }
960
+ }
961
+ }
962
+ async function diff(components, options) {
963
+ const cwd = path6.resolve(options.cwd);
964
+ const config = await getProjectConfig(cwd) ?? DEFAULT_CONFIG;
965
+ print.logo();
966
+ console.log(` Comparing your components against the registry...`);
967
+ print.newline();
968
+ const spinner = ora4({ text: messages.fetching(), color: "green" }).start();
969
+ let installed;
970
+ try {
971
+ installed = await getInstalledEntries(cwd, config, components);
972
+ } catch (error) {
973
+ spinner.fail(
974
+ error instanceof Error ? error.message : messages.networkError()
975
+ );
976
+ print.hint(`Run ${chalk5.cyan("npx bearnie list")} to see valid names.`);
977
+ print.newline();
978
+ process.exit(1);
979
+ }
980
+ if (installed.length === 0) {
981
+ spinner.fail("No installed Bearnie components found.");
982
+ print.hint(`Add some first: ${chalk5.cyan("npx bearnie add button")}`);
983
+ print.newline();
984
+ return;
985
+ }
986
+ const changed = installed.filter((item) => item.hasChanges);
987
+ spinner.succeed(
988
+ `Checked ${chalk5.bold(installed.length)} installed component${installed.length > 1 ? "s" : ""}`
989
+ );
990
+ print.newline();
991
+ if (changed.length === 0) {
992
+ console.log(
993
+ ` ${brand.success("\u2713")} Everything is up to date with the registry.`
994
+ );
995
+ print.newline();
996
+ return;
997
+ }
998
+ for (const item of changed) {
999
+ console.log(` ${chalk5.bold(item.entry.name)}`);
1000
+ for (const file of item.files) {
1001
+ if (file.status === "unchanged") continue;
1002
+ if (file.status === "missing") {
1003
+ console.log(
1004
+ ` ${brand.warning("+")} ${file.path} ${brand.muted("(not installed yet)")}`
1005
+ );
1006
+ continue;
1007
+ }
1008
+ console.log(` ${brand.warning("~")} ${file.path}`);
1009
+ if (!options.nameOnly && file.localContent !== void 0) {
1010
+ printFileDiff(file.path, file.localContent, file.registryContent);
1011
+ }
1012
+ }
1013
+ print.newline();
1014
+ }
1015
+ console.log(
1016
+ ` ${chalk5.bold(changed.length)} component${changed.length > 1 ? "s" : ""} differ${changed.length > 1 ? "" : "s"} from the registry.`
1017
+ );
1018
+ print.hint(
1019
+ `Pull the registry version with ${chalk5.cyan(`npx bearnie update ${changed.map((c) => c.entry.name).join(" ")}`)}`
1020
+ );
1021
+ print.newline();
1022
+ }
1023
+
1024
+ // src/commands/update.ts
1025
+ import chalk6 from "chalk";
1026
+ import ora5 from "ora";
1027
+ import prompts3 from "prompts";
1028
+ import path7 from "path";
1029
+ import fs7 from "fs-extra";
1030
+ import { execa as execa3 } from "execa";
1031
+ async function update(components, options) {
1032
+ const cwd = path7.resolve(options.cwd);
1033
+ const config = await getProjectConfig(cwd) ?? DEFAULT_CONFIG;
1034
+ print.logo();
1035
+ console.log(` Let's bring your components up to date.`);
1036
+ print.newline();
1037
+ const spinner = ora5({ text: messages.fetching(), color: "green" }).start();
1038
+ let installed;
1039
+ try {
1040
+ installed = await getInstalledEntries(cwd, config, components);
1041
+ } catch (error) {
1042
+ spinner.fail(
1043
+ error instanceof Error ? error.message : messages.networkError()
1044
+ );
1045
+ print.hint(`Run ${chalk6.cyan("npx bearnie list")} to see valid names.`);
1046
+ print.newline();
1047
+ process.exit(1);
1048
+ }
1049
+ if (installed.length === 0) {
1050
+ spinner.fail("No installed Bearnie components found.");
1051
+ print.hint(`Add some first: ${chalk6.cyan("npx bearnie add button")}`);
1052
+ print.newline();
1053
+ return;
1054
+ }
1055
+ const changed = installed.filter((item) => item.hasChanges);
1056
+ spinner.succeed(
1057
+ `Checked ${chalk6.bold(installed.length)} installed component${installed.length > 1 ? "s" : ""}`
1058
+ );
1059
+ print.newline();
1060
+ if (changed.length === 0) {
1061
+ console.log(
1062
+ ` ${brand.success("\u2713")} Everything is already up to date.`
1063
+ );
1064
+ print.newline();
1065
+ return;
1066
+ }
1067
+ const changedFiles = changed.flatMap(
1068
+ (item) => item.files.filter((file) => file.status !== "unchanged")
1069
+ );
1070
+ const modifiedCount = changedFiles.filter(
1071
+ (file) => file.status === "modified"
1072
+ ).length;
1073
+ console.log(` ${chalk6.bold("These will be updated:")}`);
1074
+ for (const item of changed) {
1075
+ const parts = item.files.filter((file) => file.status !== "unchanged").map(
1076
+ (file) => file.status === "missing" ? `${file.path} (new)` : file.path
1077
+ );
1078
+ console.log(
1079
+ ` ${brand.warning("~")} ${chalk6.bold(item.entry.name)} ${brand.muted(`(${parts.join(", ")})`)}`
1080
+ );
1081
+ }
1082
+ print.newline();
1083
+ if (modifiedCount > 0) {
1084
+ print.warning(
1085
+ `${modifiedCount} file${modifiedCount > 1 ? "s" : ""} differ${modifiedCount > 1 ? "" : "s"} locally \u2014 updating overwrites your local edits.`
1086
+ );
1087
+ print.hint(
1088
+ `Review changes first with ${chalk6.cyan("npx bearnie diff")}`
1089
+ );
1090
+ print.newline();
1091
+ }
1092
+ if (!options.yes) {
1093
+ const { proceed } = await prompts3({
1094
+ type: "confirm",
1095
+ name: "proceed",
1096
+ message: `Update ${changed.length} component${changed.length > 1 ? "s" : ""}?`,
1097
+ initial: true
1098
+ });
1099
+ if (!proceed) {
1100
+ print.hint("No changes made.");
1101
+ print.newline();
1102
+ return;
1103
+ }
1104
+ print.newline();
1105
+ }
1106
+ const npmDeps = /* @__PURE__ */ new Set();
1107
+ let written = 0;
1108
+ for (const item of changed) {
1109
+ const writeSpinner = ora5({
1110
+ text: `Updating ${chalk6.cyan(item.entry.name)}...`,
1111
+ color: "green"
1112
+ }).start();
1113
+ try {
1114
+ for (const file of item.files) {
1115
+ if (file.status === "unchanged") continue;
1116
+ await fs7.ensureDir(path7.dirname(file.absPath));
1117
+ await fs7.writeFile(file.absPath, file.registryContent);
1118
+ written++;
1119
+ }
1120
+ item.entry.dependencies?.forEach((dep) => npmDeps.add(dep));
1121
+ writeSpinner.succeed(`${item.entry.name} updated`);
1122
+ } catch (error) {
1123
+ writeSpinner.fail(`Couldn't update ${item.entry.name}`);
1124
+ print.hint(`${error}`);
1125
+ }
1126
+ }
1127
+ if (npmDeps.size > 0) {
1128
+ try {
1129
+ const packageJson = await fs7.readJson(path7.join(cwd, "package.json"));
1130
+ const existing = {
1131
+ ...packageJson.dependencies,
1132
+ ...packageJson.devDependencies
1133
+ };
1134
+ const missing = [...npmDeps].filter((dep) => !(dep in existing));
1135
+ if (missing.length > 0) {
1136
+ const pm = detectPackageManager(cwd);
1137
+ const depsSpinner = ora5({
1138
+ text: `Installing ${missing.join(", ")} with ${pm}...`,
1139
+ color: "green"
1140
+ }).start();
1141
+ try {
1142
+ const { command, args } = installCommand(pm, missing);
1143
+ await execa3(command, args, { cwd });
1144
+ depsSpinner.succeed("Dependencies installed");
1145
+ } catch {
1146
+ depsSpinner.fail("Some dependencies couldn't be installed");
1147
+ print.hint(`Install manually: ${missing.join(" ")}`);
1148
+ }
1149
+ }
1150
+ } catch {
1151
+ }
1152
+ }
1153
+ print.newline();
1154
+ console.log(
1155
+ ` ${brand.success("\u2713")} Updated ${chalk6.bold(written)} file${written > 1 ? "s" : ""} across ${chalk6.bold(changed.length)} component${changed.length > 1 ? "s" : ""}.`
1156
+ );
1157
+ print.success();
1158
+ }
1159
+
633
1160
  // src/index.ts
634
1161
  var __dirname = dirname(fileURLToPath(import.meta.url));
635
1162
  var pkg = JSON.parse(
636
1163
  readFileSync(join(__dirname, "../package.json"), "utf-8")
637
1164
  );
638
- var amber = chalk5.hex("#F59E0B");
639
- var logo2 = `${amber("\u{1F43B}")} ${chalk5.bold("bearnie")}`;
640
- var link = (text, url) => `\x1B]8;;${url}\x07${chalk5.cyan(text)}\x1B]8;;\x07`;
1165
+ var amber = chalk7.hex("#F59E0B");
1166
+ var logo2 = `${amber("\u{1F43B}")} ${chalk7.bold("bearnie")}`;
1167
+ var link = (text, url) => `\x1B]8;;${url}\x07${chalk7.cyan(text)}\x1B]8;;\x07`;
641
1168
  var program = new Command();
642
1169
  program.name("bearnie").description("UI components for Astro").version(pkg.version).configureOutput({
643
1170
  writeOut: (str) => process.stdout.write(str),
@@ -647,7 +1174,7 @@ program.name("bearnie").description("UI components for Astro").version(pkg.versi
647
1174
  ${logo2}
648
1175
 
649
1176
  `);
650
- write(` ${chalk5.red("Oops!")} ${str.replace("error: ", "")}
1177
+ write(` ${chalk7.red("Oops!")} ${str.replace("error: ", "")}
651
1178
  `);
652
1179
  }
653
1180
  }).addHelpText("beforeAll", `
@@ -655,40 +1182,45 @@ program.name("bearnie").description("UI components for Astro").version(pkg.versi
655
1182
  `).addHelpText(
656
1183
  "afterAll",
657
1184
  `
658
- ${chalk5.dim("Made with")} ${amber("\u{1F43B}")} ${chalk5.dim("by")} ${link("Michael Andreuzza", "https://michaelandreuzza.com")}
1185
+ ${chalk7.dim("Made with")} ${amber("\u{1F43B}")} ${chalk7.dim("by")} ${link("Michael Andreuzza", "https://michaelandreuzza.com")}
659
1186
  `
660
1187
  );
661
1188
  program.command("init").description("Set up Bearnie in your project").option("-y, --yes", "Skip prompts and use defaults").option("--cwd <path>", "Working directory", process.cwd()).action(init);
662
- program.command("add").description("Add components to your project").argument("[components...]", "Components to add").option("-y, --yes", "Skip prompts and overwrite files").option("-a, --all", "Add all components").option("--cwd <path>", "Working directory", process.cwd()).action(add);
1189
+ program.command("add").description("Add components to your project").argument("[components...]", "Components to add").option("-y, --yes", "Skip prompts and overwrite files").option("-a, --all", "Add all components").option("-o, --overwrite", "Overwrite existing files without asking").option("--cwd <path>", "Working directory", process.cwd()).action(add);
663
1190
  program.command("list").description("Browse available components").option("--json", "Output as JSON").action(list);
664
- program.action(() => {
1191
+ program.command("diff").description("See how your components differ from the registry").argument("[components...]", "Components to check (defaults to all installed)").option("--name-only", "Only show which files changed, not the diff").option("--cwd <path>", "Working directory", process.cwd()).action(diff);
1192
+ program.command("update").description("Update installed components to the latest registry version").argument("[components...]", "Components to update (defaults to all installed)").option("-y, --yes", "Skip confirmation prompt").option("--cwd <path>", "Working directory", process.cwd()).action(update);
1193
+ program.argument("[args...]").action((args) => {
1194
+ if (args.length > 0) {
1195
+ console.log(`
1196
+ ${logo2}
1197
+
1198
+ ${chalk7.yellow("Hmm,")} I don't know that command: ${chalk7.red(args.join(" "))}
1199
+
1200
+ Run ${chalk7.cyan("npx bearnie --help")} to see what I can do.
1201
+ `);
1202
+ process.exit(1);
1203
+ }
665
1204
  console.log(`
666
1205
  ${logo2}
667
1206
 
668
1207
  ${amber("Hey!")} UI components for Astro.
669
1208
  Built with Tailwind CSS, no frameworks required.
670
1209
 
671
- ${chalk5.bold("Commands:")}
672
- ${chalk5.cyan("init")} Set up Bearnie in your project
673
- ${chalk5.cyan("add")} ${chalk5.dim("<name>")} Add a component
674
- ${chalk5.cyan("list")} Browse all components
1210
+ ${chalk7.bold("Commands:")}
1211
+ ${chalk7.cyan("init")} Set up Bearnie in your project
1212
+ ${chalk7.cyan("add")} ${chalk7.dim("<name>")} Add a component
1213
+ ${chalk7.cyan("list")} Browse all components
1214
+ ${chalk7.cyan("diff")} See what changed in the registry
1215
+ ${chalk7.cyan("update")} Pull the latest component fixes
675
1216
 
676
- ${chalk5.bold("Examples:")}
677
- ${chalk5.dim("$")} npx bearnie init
678
- ${chalk5.dim("$")} npx bearnie add button card
679
- ${chalk5.dim("$")} npx bearnie add --all
1217
+ ${chalk7.bold("Examples:")}
1218
+ ${chalk7.dim("$")} npx bearnie init
1219
+ ${chalk7.dim("$")} npx bearnie add button card
1220
+ ${chalk7.dim("$")} npx bearnie diff
1221
+ ${chalk7.dim("$")} npx bearnie update --yes
680
1222
 
681
- ${chalk5.dim("Run")} ${chalk5.cyan("bearnie <command> --help")} ${chalk5.dim("for more info")}
1223
+ ${chalk7.dim("Run")} ${chalk7.cyan("bearnie <command> --help")} ${chalk7.dim("for more info")}
682
1224
  `);
683
1225
  });
684
1226
  program.parse();
685
- program.on("command:*", () => {
686
- console.log(`
687
- ${logo2}
688
-
689
- ${chalk5.yellow("Hmm,")} I don't know that command: ${chalk5.red(program.args.join(" "))}
690
-
691
- Run ${chalk5.cyan("npx bearnie --help")} to see what I can do.
692
- `);
693
- process.exit(1);
694
- });
@@ -4,8 +4,20 @@ export interface ProjectConfig {
4
4
  stylesDir: string;
5
5
  tailwindConfig: string;
6
6
  typescript: boolean;
7
+ /** Which color theme is installed ("default", "amber", ...). */
8
+ theme: string;
7
9
  }
8
10
  export declare const DEFAULT_CONFIG: ProjectConfig;
11
+ /** Registry entry name for a theme: "default" -> styles, "amber" -> styles-amber. */
12
+ export declare function themeEntryName(theme: string): string;
13
+ /**
14
+ * Theme name from a base (gray scale) + accent (primary color) pair:
15
+ * neutral+default -> "default", neutral+blue -> "blue",
16
+ * slate+default -> "slate", slate+blue -> "slate-blue".
17
+ */
18
+ export declare function composeThemeName(base: string, accent: string): string;
19
+ /** True for the styles/styles-* entries, which all install the same CSS file. */
20
+ export declare function isThemeEntry(name: string): boolean;
9
21
  export declare const CONFIG_FILE = "bearnie.json";
10
22
  export declare function getProjectConfig(cwd: string): Promise<ProjectConfig | null>;
11
23
  export declare function saveProjectConfig(cwd: string, config: ProjectConfig): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { type ProjectConfig } from "./config.js";
2
+ import { type RegistryComponent } from "./registry.js";
3
+ export type FileStatus = "unchanged" | "modified" | "missing";
4
+ export interface FileState {
5
+ /** Registry-relative path (e.g. `button/Button.astro`). */
6
+ path: string;
7
+ /** Absolute path in the user's project. */
8
+ absPath: string;
9
+ status: FileStatus;
10
+ /** Current content on disk (undefined when missing). */
11
+ localContent?: string;
12
+ /** Content in the registry. */
13
+ registryContent: string;
14
+ }
15
+ export interface InstalledEntry {
16
+ entry: RegistryComponent;
17
+ files: FileState[];
18
+ hasChanges: boolean;
19
+ }
20
+ /** Every name in the registry: components, utilities, styles, themes, barrel. */
21
+ export declare function getAllRegistryNames(): Promise<string[]>;
22
+ /**
23
+ * Compares one registry entry against the project. Returns null when the
24
+ * entry is not installed (none of its files exist locally).
25
+ */
26
+ export declare function getEntryState(cwd: string, config: ProjectConfig, name: string): Promise<InstalledEntry | null>;
27
+ /**
28
+ * Scans the project for installed registry entries. When `names` is given,
29
+ * only those entries are considered; unknown names throw.
30
+ */
31
+ export declare function getInstalledEntries(cwd: string, config: ProjectConfig, names?: string[]): Promise<InstalledEntry[]>;
@@ -0,0 +1,11 @@
1
+ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
2
+ /**
3
+ * Detects the project's package manager from its lockfile, falling back to
4
+ * the npm_config_user_agent set by `npx` / `pnpm dlx` / `yarn dlx` / `bunx`.
5
+ */
6
+ export declare function detectPackageManager(cwd: string): PackageManager;
7
+ /** Arguments for installing packages with the given package manager. */
8
+ export declare function installCommand(pm: PackageManager, packages: string[], dev?: boolean): {
9
+ command: string;
10
+ args: string[];
11
+ };
@@ -24,6 +24,13 @@ export interface RegistryIndex {
24
24
  description: string;
25
25
  category: string;
26
26
  }[];
27
+ utilities?: {
28
+ name: string;
29
+ description: string;
30
+ }[];
31
+ themes?: string[];
32
+ themeBases?: string[];
33
+ themeAccents?: string[];
27
34
  }
28
35
  export declare function getRegistryIndex(): Promise<RegistryIndex>;
29
36
  export declare function getComponent(name: string): Promise<RegistryComponent>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bearnie",
3
- "version": "0.1.7",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for installing Bearnie UI components",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,23 +15,23 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "chalk": "^5.3.0",
19
- "commander": "^12.1.0",
20
- "execa": "^9.3.1",
18
+ "chalk": "^6.0.0",
19
+ "commander": "^15.0.0",
20
+ "diff": "^9.0.0",
21
+ "execa": "^10.0.1",
21
22
  "fs-extra": "^11.2.0",
22
- "node-fetch": "^3.3.2",
23
- "ora": "^8.0.1",
23
+ "ora": "^9.4.1",
24
24
  "prompts": "^2.4.2"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/fs-extra": "^11.0.4",
28
- "@types/node": "^20.14.9",
28
+ "@types/node": "^22.10.0",
29
29
  "@types/prompts": "^2.4.9",
30
30
  "tsup": "^8.1.0",
31
31
  "typescript": "^5.7.3"
32
32
  },
33
33
  "engines": {
34
- "node": ">=18"
34
+ "node": ">=22.12.0"
35
35
  },
36
36
  "author": "Michael Andreuzza",
37
37
  "repository": {