@spardutti/claude-skills 2.33.8 → 2.35.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 +9 -3
- package/bin/cli.mjs +9 -35
- package/hooks/ship-gate-structure.sh +102 -0
- package/hooks/ship-gate.sh +4 -6
- package/lib/setup-hook.mjs +2 -0
- package/lib/sync.mjs +81 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
|
|
|
131
131
|
|
|
132
132
|
| Command | What it does |
|
|
133
133
|
|---------|--------------|
|
|
134
|
-
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths, audits it against the skills installed in the project, and mutation-tests the changed lines to prove the tests would catch a break — **fixing what it finds** rather than handing you a list. It runs before the **PR**, not before every commit: its scope is the whole branch, so gating each commit re-mutated every file the branch had ever touched. `--force` skips it. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
|
|
134
|
+
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths and that new files sit in their kind folder, audits it against the skills installed in the project, and mutation-tests the changed lines to prove the tests would catch a break — **fixing what it finds** rather than handing you a list. It runs before the **PR**, not before every commit: its scope is the whole branch, so gating each commit re-mutated every file the branch had ever touched. `--force` skips it. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
|
|
135
135
|
| `/discover` | Find the right problem before deciding what to build — diverges first: generates competing framings through blind subagents under forced constraints, stress-tests the winner with a blind critic, and refuses to converge until every open question is answered or deferred, then drafts scope, non-goals, edge cases and success criteria for you to correct. Run before `/plan-feature` |
|
|
136
136
|
| `/plan-feature` | Integration-first feature planning, then building — 3 parallel subagents scan for reusable code, patterns and touch points, grounded clarifying questions follow, and on your go the same agent builds the plan |
|
|
137
137
|
| `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
|
|
@@ -169,7 +169,13 @@ Every install writes a manifest at `.claude/.claude-skills.json` recording what
|
|
|
169
169
|
npx @spardutti/claude-skills --sync
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
`--sync` refreshes every tracked item to the latest catalog and prunes stale ones in one shot — no menu. For a project that predates the manifest, the first normal run offers a one-time cleanup of `.claude/` content no longer in the catalog.
|
|
172
|
+
`--sync` refreshes every tracked item, and any hooks the project already has, to the latest catalog and prunes stale ones in one shot — no menu. For a project that predates the manifest, the first normal run offers a one-time cleanup of `.claude/` content no longer in the catalog.
|
|
173
|
+
|
|
174
|
+
`--sync-all[=dir]` does the same for every project under `dir` (default: your home folder) that has a manifest, fetching the catalog once:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx @spardutti/claude-skills --sync-all
|
|
178
|
+
```
|
|
173
179
|
|
|
174
180
|
`--local[=path]` reads the catalog from a working copy instead of GitHub, so an unreleased change can be installed and tried without publishing it first:
|
|
175
181
|
|
|
@@ -185,7 +191,7 @@ commands/ Slash commands installed to .claude/commands/
|
|
|
185
191
|
agents/ Subagent definitions — commands declare which they need via requires-agents
|
|
186
192
|
scripts/ validate-skills.mjs — checks skill length caps and reference integrity
|
|
187
193
|
gauntlet.sh — the Stop-hook verification gates, embedded by the CLI
|
|
188
|
-
ship-gate.sh — /ship's file-length and mutation checks, behind an exit code
|
|
194
|
+
ship-gate.sh — /ship's file-length, folder-structure and mutation checks, behind an exit code
|
|
189
195
|
ship-gate-hook.sh — refuses gh pr create/merge without a ship-gate receipt
|
|
190
196
|
version-check.sh — SessionStart nudge when a newer catalog is published
|
|
191
197
|
gauntlet-selftest.sh — behavioural tests for the hooks (runs on pre-push)
|
package/bin/cli.mjs
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { confirm } from "@inquirer/prompts";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { readFileSync
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
|
|
9
10
|
import { promptSkillSelection, promptCommandSelection, promptRemoval } from "../lib/prompt.mjs";
|
|
10
11
|
import { installSkills, installCommands, installRequiredAgents } from "../lib/install.mjs";
|
|
11
12
|
import { makeLocalSource } from "../lib/local.mjs";
|
|
12
13
|
import { runPostInstall } from "../lib/post-install.mjs";
|
|
13
|
-
import {
|
|
14
|
+
import { runSync, runSyncAll } from "../lib/sync.mjs";
|
|
14
15
|
import {
|
|
15
16
|
readManifest, writeManifest, computeOrphans, computeRemovals, scanInstalled, removeArtifacts,
|
|
16
17
|
MANIFEST_FILE,
|
|
@@ -20,38 +21,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
20
21
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
21
22
|
const CWD = process.cwd();
|
|
22
23
|
|
|
23
|
-
// `--sync`: re-install everything the manifest records (refreshed to the latest
|
|
24
|
-
// catalog), refresh hooks the project already has, and prune anything removed upstream.
|
|
25
|
-
async function runSync(manifest, catalog) {
|
|
26
|
-
if (!manifest) {
|
|
27
|
-
console.log(` ${chalk.yellow("Nothing to sync")} — no manifest in this project. Run without --sync first.\n`);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const orphans = computeOrphans(manifest, catalog);
|
|
31
|
-
const orphanCount = orphans.skills.length + orphans.commands.length + orphans.agents.length;
|
|
32
|
-
if (orphanCount > 0) {
|
|
33
|
-
await removeArtifacts(CWD, orphans);
|
|
34
|
-
console.log(` ${chalk.green("✔")} Pruned ${orphanCount} item(s) removed from the catalog.`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const skills = catalog.skills.filter((s) => manifest.skills.includes(s.dirName));
|
|
38
|
-
const commands = catalog.commands.filter((c) => manifest.commands.includes(c.fileName));
|
|
39
|
-
if (skills.length > 0) { console.log(); await installSkills(skills); }
|
|
40
|
-
if (commands.length > 0) { console.log(); await installCommands(commands); }
|
|
41
|
-
const { installed } = await installRequiredAgents(commands, catalog.agents, CWD);
|
|
42
|
-
if (existsSync(join(CWD, ".claude", "hooks", "skill-gate.sh"))) { console.log(); await setupHook(CWD); }
|
|
43
|
-
|
|
44
|
-
await writeManifest(CWD, {
|
|
45
|
-
catalogVersion: pkg.version,
|
|
46
|
-
skills: skills.map((s) => s.dirName),
|
|
47
|
-
commands: commands.map((c) => c.fileName),
|
|
48
|
-
agents: installed.map((a) => a.fileName),
|
|
49
|
-
});
|
|
50
|
-
console.log(`\n ${chalk.green("✔")} ${chalk.bold(`Synced to catalog v${pkg.version}.`)}\n`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
24
|
async function main() {
|
|
54
25
|
const isSync = process.argv.includes("--sync");
|
|
26
|
+
// --sync-all[=dir] syncs every project under dir (default: home) from one catalog fetch.
|
|
27
|
+
const syncAllArg = process.argv.find((a) => a === "--sync-all" || a.startsWith("--sync-all="));
|
|
28
|
+
const syncAllRoot = syncAllArg ? resolve(syncAllArg.split("=")[1] || homedir()) : null;
|
|
55
29
|
|
|
56
30
|
// --local[=path] reads the catalog from a working copy instead of GitHub, so an
|
|
57
31
|
// unreleased change can be installed and tried without publishing it first.
|
|
@@ -71,10 +45,10 @@ async function main() {
|
|
|
71
45
|
fetchers.fetchSkills(), fetchers.fetchCommands(), fetchers.fetchAgents(),
|
|
72
46
|
]);
|
|
73
47
|
const catalog = { skills, commands, agents };
|
|
48
|
+
if (syncAllRoot) return runSyncAll(syncAllRoot, catalog, pkg.version);
|
|
49
|
+
if (isSync) return runSync(CWD, catalog, pkg.version);
|
|
74
50
|
const manifest = await readManifest(CWD);
|
|
75
51
|
|
|
76
|
-
if (isSync) return runSync(manifest, catalog);
|
|
77
|
-
|
|
78
52
|
// --- Prune items renamed or removed from the catalog upstream ---
|
|
79
53
|
const orphans = computeOrphans(manifest, catalog);
|
|
80
54
|
const orphanNames = [...orphans.skills, ...orphans.commands, ...orphans.agents];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced by ship-gate.sh: a new file must sit in the folder named for its kind.
|
|
3
|
+
# Only ADDED files are judged, so a repo laid out the old way cannot get worse and need not move.
|
|
4
|
+
|
|
5
|
+
STRUCTURE_JS_KINDS="components|hooks|api|queries|schemas|types|utils|stores|test"
|
|
6
|
+
STRUCTURE_KIND_FILE='(^|[._])(router|routes|controller|service|schema|model)s?\.[cm]?[jt]s$|(Router|Routes|Controller|Service|Schema|Model)s?\.[cm]?[jt]s$'
|
|
7
|
+
|
|
8
|
+
# --no-renames: a move must show up as an add, or renaming a misplaced file would slip through.
|
|
9
|
+
added_files() {
|
|
10
|
+
{ git diff --no-renames --diff-filter=A --name-only "$BASE"...HEAD 2>/dev/null
|
|
11
|
+
git diff --no-renames --diff-filter=A --name-only HEAD 2>/dev/null
|
|
12
|
+
git ls-files --others --exclude-standard 2>/dev/null; } | sort -u
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
has_subject() { # has_subject <test path>: a file the test is named for, in the same folder
|
|
16
|
+
d=$(dirname "$1")
|
|
17
|
+
stem=$(basename "$1" | sed -E 's/\.(test|spec)\.[^.]+$//')
|
|
18
|
+
while :; do
|
|
19
|
+
for e in ts tsx js jsx mjs cjs astro; do [ -f "$d/$stem.$e" ] && return 0; done
|
|
20
|
+
case "$stem" in *.*) stem=${stem%.*} ;; *) return 1 ;; esac
|
|
21
|
+
done
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
feature_misplaced() { # feature_misplaced <path below features/<name>/>
|
|
25
|
+
case "$1" in
|
|
26
|
+
index.*) ;;
|
|
27
|
+
*/*) printf '%s' "${1%%/*}" | grep -qxE "$STRUCTURE_JS_KINDS" \
|
|
28
|
+
|| echo "${1%%/*}/ is not a kind folder ($STRUCTURE_JS_KINDS)" ;;
|
|
29
|
+
*) echo "loose at the feature root, where only index.* belongs" ;;
|
|
30
|
+
esac
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
js_misplaced() { # js_misplaced <path> <owner>
|
|
34
|
+
f=$1; b=${f##*/}; parent=$(basename "$(dirname "$f")")
|
|
35
|
+
if printf '%s' "$b" | grep -qE '\.(test|spec)\.[^.]+$'; then
|
|
36
|
+
printf '%s' "/$f" | grep -qE '/(e2e|tests?)/' && return
|
|
37
|
+
has_subject "$f" || echo "a test sits beside the file it tests, and nothing here is named ${b%%.*}"
|
|
38
|
+
return
|
|
39
|
+
fi
|
|
40
|
+
rest=$(printf '%s' "/$f" | sed -nE 's#.*/features/[^/]+/##p')
|
|
41
|
+
if [ -n "$rest" ]; then feature_misplaced "$rest"; return; fi
|
|
42
|
+
kinds="components|hooks|queries|api|schemas|types|utils"
|
|
43
|
+
grep -qs '"express"' "$2/package.json" && kinds="$kinds|routes|controllers|services|middleware|models"
|
|
44
|
+
if printf '%s' "/$f" | grep -qE "/src/($kinds)/"; then
|
|
45
|
+
echo "sorted by kind first; it belongs in <domain>/<kind>/ or shared/<kind>/"
|
|
46
|
+
elif printf '%s' "$b" | grep -qE '^use[A-Z].*\.[jt]sx?$' && ! printf '%s' "/$f" | grep -qE '/(hooks|queries)/'; then
|
|
47
|
+
echo "a hook belongs in hooks/ or queries/"
|
|
48
|
+
elif printf '%s' "$b" | grep -qE "$STRUCTURE_KIND_FILE" \
|
|
49
|
+
&& ! printf '%s' "$parent" | grep -qxE 'routes|controllers|services|schemas|models|api|queries|types'; then
|
|
50
|
+
echo "the kind is in the filename; it belongs in a kind folder"
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
py_misplaced() { # py_misplaced <path>
|
|
55
|
+
f=$1; b=${f##*/}; d=$(dirname "$f"); parent=${d##*/}
|
|
56
|
+
case "$b" in
|
|
57
|
+
test_*.py|*_test.py|conftest.py)
|
|
58
|
+
printf '%s' "/$f" | grep -qE '/tests/' || echo "tests live under tests/, mirroring the app"
|
|
59
|
+
return ;;
|
|
60
|
+
esac
|
|
61
|
+
k=$(printf '%s' "$b" | sed -nE 's/^(.*_)?(router|service|schema|model)s?\.py$/\2/p')
|
|
62
|
+
if [ -n "$k" ] && [ "$parent" != "${k}s" ]; then
|
|
63
|
+
echo "the kind is in the filename; it belongs in ${k}s/"
|
|
64
|
+
elif printf '%s' "$parent" | grep -qxE 'routers|services|schemas|models' && [ -f "$(dirname "$d")/main.py" ]; then
|
|
65
|
+
echo "sorted by kind first; it belongs in <domain>/$parent/"
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
structure_check() {
|
|
70
|
+
bad=""; count=0
|
|
71
|
+
while IFS= read -r f; do
|
|
72
|
+
[ -f "$f" ] && ! is_ignored "$f" || continue
|
|
73
|
+
printf '%s' "/$f" | grep -qE '/(node_modules|\.stryker-tmp|mutants)/' && continue
|
|
74
|
+
o=$(owner_of "$f")
|
|
75
|
+
case "$f" in
|
|
76
|
+
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.astro) why=$(js_misplaced "$f" "$o") ;;
|
|
77
|
+
*.py) grep -qis fastapi "$o/pyproject.toml" || continue; why=$(py_misplaced "$f") ;;
|
|
78
|
+
*) continue ;;
|
|
79
|
+
esac
|
|
80
|
+
count=$((count+1))
|
|
81
|
+
[ -n "$why" ] && bad="$bad $f — $why
|
|
82
|
+
"
|
|
83
|
+
done <<< "$(added_files)"
|
|
84
|
+
echo
|
|
85
|
+
if [ -n "$bad" ]; then
|
|
86
|
+
echo "STRUCTURE — new files outside their kind folder:"
|
|
87
|
+
printf '%s' "$bad"
|
|
88
|
+
echo " Move them (Project Structure in the react, fastapi or express skill), or ship with --force."
|
|
89
|
+
echo
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
echo "STRUCTURE — ok, $count new file(s) in their kind folder"
|
|
93
|
+
echo
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Called when nothing else in the diff is gated, so the gate would otherwise pass it.
|
|
97
|
+
structure_fail() {
|
|
98
|
+
echo "ship-gate: FAIL — deal with the findings above, then run this again."
|
|
99
|
+
echo " To ship anyway: bash .claude/hooks/ship-gate.sh --force"
|
|
100
|
+
rm -f "$RECEIPT"
|
|
101
|
+
exit 1
|
|
102
|
+
}
|
package/hooks/ship-gate.sh
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
set -uo pipefail
|
|
47
47
|
|
|
48
48
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
49
|
-
. "$HERE/ship-gate-projects.sh" || { echo "ship-gate: cannot read $HERE
|
|
49
|
+
. "$HERE/ship-gate-projects.sh" && . "$HERE/ship-gate-structure.sh" || { echo "ship-gate: cannot read its helpers in $HERE"; exit 1; }
|
|
50
50
|
|
|
51
51
|
MODE=""
|
|
52
52
|
case "${1:-}" in
|
|
@@ -76,7 +76,7 @@ GAUNTLET_MAX_LINES=200
|
|
|
76
76
|
GAUNTLET_MUTATE=""
|
|
77
77
|
GAUNTLET_SOURCE_EXT="ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|gd"
|
|
78
78
|
GAUNTLET_IGNORE_EXT="md|mdx|txt|rst|adoc|jsonc?|ya?ml|toml|lock|cfg|ini|env|csv|tsv|sql|html|css|scss|svg|png|jpg|jpeg|gif|webp|ico|pdf|woff2?|ttf|otf|mp3|mp4|wav|zip|gz|tres|tscn|import|godot"
|
|
79
|
-
GAUNTLET_IGNORE_FILES="*.gen.ts *.gen.tsx *.generated.* */migrations/*.py */alembic/versions/*.py */components/ui/*.tsx */*.config.*"
|
|
79
|
+
GAUNTLET_IGNORE_FILES="*.gen.ts *.gen.tsx *.generated.* */migrations/*.py */alembic/versions/*.py */components/ui/*.tsx */hooks/use-mobile.ts */*.config.*"
|
|
80
80
|
# Length-checked and skill-audited like everything else, but not mutated.
|
|
81
81
|
# Mutation pays on logic and burns time on presentation: a component's mutants
|
|
82
82
|
# are class names, copy and JSX shape, none of which is behaviour, and one
|
|
@@ -159,6 +159,8 @@ if [ -n "$IGNORED" ]; then
|
|
|
159
159
|
printf '%s\n' "$IGNORED" | sed 's/^/ /'
|
|
160
160
|
fi
|
|
161
161
|
|
|
162
|
+
[ -n "$FILES" ] && echo "ship-gate: $(printf '%s\n' "$FILES" | wc -l) changed code file(s), base $(git rev-parse --short "$BASE")"
|
|
163
|
+
STATUS=0; structure_check || { STATUS=1; [ -z "$FILES" ] && structure_fail; }
|
|
162
164
|
if [ -z "$FILES" ]; then
|
|
163
165
|
# "I recognised nothing" is not "there is nothing", and the gate used to report
|
|
164
166
|
# both as a PASS. A Godot repo changed only .gd files, which no extension in
|
|
@@ -183,10 +185,6 @@ if [ -z "$FILES" ]; then
|
|
|
183
185
|
exit 0
|
|
184
186
|
fi
|
|
185
187
|
|
|
186
|
-
echo "ship-gate: $(printf '%s\n' "$FILES" | wc -l) changed code file(s), base $(git rev-parse --short "$BASE")"
|
|
187
|
-
echo
|
|
188
|
-
STATUS=0
|
|
189
|
-
|
|
190
188
|
# ------------------------------------------------- check 1: file length (hard)
|
|
191
189
|
OVER=""
|
|
192
190
|
LARGEST=0
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -27,6 +27,7 @@ const APPLICATION_GATE_FILENAME = "skill-application-gate.sh";
|
|
|
27
27
|
const GAUNTLET_FILENAME = "gauntlet.sh";
|
|
28
28
|
const SHIP_GATE_FILENAME = "ship-gate.sh";
|
|
29
29
|
const SHIP_GATE_PROJECTS_FILENAME = "ship-gate-projects.sh";
|
|
30
|
+
const SHIP_GATE_STRUCTURE_FILENAME = "ship-gate-structure.sh";
|
|
30
31
|
const SHIP_GATE_HOOK_FILENAME = "ship-gate-hook.sh";
|
|
31
32
|
const VERSION_CHECK_FILENAME = "version-check.sh";
|
|
32
33
|
const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
|
|
@@ -106,6 +107,7 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
106
107
|
GAUNTLET_FILENAME,
|
|
107
108
|
SHIP_GATE_FILENAME,
|
|
108
109
|
SHIP_GATE_PROJECTS_FILENAME,
|
|
110
|
+
SHIP_GATE_STRUCTURE_FILENAME,
|
|
109
111
|
SHIP_GATE_HOOK_FILENAME,
|
|
110
112
|
VERSION_CHECK_FILENAME,
|
|
111
113
|
]) {
|
package/lib/sync.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import { join, basename } from "node:path";
|
|
5
|
+
import { installSkills, installCommands, installRequiredAgents } from "./install.mjs";
|
|
6
|
+
import { setupHook } from "./setup-hook.mjs";
|
|
7
|
+
import { readManifest, writeManifest, computeOrphans, removeArtifacts, MANIFEST_FILE } from "./manifest.mjs";
|
|
8
|
+
|
|
9
|
+
const SEARCH_DEPTH = 5;
|
|
10
|
+
|
|
11
|
+
// Re-install everything the manifest records (refreshed to the latest catalog),
|
|
12
|
+
// refresh hooks the project already has, and prune anything removed upstream.
|
|
13
|
+
export async function runSync(dir, catalog, version) {
|
|
14
|
+
const manifest = await readManifest(dir);
|
|
15
|
+
if (!manifest) {
|
|
16
|
+
console.log(` ${chalk.yellow("Nothing to sync")} — no manifest in this project. Run without --sync first.\n`);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const orphans = computeOrphans(manifest, catalog);
|
|
20
|
+
const orphanCount = orphans.skills.length + orphans.commands.length + orphans.agents.length;
|
|
21
|
+
if (orphanCount > 0) {
|
|
22
|
+
await removeArtifacts(dir, orphans);
|
|
23
|
+
console.log(` ${chalk.green("✔")} Pruned ${orphanCount} item(s) removed from the catalog.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const skills = catalog.skills.filter((s) => manifest.skills.includes(s.dirName));
|
|
27
|
+
const commands = catalog.commands.filter((c) => manifest.commands.includes(c.fileName));
|
|
28
|
+
if (skills.length > 0) { console.log(); await installSkills(skills, dir); }
|
|
29
|
+
if (commands.length > 0) { console.log(); await installCommands(commands, dir); }
|
|
30
|
+
const { installed } = await installRequiredAgents(commands, catalog.agents, dir);
|
|
31
|
+
if (existsSync(join(dir, ".claude", "hooks", "skill-gate.sh"))) { console.log(); await setupHook(dir); }
|
|
32
|
+
|
|
33
|
+
await writeManifest(dir, {
|
|
34
|
+
catalogVersion: version,
|
|
35
|
+
skills: skills.map((s) => s.dirName),
|
|
36
|
+
commands: commands.map((c) => c.fileName),
|
|
37
|
+
agents: installed.map((a) => a.fileName),
|
|
38
|
+
});
|
|
39
|
+
console.log(`\n ${chalk.green("✔")} ${chalk.bold(`Synced to catalog v${version}.`)}\n`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function findProjects(root, depth = SEARCH_DEPTH) {
|
|
44
|
+
if (existsSync(join(root, ".claude", MANIFEST_FILE))) return [root];
|
|
45
|
+
if (depth === 0) return [];
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const found = [];
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
55
|
+
found.push(...(await findProjects(join(root, e.name), depth - 1)));
|
|
56
|
+
}
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runSyncAll(root, catalog, version) {
|
|
61
|
+
const projects = await findProjects(root);
|
|
62
|
+
if (projects.length === 0) {
|
|
63
|
+
console.log(` ${chalk.yellow("No projects found")} under ${root} with .claude/${MANIFEST_FILE}.\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const failed = [];
|
|
67
|
+
for (const dir of projects) {
|
|
68
|
+
console.log(chalk.bold.cyan(`── ${dir}`));
|
|
69
|
+
try {
|
|
70
|
+
await runSync(dir, catalog, version);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
failed.push(basename(dir));
|
|
73
|
+
console.log(` ${chalk.red("Error:")} ${err.message}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const ok = projects.length - failed.length;
|
|
77
|
+
console.log(` ${chalk.green("✔")} ${chalk.bold(`${ok} of ${projects.length} project(s) synced to v${version}.`)}`);
|
|
78
|
+
if (failed.length > 0) console.log(` ${chalk.red("✘")} Failed: ${failed.join(", ")}`);
|
|
79
|
+
console.log();
|
|
80
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spardutti/claude-skills",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
4
4
|
"description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"claude-skills": "bin/cli.mjs"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"prepack": "cp ../README.md README.md && mkdir -p hooks && cp ../scripts/skill-gate.sh ../scripts/skill-gate-automark.sh ../scripts/skill-application-gate.sh ../scripts/gauntlet.sh ../scripts/ship-gate.sh ../scripts/ship-gate-projects.sh ../scripts/ship-gate-hook.sh ../scripts/version-check.sh hooks/",
|
|
14
|
+
"prepack": "cp ../README.md README.md && mkdir -p hooks && cp ../scripts/skill-gate.sh ../scripts/skill-gate-automark.sh ../scripts/skill-application-gate.sh ../scripts/gauntlet.sh ../scripts/ship-gate.sh ../scripts/ship-gate-projects.sh ../scripts/ship-gate-structure.sh ../scripts/ship-gate-hook.sh ../scripts/version-check.sh hooks/",
|
|
15
15
|
"postpack": "git checkout -- README.md && rm -rf hooks"
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|