Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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