bullframe.css 6.0.0 → 6.2.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +102 -28
  3. package/bin/bullframe.mjs +277 -0
  4. package/dist/css/bullframe-classless-dark.css +65 -58
  5. package/dist/css/bullframe-classless-dark.css.map +1 -1
  6. package/dist/css/bullframe-classless-dark.min.css +1 -1
  7. package/dist/css/bullframe-classless-dark.min.css.map +1 -1
  8. package/dist/css/bullframe-classless-system-default.css +61 -102
  9. package/dist/css/bullframe-classless-system-default.css.map +1 -1
  10. package/dist/css/bullframe-classless-system-default.min.css +1 -1
  11. package/dist/css/bullframe-classless-system-default.min.css.map +1 -1
  12. package/dist/css/bullframe-classless.css +37 -52
  13. package/dist/css/bullframe-classless.css.map +1 -1
  14. package/dist/css/bullframe-classless.min.css +1 -1
  15. package/dist/css/bullframe-classless.min.css.map +1 -1
  16. package/dist/css/bullframe-dark.css +216 -155
  17. package/dist/css/bullframe-dark.css.map +1 -1
  18. package/dist/css/bullframe-dark.min.css +1 -1
  19. package/dist/css/bullframe-dark.min.css.map +1 -1
  20. package/dist/css/bullframe-system-default.css +221 -154
  21. package/dist/css/bullframe-system-default.css.map +1 -1
  22. package/dist/css/bullframe-system-default.min.css +1 -1
  23. package/dist/css/bullframe-system-default.min.css.map +1 -1
  24. package/dist/css/bullframe-utilities.css +132 -65
  25. package/dist/css/bullframe-utilities.css.map +1 -1
  26. package/dist/css/bullframe-utilities.min.css +1 -1
  27. package/dist/css/bullframe-utilities.min.css.map +1 -1
  28. package/dist/css/bullframe.css +160 -115
  29. package/dist/css/bullframe.css.map +1 -1
  30. package/dist/css/bullframe.min.css +1 -1
  31. package/dist/css/bullframe.min.css.map +1 -1
  32. package/dist/skills/AGENTS.bullframe.md +46 -0
  33. package/dist/skills/_shared/builds.md +66 -0
  34. package/dist/skills/_shared/conventions.md +93 -0
  35. package/dist/skills/api.json +1023 -0
  36. package/dist/skills/bullframe-convert/SKILL.md +117 -0
  37. package/dist/skills/bullframe-convert/examples/after.html +43 -0
  38. package/dist/skills/bullframe-convert/examples/before.html +44 -0
  39. package/dist/skills/bullframe-core/SKILL.md +143 -0
  40. package/dist/skills/bullframe-docs-page/SKILL.md +89 -0
  41. package/dist/skills/bullframe-docs-page/examples/docs-page.html +72 -0
  42. package/dist/skills/bullframe-forms/SKILL.md +99 -0
  43. package/dist/skills/bullframe-forms/examples/contact-form.html +67 -0
  44. package/dist/skills/bullframe-landing-page/SKILL.md +96 -0
  45. package/dist/skills/bullframe-landing-page/examples/landing.html +98 -0
  46. package/dist/skills/index.json +134 -0
  47. package/package.json +21 -7
