Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ TypeScript-first React component library for the **Bulma v1** CSS framework —
| **Icons (optional)** | Font Awesome, Material Design Icons, Ionicons, Material Icons, Material Symbols — all optional peer dependencies |
| **Docs** | [bestax.io](https://bestax.io) · [Storybook](https://bestax.io/storybook) |
| **LLM docs** | [llms.txt](https://bestax.io/llms.txt) (curated index) · [llms-full.txt](https://bestax.io/llms-full.txt) (complete docs, one file) |
| **Agent Skills** | `npx skills add https://github.com/allxsmith/bestax --skill bestax-layout-scaffold` ([all four skills](skills/README.md)) |
| **Agent Skills** | `npx skills add https://github.com/allxsmith/bestax --skill bestax-layout-scaffold` ([the full roster](skills/README.md)) |
| **Community** | [Discord](https://discord.gg/zehJrQGtKu) · [Stack Overflow `bestax`](https://stackoverflow.com/questions/tagged/bestax) · [Issues](https://github.com/allxsmith/bestax/issues) |

> **If you are an AI agent or LLM:** fetch **<https://bestax.io/llms.txt>** for a curated index of the full documentation, or **<https://bestax.io/llms-full.txt>** for the complete docs in a single file, before answering questions about or writing code with this library. Every page on [bestax.io](https://bestax.io) is also available as raw markdown — see the [LLMs guide](https://bestax.io/docs/guides/llms).
Expand Down Expand Up @@ -164,6 +164,9 @@ Building with an AI agent (Claude Code, Cursor, Copilot)? bestax-bulma ships LLM
| `bestax-form` | Building forms — Field/Control composition and the full input inventory |
| `bestax-theming` | Customizing colors, fonts, dark mode via `Theme` and `--bulma-*` variables |
| `bestax-custom-component` | Building a new custom component beyond stock Bulma, the bestax way |
| `bestax-icons` | Adding icons — Icon/IconText and the five supported icon libraries |
| `bestax-optimize` | Shrinking the built CSS — flavor builds, modular Sass, import hygiene |
| `bestax-migrate` | Moving an app off react-bulma-components (v4) onto bestax-bulma |

```bash
npx skills add https://github.com/allxsmith/bestax --skill bestax-layout-scaffold
Expand Down
19 changes: 11 additions & 8 deletions bestax-mcp/scripts/sync-skills.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@
// `prepack` — five entry points, and turbo runs the first three concurrently,
// which is what the locking below exists for. See "Concurrency" at the bottom.
//
// Three deliberate differences from create-bestax/scripts/sync-skills.mjs,
// Two deliberate differences from create-bestax/scripts/sync-skills.mjs,
// which solves the same problem for the scaffolder:
//
// 1. The roster is READ, not listed. That file carries a hardcoded `SKILLS`
// array, and skills/CLAUDE.md counts it among the five places the roster is
// already duplicated (#385: nothing warns when a directory is absent).
// Adding a sixth would be a sixth thing to forget. Any directory with a
// SKILL.md ships.
// 2. No fs-extra. This package has one runtime dependency and no reason for a
// 1. No fs-extra. This package has one runtime dependency and no reason for a
// second; node:fs/promises `cp` covers it.
// 3. This one is concurrency-safe. The scaffolder's copy is only reachable
// 2. This one is concurrency-safe. The scaffolder's copy is only reachable
// from `build` and `prepack`, which never overlap, so it does not need to
// be — but it has the same destructive shape, so if a third caller is ever
// added there, port this.
//
// There used to be a third, and it is worth recording that it closed rather
// than leaving a reader to wonder. That file carried a hardcoded `SKILLS`
// array while this one read the directory, so a new skill could ship here and
// silently not there. #540 made it read the directory too, and added the
// `skills-roster` conformance check for the prose copies that cannot be
// derived, so the claim "nothing warns when a directory is absent" no longer
// holds anywhere.
//
// The MANIFEST (names, descriptions, reference lists) is committed instead, in
// data/skills.json — generated by scripts/gen-mcp-index.mjs. That is what the
// CI staleness gate watches, so a new or renamed skill still fails the build
Expand Down
22 changes: 12 additions & 10 deletions create-bestax/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ path (`-y` + flags, no TTY) must never hang or regress (#192).

## Sync rules (this package re-ships other parts of the repo)

- `pnpm build` and `prepack` run `scripts/sync-skills.mjs`, which copies **the skills named
in its hardcoded `SKILLS` allowlist** — not the whole repo-root `skills/` dir — into the
package. An unlisted skill silently doesn't bundle; a delisted one silently vanishes (the
script empties the destination first, and its success line reports the array length either
way); no CI check compares the list against `skills/*`. Bundling is a per-skill policy
decision (bestax-migrate's was settled as bundled, #385) — when adding a skill, decide it
explicitly and keep the allowlist, `skills/README.md`, `docs/docs/skills/intro.md`, any
per-skill docs page that states bundling (`docs/docs/skills/migrate.mdx` does), and the
`CLAUDE_MD` roster in `src/constants.ts` in agreement. **Never edit the bundled copy** — change `skills/` at the
repo root; the build re-syncs.
- `pnpm build` and `prepack` run `scripts/sync-skills.mjs`, which copies **every directory
under the repo-root `skills/` that holds a `SKILL.md`** into the package. The roster is read,
not listed (#540): there is no allowlist, so a new skill bundles without anyone remembering
to add it, and a deleted one stops bundling. That replaced a hardcoded `SKILLS` array which
errored on a listed-but-missing skill and said nothing about the reverse. Every skill now
bundles; #385 settled only `bestax-migrate`'s case and kept the per-skill rule, so dropping
that rule is #540's decision, taken on #385's reasoning. What is _not_ derivable is the
prose rosters, this package's `CLAUDE_MD` roster in `src/constants.ts` among them. The
`skills-roster` conformance check holds every one of them to the directory in both directions;
`SKILL_ROSTERS` in `scripts/check-conformance.mjs` is the authoritative list, and its failure
names each file you missed. **Never edit the bundled copy** — change `skills/` at the repo
root; the build re-syncs.
- The `CLAUDE_MD` template in `constants.ts` is what every generated app tells its AI agents.
When library conventions, skills, or the canonical docs entrypoint change (#203), check
whether this template must change too.
Expand Down
71 changes: 53 additions & 18 deletions create-bestax/scripts/sync-skills.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@
//
// `/skills` is the single source of truth; `templates/skills` is generated and
// gitignored. Runs on `build` and `prepack`.
//
// The roster is READ, not listed (#540). This file used to carry a hardcoded
// `SKILLS` array. It errored when a LISTED skill was missing from disk, but
// never when a skill on disk was missing from the list, so a new skill
// directory that nobody added to it silently never bundled and no gate
// noticed. Every skill directory now bundles by construction.
//
// State the provenance precisely, because it is easy to overstate: #385
// settled ONE skill's case (bestax-migrate bundles) and explicitly kept the
// per-skill decision rule — "The per-skill-decision rule stays". What it did
// give is the reasoning this generalises, "a per-skill carve-out is exactly
// the kind of thing that drifts, and this issue is the proof". Removing the
// rule outright is a NEW decision made in #540, not something #385 already
// concluded. If a skill ever must not bundle, add an explicit opt-out here,
// so the omission is a decision in this file rather than an absence nobody
// sees.
//
// This was the last of the three roster consumers to be listed rather than
// read; `bestax-mcp/scripts/sync-skills.mjs` and `scripts/gen-mcp-index.mjs`
// already read the directory. The copies that CANNOT be derived are prose
// (README, docs, the scaffolded CLAUDE.md), and those are held by
// `pnpm check:conformance --only=skills-roster`.
//
// Two deliberate differences from bestax-mcp's copy survive, and its header
// lists the same two: fs-extra stays (this package already depends on it, so
// avoiding it buys nothing), and this is still not concurrency-safe (the only
// callers are `build` and `prepack`, which never overlap — port the locking if
// a third is ever added).
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'fs-extra';
Expand All @@ -13,30 +41,37 @@ const pkgRoot = path.resolve(here, '..');
const skillsSrc = path.resolve(pkgRoot, '..', 'skills');
const skillsDest = path.join(pkgRoot, 'templates', 'skills');

const SKILLS = [
'bestax-custom-component',
'bestax-form',
'bestax-theming',
'bestax-layout-scaffold',
'bestax-icons',
'bestax-optimize',
'bestax-migrate',
];

if (!fs.existsSync(skillsSrc)) {
console.error(`[sync-skills] source not found: ${skillsSrc}`);
process.exit(1);
}

// A directory with a SKILL.md is a skill. That predicate is what excludes
// `skills/README.md` and `skills/CLAUDE.md` without naming them, so a new
// non-skill file at that level does not become a phantom skill. Sorted so the
// copy order — and the count in the success line — is deterministic.
const skills = (await fs.readdir(skillsSrc, { withFileTypes: true }))
.filter(
entry =>
entry.isDirectory() &&
fs.existsSync(path.join(skillsSrc, entry.name, 'SKILL.md'))
)
.map(entry => entry.name)
.sort();

// Checked BEFORE emptying the destination, which the hardcoded version did not
// have to think about. Reading a roster off disk means a wrong path or a
// renamed directory now yields zero skills instead of an error, and emptying
// first would ship an empty bundle rather than failing.
if (!skills.length) {
console.error(`[sync-skills] no skills found in ${skillsSrc}`);
process.exit(1);
}

await fs.emptyDir(skillsDest);

for (const name of SKILLS) {
const from = path.join(skillsSrc, name);
if (!fs.existsSync(from)) {
console.error(`[sync-skills] missing skill: ${from}`);
process.exit(1);
}
await fs.copy(from, path.join(skillsDest, name));
for (const name of skills) {
await fs.copy(path.join(skillsSrc, name), path.join(skillsDest, name));
}

console.log(`[sync-skills] copied ${SKILLS.length} skills -> templates/skills`);
console.log(`[sync-skills] copied ${skills.length} skills -> templates/skills`);
Loading
Loading