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.
- package/CHANGELOG.md +48 -1
- package/README.md +102 -28
- package/bin/bullframe.mjs +277 -0
- package/dist/css/bullframe-classless-dark.css +65 -58
- package/dist/css/bullframe-classless-dark.css.map +1 -1
- package/dist/css/bullframe-classless-dark.min.css +1 -1
- package/dist/css/bullframe-classless-dark.min.css.map +1 -1
- package/dist/css/bullframe-classless-system-default.css +61 -102
- package/dist/css/bullframe-classless-system-default.css.map +1 -1
- package/dist/css/bullframe-classless-system-default.min.css +1 -1
- package/dist/css/bullframe-classless-system-default.min.css.map +1 -1
- package/dist/css/bullframe-classless.css +37 -52
- package/dist/css/bullframe-classless.css.map +1 -1
- package/dist/css/bullframe-classless.min.css +1 -1
- package/dist/css/bullframe-classless.min.css.map +1 -1
- package/dist/css/bullframe-dark.css +216 -155
- package/dist/css/bullframe-dark.css.map +1 -1
- package/dist/css/bullframe-dark.min.css +1 -1
- package/dist/css/bullframe-dark.min.css.map +1 -1
- package/dist/css/bullframe-system-default.css +221 -154
- package/dist/css/bullframe-system-default.css.map +1 -1
- package/dist/css/bullframe-system-default.min.css +1 -1
- package/dist/css/bullframe-system-default.min.css.map +1 -1
- package/dist/css/bullframe-utilities.css +132 -65
- package/dist/css/bullframe-utilities.css.map +1 -1
- package/dist/css/bullframe-utilities.min.css +1 -1
- package/dist/css/bullframe-utilities.min.css.map +1 -1
- package/dist/css/bullframe.css +160 -115
- package/dist/css/bullframe.css.map +1 -1
- package/dist/css/bullframe.min.css +1 -1
- package/dist/css/bullframe.min.css.map +1 -1
- package/dist/skills/AGENTS.bullframe.md +46 -0
- package/dist/skills/_shared/builds.md +66 -0
- package/dist/skills/_shared/conventions.md +93 -0
- package/dist/skills/api.json +1023 -0
- package/dist/skills/bullframe-convert/SKILL.md +117 -0
- package/dist/skills/bullframe-convert/examples/after.html +43 -0
- package/dist/skills/bullframe-convert/examples/before.html +44 -0
- package/dist/skills/bullframe-core/SKILL.md +143 -0
- package/dist/skills/bullframe-docs-page/SKILL.md +89 -0
- package/dist/skills/bullframe-docs-page/examples/docs-page.html +72 -0
- package/dist/skills/bullframe-forms/SKILL.md +99 -0
- package/dist/skills/bullframe-forms/examples/contact-form.html +67 -0
- package/dist/skills/bullframe-landing-page/SKILL.md +96 -0
- package/dist/skills/bullframe-landing-page/examples/landing.html +98 -0
- package/dist/skills/index.json +134 -0
- 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.
|
|
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
|
-

|
|
4
|
+

|
|
5
5
|
|
|
6
6
|
# Bullframe CSS
|
|
7
7
|
|
|
8
|
-
Semantic by default. Any stack
|
|
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://
|
|
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
|
-
|
|
20
|
+
One CSS file for plain HTML: type, forms, tables, buttons, and layout. No build step. No JavaScript.
|
|
24
21
|
|
|
25
|
-
-
|
|
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
|
-
##
|
|
24
|
+
## Quick start
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
1. Add the stylesheet (CDN, always the latest release):
|
|
34
27
|
|
|
35
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
##
|
|
52
|
+
## Features
|
|
50
53
|
|
|
51
|
-
|
|
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
|
-
|
|
66
|
+
Ready to copy: landings, blogs, forms, docs. Plain HTML, one stylesheet.
|
|
56
67
|
|
|
57
|
-
|
|
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
|
-
[
|
|
83
|
+
[Browse all examples →](https://bullframecss.marcopontili.com/examples)
|
|
60
84
|
|
|
61
|
-
##
|
|
85
|
+
## AI skills
|
|
62
86
|
|
|
63
|
-
|
|
87
|
+
Markdown skills for AI coding agents ship in the npm package. No server. No API key.
|
|
64
88
|
|
|
65
|
-
|
|
89
|
+
```bash
|
|
90
|
+
npx bullframe.css skills install
|
|
91
|
+
```
|
|
66
92
|
|
|
67
|
-
|
|
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));
|