package/CHANGELOG.md CHANGED
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.2.0] - 2026-09-29
11
+
12
+ ### Added
13
+
14
+ - AI agent skills: `bullframe-core`, `bullframe-landing-page`, `bullframe-forms`, `bullframe-docs-page`, `bullframe-convert`; `npx bullframe.css skills install` (Claude, Cursor, Codex).
15
+ - Generated `api.json` (`.bf-*` classes, `--bf-*` tokens) in the package and at `/api.json`.
16
+ - AI skills docs page; docs site 404 page.
17
+ - `npm run test:e2e:sync-from-ci` to copy Playwright linux baselines from a CI run artifact.
18
+
19
+ ### Fixed
20
+
21
+ - Non-existent classes in Grid, Cards, Spacing, and Text docs.
22
+ - Wrong v6 token names in the migration guide.
23
+
24
+ ### Changed
25
+
26
+ - npm publish is manual from the release tag; **Publish to npm** is `workflow_dispatch` only (no `release: published` trigger).
27
+
28
+ ### Security
29
+
30
+ - Explicit `permissions: contents: read` on CI, Deploy docs, and Skills eval workflows.
31
+
32
+ ## [6.1.0] - 2026-09-14
33
+
34
+ ### Added
35
+
36
+ - CSS variables `--bf-focus-ring-color`, `--bf-focus-ring-width`, `--bf-focus-ring-offset`.
37
+ - `.bf-skip-link`, `.bf-focusable`, `.bf-scheme-*`, `.bf-target-size`, `.bf-accent-native`.
38
+ - `@supports` for `text-wrap: balance` and `scrollbar-gutter: stable`.
39
+
40
+ ### Changed
41
+
42
+ - README: install, CDN, examples; unversioned CDN (docs stay pinned + SRI).
43
+ - README, hero, OG, and social preview images regenerated from one source (`npm run brand:images`).
44
+
45
+ ### Fixed
46
+
47
+ - `caption` and `progress` in classless always-dark.
48
+ - Accessibility markup in the kitchen sink and examples.
49
+ - Docs site cache headers, so a new release reaches returning visitors.
50
+
51
+ ### Removed
52
+
53
+ - Dead UA+ rules that never applied inside `@layer`.
54
+
10
55
  ## [6.0.0] - 2026-07-31 - BREAKING CHANGES
11
56
 
12
57
  ### Changed
@@ -489,5 +534,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
489
534
  - Initial commit on 19 November 2012
490
535
  - Framework aimed to kickstart cross-browser responsive HTML/CSS development
491
536
 
492
- [Unreleased]: https://github.com/marcop135/bullframe.css/compare/v6.0.0...HEAD
537
+ [Unreleased]: https://github.com/marcop135/bullframe.css/compare/v6.2.0...HEAD
538
+ [6.2.0]: https://github.com/marcop135/bullframe.css/compare/v6.1.0...v6.2.0
539
+ [6.1.0]: https://github.com/marcop135/bullframe.css/compare/v6.0.0...v6.1.0
493
540
  [6.0.0]: https://github.com/marcop135/bullframe.css/compare/v5.1.0...v6.0.0
package/README.md CHANGED
@@ -1,38 +1,41 @@
1
1
  <div align="center">
2
2
 
