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 +86 -9
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/diff.d.ts +6 -0
- package/dist/commands/update.d.ts +6 -0
- package/dist/index.js +662 -130
- package/dist/utils/config.d.ts +12 -0
- package/dist/utils/installed.d.ts +31 -0
- package/dist/utils/pm.d.ts +11 -0
- package/dist/utils/registry.d.ts +7 -0
- package/package.json +8 -8
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
|
-
-
|
|
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
|
-
- `-
|
|
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,
|
|
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
|
-
**
|
|
269
|
+
**Display:** avatar, badge, carousel, hover-card, icon, kbd, table, tooltip
|
|
193
270
|
|
|
194
|
-
**
|
|
271
|
+
**Theme:** styles, theme-toggle
|
|
195
272
|
|
|
196
|
-
**
|
|
273
|
+
**Meta:** barrel
|
|
197
274
|
|
|
198
|
-
|
|
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
|
-
|
|
277
|
+
Run `npx bearnie list` for the always-current list.
|
|
201
278
|
|
|
202
279
|
## Usage Examples
|
|
203
280
|
|
package/dist/commands/add.d.ts
CHANGED
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
|
|
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
|
|
15
|
-
import
|
|
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 =
|
|
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 =
|
|
192
|
-
if (await
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
["
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
...
|
|
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
|
|
266
|
-
await
|
|
267
|
-
await
|
|
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
|
|
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 =
|
|
282
|
-
if (!await
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
|
322
|
-
import
|
|
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 =
|
|
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
|
|
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
|
|
668
|
+
const absPath = resolveInstallPath(
|
|
485
669
|
cwd,
|
|
486
670
|
config,
|
|
487
671
|
file.path,
|
|
488
672
|
component.type
|
|
489
673
|
);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
496
|
-
await
|
|
497
|
-
writtenFiles.push(file.
|
|
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(
|
|
724
|
+
spinner.succeed(messages.installed(component.name));
|
|
500
725
|
} catch (error) {
|
|
501
|
-
spinner.fail(`Couldn't add ${
|
|
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
|
|
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
|
-
|
|
760
|
+
const { command, args } = installCommand(pm, newDeps);
|
|
761
|
+
await execa2(command, args, { cwd });
|
|
522
762
|
}
|
|
523
763
|
if (newDevDeps.length > 0) {
|
|
524
|
-
|
|
764
|
+
const { command, args } = installCommand(pm, newDevDeps, true);
|
|
765
|
+
await execa2(command, args, { cwd });
|
|
525
766
|
}
|
|
526
|
-
depsSpinner.succeed(
|
|
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("--
|
|
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 =
|
|
639
|
-
var logo2 = `${amber("\u{1F43B}")} ${
|
|
640
|
-
var link = (text, url) => `\x1B]8;;${url}\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(` ${
|
|
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
|
-
${
|
|
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.
|
|
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
|
-
${
|
|
672
|
-
${
|
|
673
|
-
${
|
|
674
|
-
${
|
|
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
|
-
${
|
|
677
|
-
${
|
|
678
|
-
${
|
|
679
|
-
${
|
|
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
|
-
${
|
|
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
|
-
});
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/dist/utils/registry.d.ts
CHANGED
|
@@ -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.
|
|
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": "^
|
|
19
|
-
"commander": "^
|
|
20
|
-
"
|
|
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
|
-
"
|
|
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": "^
|
|
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": ">=
|
|
34
|
+
"node": ">=22.12.0"
|
|
35
35
|
},
|
|
36
36
|
"author": "Michael Andreuzza",
|
|
37
37
|
"repository": {
|