@spardutti/claude-skills 2.13.0 → 2.14.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/README.md CHANGED
@@ -51,7 +51,7 @@ The CLI will:
51
51
 
52
52
  ## Skill Catalog
53
53
 
54
- **13 skills**, grouped the same way the installer presents them.
54
+ **14 skills**, grouped the same way the installer presents them.
55
55
 
56
56
  > [!NOTE]
57
57
  > Skills marked **📦 Bundle** ship a concise always-loaded entry point plus reference files Claude reads only when a task needs them — comprehensive coverage at a low context cost.
@@ -69,6 +69,7 @@ The CLI will:
69
69
  | Skill | What it covers |
70
70
  |-------|----------------|
71
71
  | `fastapi` 📦 | FastAPI — async correctness, `Annotated` dependency injection, `lifespan`, response models, testing with dependency overrides; bundle covers Pydantic, Alembic, Celery, and list endpoints (pagination/filtering/search/sorting) |
72
+ | `express` 📦 | Express 5 — automatic async error forwarding, the four-argument error handler, named wildcards, `req.query` as a getter, validation at the boundary, thin routes, the helmet/CORS/rate-limit baseline; bundle covers tRPC v11 on Express |
72
73
  | `drf-best-practices` | Django REST Framework — thin serializers, service layer, queryset optimization, object-level permissions |
73
74
  | `drizzle-orm` | Drizzle ORM — schema design, identity columns, relations, migration safety, type inference |
74
75
  | `docker-best-practices` | Multi-stage builds, layer caching, security hardening, Compose Watch, health checks |
@@ -346,6 +347,7 @@ scripts/ validate-skills.mjs — checks skill length caps and reference in
346
347
  ship-gate-hook.sh — refuses git commit/push without a ship-gate receipt
347
348
  gauntlet-selftest.sh — 15 behavioural tests for the hook (runs on pre-push)
348
349
  gauntlet-survey.sh — what the hook would do in every repo under a directory
350
+ preflight.sh — everything that must pass before a release
349
351
  cli/ The npm installer (npx @spardutti/claude-skills); version in cli/package.json
350
352
  .husky/ pre-push hook running the skill validator
351
353
  package.json Private dev-tooling package (claude-skills-dev) — not the published one
