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
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ jobs:
with:
node-version: 22

# The skill validator and the plugin version check need no dependencies.
# HUSKY=0: CI does not need the git hooks. They run the same checks as the steps below, so a
# contributor who skips them (--no-verify, no npm install) is caught here.
- name: Install dependencies
run: npm ci --no-audit --no-fund
env:
HUSKY: "0"

- name: Lint files
run: npm run lint

- name: Validate skills
run: npm run validate

Expand Down
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Runs on every commit. Lints the staged files, then the package-wide rules.
# Skip once with `git commit --no-verify`; CI runs the same checks, so the skip only defers them.
npx --no-install lint-staged
npm run --silent validate
2 changes: 2 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Runs before every push: everything CI runs.
npm run --silent check
15 changes: 14 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,25 @@ Boilerplate every skill shares lives once in the protocol. Do not copy it back i
## Checks

```bash
npm run lint # syntax of .mjs, .sh, .json, .yml and skill front matter (scripts/lint-files.mjs)
npm run validate
npm test
npm test # starts with hooks:check
npm run sync:check
npm run check-plugin-version
npm run check # lint, validate, check-plugin-version and test: everything CI runs
```

## Git hooks

`npm install` runs the `prepare` script, which installs [husky](https://typicode.github.io/husky/) and points `core.hooksPath` at `.husky/_`. The hooks live in `.husky/`.

- **`pre-commit`**: `lint-staged` runs `scripts/lint-files.mjs` on the staged files, then `npm run validate` checks the package-wide rules (this includes the generated-files check, `sync:check`).
- **`pre-push`**: `npm run check`.

CI runs the same checks, so `git commit --no-verify` only defers a failure. A hook does not run when `core.hooksPath` points elsewhere, when `npm install` was never run, or when `HUSKY=0` is set. `npm test` starts with `npm run hooks:check`, which fails and says how to fix it; `npm run prepare` reinstalls.

Add a check in `scripts/lint-files.mjs` (per file) or `scripts/validate-skills.mjs` (package-wide), with a test. The hooks and CI both call those scripts, so there is nowhere else to wire it in.

## Releasing

Add a changeset (`npm run changeset`) for user-visible changes.
139 changes: 132 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
"license": "MIT",
"scripts": {
"validate": "node scripts/validate-skills.mjs",
"test": "bash tests/lib.test.sh && node --test tests/helpers.test.mjs",
"lint": "node scripts/lint-files.mjs --all",
"pretest": "node scripts/check-hooks.mjs",
"test": "bash tests/lib.test.sh && node --test tests/helpers.test.mjs tests/lint.test.mjs",
"check-plugin-version": "node scripts/sync-plugin-version.mjs --check",
"check": "npm run lint && npm run validate && npm run check-plugin-version && npm test",
"hooks:check": "node scripts/check-hooks.mjs",
"prepare": "husky || true",
"link": "bash scripts/link-skills.sh",
"changeset": "changeset",
"version": "changeset version && node scripts/sync-plugin-version.mjs",
Expand All @@ -18,6 +23,12 @@
"mattpocock-skills": "1.3.0"
},
"devDependencies": {
"@changesets/cli": "2.31.0"
"@changesets/cli": "2.31.0",
"husky": "9.1.7",
"lint-staged": "17.6.0",
"yaml": "2.9.1"
},
"lint-staged": {
"*": "node scripts/lint-files.mjs"
}
}
36 changes: 36 additions & 0 deletions scripts/check-hooks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node
// Fails when this checkout's git hooks are not the ones the package installs, so a hook that is
// silently not running shows up as an error instead of a hunch. `npm test` runs it first.
//
// node scripts/check-hooks.mjs
//
// A hook never runs when `core.hooksPath` points somewhere else, when `npm install` was skipped
// (`prepare` is what installs husky), or when HUSKY=0. The fix is `npm install` (or `npm run prepare`).
// The check is skipped where hooks do not apply: CI, no .git directory, or HUSKY=0.

import { existsSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const HOOKS = ["pre-commit", "pre-push"];

if (process.env.CI || process.env.HUSKY === "0" || !existsSync(join(root, ".git"))) {
console.log("hooks: skipped (CI, HUSKY=0 or no .git)");
process.exit(0);
}

const problems = [];
const hooksPath = spawnSync("git", ["config", "--get", "core.hooksPath"], { cwd: root, encoding: "utf8" }).stdout.trim();
if (hooksPath !== ".husky/_") problems.push(`core.hooksPath is ${hooksPath ? `"${hooksPath}"` : "not set"}, expected ".husky/_"`);
for (const h of HOOKS) {
if (!existsSync(join(root, ".husky", h))) problems.push(`.husky/${h} is missing`);
else if (!existsSync(join(root, ".husky", "_", h))) problems.push(`.husky/_/${h} is not installed`);
}

if (problems.length) {
console.error(`hooks: not installed\n - ${problems.join("\n - ")}\nRun \`npm run prepare\` (or \`npm install\`) in the package root.`);
process.exit(1);
}
console.log("hooks: installed");
Loading
Loading