3
- ![Bullframe CSS](https://raw.githubusercontent.com/marcop135/bullframe.css/main/src/docs/github-readme/bf-readme-hero-light-16x9.png#gh-light-mode-only)
4
- ![Bullframe CSS](https://raw.githubusercontent.com/marcop135/bullframe.css/main/src/docs/github-readme/bf-readme-hero-dark-16x9.png#gh-dark-mode-only)
3
+ ![Bullframe CSS](src/docs/github-readme/bf-readme-hero-light-16x9.png#gh-light-mode-only)
4
+ ![Bullframe CSS](src/docs/github-readme/bf-readme-hero-dark-16x9.png#gh-dark-mode-only)
5
5
 
6
6
  # Bullframe CSS
7
7
 
8
- Semantic by default. Any stack.<br>
9
- <em>Classless when you want it. System dark built in.</em>
8
+ Semantic by default. Any stack.
10
9
 
11
10
  </div>
12
11
 
13
12
  <p align="center">
14
13
  <a href="https://www.npmjs.com/package/bullframe.css"><img src="https://img.shields.io/npm/v/bullframe.css.svg?style=flat-square" alt="npm version"></a>
15
- <a href="https://bullframecss.marcopontili.com/"><img src="https://img.shields.io/badge/docs-online-0056b3?style=flat-square" alt="Docs"></a>
16
- <img src="https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square" alt="Zero dependencies">
17
- <img src="https://img.shields.io/badge/any%20stack-555555?style=flat-square" alt="Any stack">
14
+ <a href="https://github.com/marcop135/bullframe.css/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/marcop135/bullframe.css/ci.yml?branch=main&style=flat-square" alt="CI status"></a>
18
15
  <a href="LICENSE"><img src="https://img.shields.io/github/license/marcop135/bullframe.css.svg?style=flat-square" alt="MIT License"></a>
19
16
  </p>
20
17
 
21
18
  ---
22
19
 
23
- ## Why use it
20
+ One CSS file for plain HTML: type, forms, tables, buttons, and layout. No build step. No JavaScript.
24
21
 
25
- - **Semantic HTML first:** headings, forms, tables, and links are styled without a pile of classes
26
- - **Classless when you want it:** swap in `bullframe-classless.css` and keep markup as plain HTML
27
- - **Dark without a script:** always-dark builds, or system-default builds that follow `prefers-color-scheme`
28
- - **Accessible defaults:** focus-visible rings, reduced-motion respect, WCAG AA contrast on links and primary buttons
29
- - **Native CSS, any stack:** PostCSS builds, no Sass, no runtime JavaScript, solid cross-browser support
22
+ Class-based or classless. Light, dark, or system. About 8 kB gzipped. Drop it on a landing, docs site, or help center.
30
23
 
31
- ## When to use
24
+ ## Quick start
32
25
 
33
- Pages and forms, not complex app UIs: docs, blogs, landings, help centers, listings, micro-sites. One stylesheet for reset, type, forms, and layout. Class-based or classless. No Tailwind-style utility pipeline.
26
+ 1. Add the stylesheet (CDN, always the latest release):
34
27
 
35
- ## Getting started
28
+ <!-- sri:cdn:start -->
29
+ ```html
30
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bullframe.css">
31
+ ```
32
+ <!-- sri:cdn:end -->
33
+
34
+ 2. Write semantic HTML. Headings, forms, tables, and buttons are styled without classes.
35
+
36
+ 3. Swap the build when you need classless, dark, or system. See [Builds](#builds).
37
+
38
+ Or install from npm:
36
39
 
37
40
  ```bash
38
41
  npm install bullframe.css
@@ -42,30 +45,101 @@ npm install bullframe.css
42
45
  import 'bullframe.css';
43
46
  ```
44
47
 
45
- ```html
46
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bullframe.css">
47
- ```
48
+ Default import: class-based, light. Other builds: `bullframe.css/classless`, `bullframe.css/dark`, and so on.
49
+
50
+ [Docs](https://bullframecss.marcopontili.com/) · [Examples](https://bullframecss.marcopontili.com/examples)
48
51
 
49
- ## Docs
52
+ ## Features
50
53
 
51
- [bullframecss.marcopontili.com](https://bullframecss.marcopontili.com/)
54
+ - Plain HTML looks finished before you write a class
55
+ - Classless when you want clean markup; classes when you need layout control
56
+ - Light and dark out of the box, no theme script
57
+ - One stylesheet, about 8 kB gzipped: type, forms, tables, buttons, layout
58
+ - Restyle the page by changing a few colour tokens
59
+ - Accessibility built in: clear focus, reduced motion, WCAG AA defaults
60
+ - Works with any stack: landings, docs, help centers
61
+ - npm or CDN; nothing to compile to use it
62
+ - AI skills for coding agents: `npx bullframe.css skills install`
52
63
 
53
64
  ## Examples
54
65
 
55
- [Example templates](https://bullframecss.marcopontili.com/examples)
66
+ Ready to copy: landings, blogs, forms, docs. Plain HTML, one stylesheet.
56
67
 
57
- ## Kitchen sink
68
+ <table>
69
+ <tr>
70
+ <td><a href="https://bullframecss.marcopontili.com/examples/typography/"><img src="src/docs/examples/screenshots/typography.png" alt="Typography" style="border: 1px solid #999; border-radius: 10px;" /></a></td>
71
+ <td><a href="https://bullframecss.marcopontili.com/examples/album/"><img src="src/docs/examples/screenshots/album.png" alt="Album" style="border: 1px solid #999; border-radius: 10px;" /></a></td>
72
+ </tr>
73
+ <tr>
74
+ <td><a href="https://bullframecss.marcopontili.com/examples/pricing/"><img src="src/docs/examples/screenshots/pricing.png" alt="Pricing" style="border: 1px solid #999; border-radius: 10px;" /></a></td>
75
+ <td><a href="https://bullframecss.marcopontili.com/examples/branded/"><img src="src/docs/examples/screenshots/branded.png" alt="Branded" style="border: 1px solid #999; border-radius: 10px;" /></a></td>
76
+ </tr>
77
+ <tr>
78
+ <td><a href="https://bullframecss.marcopontili.com/examples/blog/"><img src="src/docs/examples/screenshots/blog.png" alt="Blog" style="border: 1px solid #999; border-radius: 10px;" /></a></td>
79
+ <td><a href="https://bullframecss.marcopontili.com/examples/cover/"><img src="src/docs/examples/screenshots/cover.png" alt="Cover" style="border: 1px solid #999; border-radius: 10px;" /></a></td>
80
+ </tr>
81
+ </table>
58
82
 
59
- [Every styled element in one page](https://bullframecss.marcopontili.com/kitchen-sink/)
83
+ [Browse all examples →](https://bullframecss.marcopontili.com/examples)
60
84
 
61
- ## Contributing
85
+ ## AI skills
62
86
 
63
- See [CONTRIBUTING.md](./.github/CONTRIBUTING.md). Bug reports and feature requests go through [issues](https://github.com/marcop135/bullframe.css/issues).
87
+ Markdown skills for AI coding agents ship in the npm package. No server. No API key.
64
88
 
65
- ## Author
89
+ ```bash
90
+ npx bullframe.css skills install
91
+ ```
66
92
 
67
- [Marco Pontili](https://marcopontili.com)
93
+ Writes skill files plus a managed `AGENTS.md` block, and the matching layout under `.claude`, `.cursor`, or `.agents` when those folders exist.
94
+
95
+ Details: [AI skills](https://bullframecss.marcopontili.com/ai-skills).
96
+
97
+ ## Builds
98
+
99
+ <details>
100
+ <summary>See all 7 variants</summary>
101
+
102
+ Seven files in `dist/css/`, each with a minified twin and a source map. Pick one; do not combine them, except utilities on top of another build.
103
+
104
+ | Build | npm subpath | Use when |
105
+ | ---------------------------------------- | -------------------------------- | ------------------------------------ |
106
+ | `bullframe.css` | `bullframe.css` | Class-based, light |
107
+ | `bullframe-dark.css` | `bullframe.css/dark` | Class-based, always dark |
108
+ | `bullframe-system-default.css` | `bullframe.css/system` | Class-based, `prefers-color-scheme` |
109
+ | `bullframe-classless.css` | `bullframe.css/classless` | Classless, light |
110
+ | `bullframe-classless-dark.css` | `bullframe.css/classless/dark` | Classless, always dark |
111
+ | `bullframe-classless-system-default.css` | `bullframe.css/classless/system` | Classless, `prefers-color-scheme` |
112
+ | `bullframe-utilities.css` | `bullframe.css/utilities` | `.bf-*` helpers only, no base styles |
113
+
114
+ **Classless** styles HTML tags only (no `.bf-*` classes). **Class-based** adds `.bf-*` grid, spacing, and helpers on top.
115
+
116
+ </details>
117
+
118
+ ## Documentation
119
+
120
+ | Topic | |
121
+ | ----- | --- |
122
+ | [Docs](https://bullframecss.marcopontili.com/) | Home |
123
+ | [Getting started](https://bullframecss.marcopontili.com/getting-started) | Install and pick a build |
124
+ | [Examples](https://bullframecss.marcopontili.com/examples) | Page templates |
125
+ | [Kitchen sink](https://bullframecss.marcopontili.com/kitchen-sink/) | Every element in one place |
126
+ | [Variables](https://bullframecss.marcopontili.com/variables) | `--bf-*` tokens |
127
+ | [Components](https://bullframecss.marcopontili.com/components/) | Cards, modals, navigation, buttons |
128
+ | [Theming](https://bullframecss.marcopontili.com/theming) | Colours and dark mode |
129
+ | [API reference](https://bullframecss.marcopontili.com/api-reference) | Full token and class list |
130
+ | [AI skills](https://bullframecss.marcopontili.com/ai-skills) | Skills for AI coding agents |
131
+ | [Changelog](./CHANGELOG.md) | Release notes |
132
+
133
+ ## Contributing
134
+
135
+ See [CONTRIBUTING.md](./.github/CONTRIBUTING.md).
136
+
137
+ Bugs → [issues](https://github.com/marcop135/bullframe.css/issues).
68
138
 
69
139
  ## License
70
140
 
71
141
  [MIT](./LICENSE)
142
+
143
+ ## Author
144
+
145
+ [Marco Pontili](https://marcopontili.com)
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ /* Bullframe CSS command line.
3
+ *
4
+ * One job: copy the agent skills that ship in this package into the current project,
5
+ * in whatever layout the project's AI tooling reads. Zero dependencies, no network,
6
+ * no postinstall hook, and it never touches the framework's CSS.
7
+ *
8
+ * npx bullframe.css skills list
9
+ * npx bullframe.css skills install [--target claude,cursor,codex,agents,dir] [--dir <path>]
10
+ * npx bullframe.css skills path
11
+ */
12
+ import { cp, mkdir, readFile, writeFile, access } from 'node:fs/promises';
13
+ import { constants } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const here = path.dirname(fileURLToPath(import.meta.url));
18
+ const packageRoot = path.resolve(here, '..');
19
+ const skillsDir = path.join(packageRoot, 'dist', 'skills');
20
+
21
+ const START = '<!-- bullframe:skills:start -->';
22
+ const END = '<!-- bullframe:skills:end -->';
23
+ const LOCAL_DIR = 'bullframe-skills';
24
+
25
+ /** Tool targets that write into `$parent/skills/` with the same SKILL.md layout. */
26
+ const TOOL_TARGETS = {
27
+ claude: { parent: '.claude', label: '.claude/skills' },
28
+ cursor: { parent: '.cursor', label: '.cursor/skills' },
29
+ codex: { parent: '.agents', label: '.agents/skills' },
30
+ };
31
+
32
+ const KNOWN_TARGETS = [...Object.keys(TOOL_TARGETS), 'agents', 'dir'];
33
+
34
+ const USAGE = `bullframe.css - agent skills for the Bullframe CSS framework
35
+
36
+ Usage
37
+ npx bullframe.css skills list
38
+ npx bullframe.css skills install [options]
39
+ npx bullframe.css skills path
40
+
41
+ Install options
42
+ --target <list> Comma separated: claude, cursor, codex, agents, dir.
43
+ Default: agents, plus claude / cursor / codex when a
44
+ .claude, .cursor, or .agents directory exists.
45
+ --dir <path> Where the "dir" and "agents" targets copy the skills.
46
+ Default: ./${LOCAL_DIR}
47
+ --dry-run Print what would be written and exit without writing.
48
+ --force Overwrite files that already differ.
49
+
50
+ Other
51
+ --help, -h This text
52
+ --version, -v Package version
53
+ `;
54
+
55
+ function parseArgs(argv) {
56
+ const options = { targets: null, dir: null, dryRun: false, force: false };
57
+ const positional = [];
58
+
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const arg = argv[i];
61
+ if (arg === '--target') options.targets = (argv[++i] ?? '').split(',').filter(Boolean);
62
+ else if (arg === '--dir') options.dir = argv[++i];
63
+ else if (arg === '--dry-run') options.dryRun = true;
64
+ else if (arg === '--force') options.force = true;
65
+ else if (arg === '--help' || arg === '-h') options.help = true;
66
+ else if (arg === '--version' || arg === '-v') options.version = true;
67
+ else if (arg.startsWith('-')) options.unknown = arg;
68
+ else positional.push(arg);
69
+ }
70
+
71
+ return { options, positional };
72
+ }
73
+
74
+ async function exists(target) {
75
+ try {
76
+ await access(target, constants.F_OK);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ async function readIndex() {
84
+ try {
85
+ return JSON.parse(await readFile(path.join(skillsDir, 'index.json'), 'utf8'));
86
+ } catch {
87
+ fail(
88
+ `no skills found in ${skillsDir}.\nInstall the published package, or run "npm run skills:build" in a clone.`
89
+ );
90
+ }
91
+ }
92
+
93
+ function fail(message) {
94
+ console.error(`bullframe: ${message}`);
95
+ process.exit(1);
96
+ }
97
+
98
+ /** Replace the managed block in place, or append it. Never duplicates. */
99
+ function mergeAgentsBlock(existing, body) {
100
+ const block = `${START}\n${body.trimEnd()}\n${END}\n`;
101
+ if (!existing) return block;
102
+
103
+ const start = existing.indexOf(START);
104
+ const end = existing.indexOf(END);
105
+ if (start !== -1 && end !== -1 && end > start) {
106
+ return existing.slice(0, start) + block.trimEnd() + existing.slice(end + END.length);
107
+ }
108
+
109
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
110
+ return `${existing}${separator}${block}`;
111
+ }
112
+
113
+ async function writeFileIfNeeded(file, content, { dryRun, force }, written) {
114
+ const current = (await exists(file)) ? await readFile(file, 'utf8') : null;
115
+ if (current === content) {
116
+ written.push(`unchanged ${path.relative(process.cwd(), file)}`);
117
+ return;
118
+ }
119
+ if (current !== null && !force && !file.endsWith('AGENTS.md')) {
120
+ written.push(`skipped ${path.relative(process.cwd(), file)} (exists, use --force)`);
121
+ return;
122
+ }
123
+ written.push(
124
+ `${current === null ? 'wrote ' : 'updated '} ${path.relative(process.cwd(), file)}`
125
+ );
126
+ if (dryRun) return;
127
+ await mkdir(path.dirname(file), { recursive: true });
128
+ await writeFile(file, content, 'utf8');
129
+ }
130
+
131
+ async function copyTree(from, to, { dryRun }, written, label) {
132
+ written.push(`${dryRun ? 'would copy' : 'copied '} ${label}`);
133
+ if (dryRun) return;
134
+ await mkdir(path.dirname(to), { recursive: true });
135
+ await cp(from, to, { recursive: true, force: true });
136
+ }
137
+
138
+ /** Copy every skill plus `_shared` and `api.json` into `$parent/skills/`. */
139
+ async function installToolLayout(index, parentDir, label, options, written) {
140
+ const root = path.join(parentDir, 'skills');
141
+ for (const skill of index.skills) {
142
+ await copyTree(
143
+ path.join(skillsDir, skill.name),
144
+ path.join(root, skill.name),
145
+ options,
146
+ written,
147
+ `${label}/${skill.name}/`
148
+ );
149
+ }
150
+ await copyTree(
151
+ path.join(skillsDir, '_shared'),
152
+ path.join(root, '_shared'),
153
+ options,
154
+ written,
155
+ `${label}/_shared/`
156
+ );
157
+ await writeFileIfNeeded(
158
+ path.join(root, '_shared', 'api.json'),
159
+ await readFile(path.join(skillsDir, 'api.json'), 'utf8'),
160
+ { ...options, force: true },
161
+ written
162
+ );
163
+ }
164
+
165
+ async function listSkills() {
166
+ const index = await readIndex();
167
+ console.log(`bullframe.css ${index.version} - ${index.skills.length} skills\n`);
168
+ for (const skill of index.skills) {
169
+ console.log(` ${skill.name}`);
170
+ console.log(` ${skill.description}`);
171
+ if (skill.requires?.length) console.log(` requires: ${skill.requires.join(', ')}`);
172
+ }
173
+ console.log(`\nInstall with: npx bullframe.css skills install`);
174
+ }
175
+
176
+ async function installSkills(options) {
177
+ const index = await readIndex();
178
+ const cwd = process.cwd();
179
+ const localDir = path.resolve(cwd, options.dir ?? LOCAL_DIR);
180
+
181
+ let targets = options.targets;
182
+ if (!targets) {
183
+ targets = ['agents'];
184
+ for (const [name, { parent }] of Object.entries(TOOL_TARGETS)) {
185
+ if (await exists(path.join(cwd, parent))) targets.push(name);
186
+ }
187
+ }
188
+
189
+ const unknown = targets.filter((t) => !KNOWN_TARGETS.includes(t));
190
+ if (unknown.length) {
191
+ fail(`unknown target "${unknown[0]}". Use ${KNOWN_TARGETS.join(', ')}.`);
192
+ }
193
+
194
+ const written = [];
195
+
196
+ for (const name of Object.keys(TOOL_TARGETS)) {
197
+ if (!targets.includes(name)) continue;
198
+ const { parent, label } = TOOL_TARGETS[name];
199
+ await installToolLayout(index, path.join(cwd, parent), label, options, written);
200
+ }
201
+
202
+ if (targets.includes('agents') || targets.includes('dir')) {
203
+ for (const skill of index.skills) {
204
+ await copyTree(
205
+ path.join(skillsDir, skill.name),
206
+ path.join(localDir, skill.name),
207
+ options,
208
+ written,
209
+ `${path.relative(cwd, localDir)}/${skill.name}/`
210
+ );
211
+ }
212
+ for (const file of ['_shared', 'api.json', 'index.json']) {
213
+ await copyTree(
214
+ path.join(skillsDir, file),
215
+ path.join(localDir, file),
216
+ options,
217
+ written,
218
+ `${path.relative(cwd, localDir)}/${file}`
219
+ );
220
+ }
221
+ }
222
+
223
+ if (targets.includes('agents')) {
224
+ const agentsFile = path.join(cwd, 'AGENTS.md');
225
+ const body = await readFile(path.join(skillsDir, index.agentsBlock), 'utf8');
226
+ const existing = (await exists(agentsFile)) ? await readFile(agentsFile, 'utf8') : null;
227
+ await writeFileIfNeeded(
228
+ agentsFile,
229
+ mergeAgentsBlock(existing, body),
230
+ { ...options, force: true },
231
+ written
232
+ );
233
+ }
234
+
235
+ console.log(`bullframe.css ${index.version} - targets: ${targets.join(', ')}`);
236
+ for (const line of written) console.log(` ${line}`);
237
+ if (options.dryRun) console.log('\nDry run: nothing was written.');
238
+ }
239
+
240
+ async function main() {
241
+ const { options, positional } = parseArgs(process.argv.slice(2));
242
+
243
+ if (options.version) {
244
+ const pkg = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
245
+ console.log(pkg.version);
246
+ return;
247
+ }
248
+ if (options.help || positional.length === 0) {
249
+ console.log(USAGE);
250
+ return;
251
+ }
252
+ if (options.unknown) {
253
+ console.error(`bullframe: unknown option "${options.unknown}"\n`);
254
+ console.log(USAGE);
255
+ process.exit(2);
256
+ }
257
+
258
+ const [group, command] = positional;
259
+ if (group !== 'skills') {
260
+ console.error(`bullframe: unknown command "${group}"\n`);
261
+ console.log(USAGE);
262
+ process.exit(2);
263
+ }
264
+
265
+ if (command === 'list') return listSkills();
266
+ if (command === 'install') return installSkills(options);
267
+ if (command === 'path') {
268
+ console.log(skillsDir);
269
+ return;
270
+ }
271
+
272
+ console.error(`bullframe: unknown subcommand "${command ?? ''}"\n`);
273
+ console.log(USAGE);
274
+ process.exit(2);
275
+ }
276
+
277
+ main().catch((error) => fail(error.message));