@@ -359,6 +361,9 @@ Skills live in `skills/<name>/SKILL.md`. Authoring conventions are in [CLAUDE.md
359
361
  - `SKILL.md` ≤ 350 lines; reference files ≤ 500 and need a `## Contents` TOC past 100 lines.
360
362
  - References are one level deep — `SKILL.md` links them, they don't link each other.
361
363
  - `npm run validate-skills` enforces this; it also runs on `pre-push`.
364
+ - Before any release, run `bash scripts/preflight.sh` — validator, self-tests, a check that the
365
+ hook scripts embedded in the CLI still match `scripts/`, detection against every repo under
366
+ `~/projects`, and that the version is ahead of the registry. Do not publish while it is red.
362
367
  - Changing `scripts/gauntlet.sh`? Run `bash scripts/gauntlet-selftest.sh` (also on `pre-push`),
363
368
  and run `bash scripts/gauntlet-survey.sh ~/projects` before releasing. Detection is where
364
369
  this hook keeps breaking, because real repos are shaped in ways invented test repos aren't —
@@ -941,6 +941,18 @@ export async function writeGauntletConf(targetDir, testCommand) {
941
941
  return confPath;
942
942
  }
943
943
 
944
+ // Register the entry, or CORRECT the one already there. Skipping when a hook of
945
+ // this name exists leaves an existing install pinned to whatever matcher it was
946
+ // first written with — which is how the skill gates kept watching only
947
+ // Write|Edit|MultiEdit after they learned to cover Bash.
948
+ function upsert(list, filename, entry) {
949
+ const existing = list.find((e) => e.hooks?.some((h) => h.command?.endsWith(filename)));
950
+ if (!existing) return list.push(entry);
951
+ if (entry.matcher === undefined) delete existing.matcher;
952
+ else existing.matcher = entry.matcher;
953
+ existing.hooks = entry.hooks;
954
+ }
955
+
944
956
  export async function setupHook(targetDir = process.cwd()) {
945
957
  const resolved = resolve(targetDir);
946
958
  const hooksDir = join(resolved, ".claude", "hooks");
@@ -1006,10 +1018,7 @@ export async function setupHook(targetDir = process.cwd()) {
1006
1018
  };
1007
1019
 
1008
1020
  if (Array.isArray(settings.hooks.PreToolUse)) {
1009
- const exists = settings.hooks.PreToolUse.some((entry) =>
1010
- entry.hooks?.some((h) => h.command?.endsWith(GATE_FILENAME))
1011
- );
1012
- if (!exists) settings.hooks.PreToolUse.push(gateEntry);
1021
+ upsert(settings.hooks.PreToolUse, GATE_FILENAME, gateEntry);
1013
1022
  } else {
1014
1023
  settings.hooks.PreToolUse = [gateEntry];
1015
1024
  }
@@ -1020,10 +1029,7 @@ export async function setupHook(targetDir = process.cwd()) {
1020
1029
  matcher: "Write|Edit|MultiEdit|Bash",
1021
1030
  hooks: [{ type: "command", command: applicationCommand }],
1022
1031
  };
1023
- const applicationAlreadyRegistered = settings.hooks.PreToolUse.some((entry) =>
1024
- entry.hooks?.some((h) => h.command?.endsWith(APPLICATION_GATE_FILENAME))
1025
- );
1026
- if (!applicationAlreadyRegistered) settings.hooks.PreToolUse.push(applicationEntry);
1032
+ upsert(settings.hooks.PreToolUse, APPLICATION_GATE_FILENAME, applicationEntry);
1027
1033
 
1028
1034
  // Register PreToolUse ship-gate receipt check on Bash.
1029
1035
  const shipGateCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${SHIP_GATE_HOOK_FILENAME}`;
@@ -1031,10 +1037,7 @@ export async function setupHook(targetDir = process.cwd()) {
1031
1037
  matcher: "Bash",
1032
1038
  hooks: [{ type: "command", command: shipGateCommand }],
1033
1039
  };
1034
- const shipGateRegistered = settings.hooks.PreToolUse.some((entry) =>
1035
- entry.hooks?.some((h) => h.command?.endsWith(SHIP_GATE_HOOK_FILENAME))
1036
- );
1037
- if (!shipGateRegistered) settings.hooks.PreToolUse.push(shipGateEntry);
1040
+ upsert(settings.hooks.PreToolUse, SHIP_GATE_HOOK_FILENAME, shipGateEntry);
1038
1041
 
1039
1042
  // Register PostToolUse auto-mark on Skill.
1040
1043
  const autoMarkCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${AUTO_MARK_FILENAME}`;
@@ -1044,10 +1047,7 @@ export async function setupHook(targetDir = process.cwd()) {
1044
1047
  };
1045
1048
 
1046
1049
  if (Array.isArray(settings.hooks.PostToolUse)) {
1047
- const exists = settings.hooks.PostToolUse.some((entry) =>
1048
- entry.hooks?.some((h) => h.command?.endsWith(AUTO_MARK_FILENAME))
1049
- );
1050
- if (!exists) settings.hooks.PostToolUse.push(autoMarkEntry);
1050
+ upsert(settings.hooks.PostToolUse, AUTO_MARK_FILENAME, autoMarkEntry);
1051
1051
  } else {
1052
1052
  settings.hooks.PostToolUse = [autoMarkEntry];
1053
1053
  }
@@ -1057,10 +1057,7 @@ export async function setupHook(targetDir = process.cwd()) {
1057
1057
  const gauntletEntry = { hooks: [{ type: "command", command: gauntletCommand }] };
1058
1058
 
1059
1059
  if (Array.isArray(settings.hooks.Stop)) {
1060
- const exists = settings.hooks.Stop.some((entry) =>
1061
- entry.hooks?.some((h) => h.command?.endsWith(GAUNTLET_FILENAME))
1062
- );
1063
- if (!exists) settings.hooks.Stop.push(gauntletEntry);
1060
+ upsert(settings.hooks.Stop, GAUNTLET_FILENAME, gauntletEntry);
1064
1061
  } else {
1065
1062
  settings.hooks.Stop = [gauntletEntry];
1066
1063
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {