feat(doctor): report the installed plugin generation, not just its presence
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 0s

Every other check in doctor is about symlink paths. That is the wrong shape
for Claude Code, which has no symlinks — it has the plugin, and it gates
updates on plugin.json's version. A host that never picked up a release loads
every skill successfully and is simply a generation behind, silently.

Found live while writing this: koala had skills@mathias 0.6.0 installed, a
marketplace clone at 0.7.0 and a checkout at 0.8.4, and doctor said "clean".

doctor now compares the version recorded in installed_plugins.json against the
newest manifest already on the host — the active checkout and any marketplace
clone, filtered to plugin.json files that actually name this plugin — and fails
naming both versions. Offline, like the rest of doctor: "available" never means
a fetch.

A host with no install record is a valid symlink-only configuration and stays
clean. --json grows a "plugin":{"installed","available"} object so per-host
results can be collected without parsing prose.

Four new fixture cases: match, stale, absent record, newer marketplace clone.
Live output on koala:

  FAIL plugin-version: Claude Code has skills@mathias 0.6.0 installed but
  0.8.4 is available on this host

Closes #30
Bump-Type: minor
This commit is contained in:
2026-08-29 10:00:11 +02:00
parent ad17a7c6c0
commit a2afb35d1d
2 changed files with 113 additions and 2 deletions
+55 -2
View File
@@ -180,10 +180,63 @@ if [ -f "$INSTALLED" ]; then
|| add WARN plugin "skills@mathias is not installed for Claude Code — /plugin marketplace add https://git.d-ma.be/mathias/skills.git"
fi
# --- Claude Code: which GENERATION of the plugin is installed ---------------
# Every check above is about symlink paths, which is the wrong shape for the
# plugin: it is the only channel Claude Code has, and Claude Code gates updates
# on plugin.json's version. A host that never picked up a release loads all the
# skills successfully and is simply a generation behind — silently. Found on
# koala 2026-08-29: installed 0.6.0, marketplace clone 0.7.0, repo 0.8.5,
# doctor said "clean" (#30).
#
# Offline by construction, like the rest of doctor: "available" means the
# newest manifest already on this host — the active checkout and any
# marketplace clone — never a fetch.
manifest_version() {
[ -f "$1" ] || return 1
grep -q '"name"[[:space:]]*:[[:space:]]*"skills"' "$1" || return 1
sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -1
}
# newer_version a b — echoes whichever sorts higher on major.minor.patch.
newer_version() {
printf '%s\n%s\n' "$1" "$2" | awk -F. '
{ raw[NR] = $0; for (i = 1; i <= 3; i++) n[NR, i] = ($i == "" ? 0 : $i + 0) }
END {
for (i = 1; i <= 3; i++) {
if (n[1, i] > n[2, i]) { print raw[1]; exit }
if (n[1, i] < n[2, i]) { print raw[2]; exit }
}
print raw[1]
}'
}
installed_v=''
if [ -f "$INSTALLED" ]; then
installed_v=$(sed -n '/"skills@mathias"/,/\]/p' "$INSTALLED" \
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
fi
available_v=''
for manifest in "$CHECKOUT/.claude-plugin/plugin.json" \
"$HOME"/.claude/plugins/marketplaces/*/.claude-plugin/plugin.json
do
v=$(manifest_version "$manifest") || continue
[ -n "$v" ] || continue
if [ -z "$available_v" ]; then
available_v=$v
else
available_v=$(newer_version "$available_v" "$v")
fi
done
if [ -n "$installed_v" ] && [ -n "$available_v" ] && [ "$installed_v" != "$available_v" ]; then
add FAIL plugin-version "Claude Code has skills@mathias $installed_v installed but $available_v is available on this host — /plugin update skills@mathias (a host stale enough to matter is running the old doctor, so the first detection may need this run by hand)"
fi
# --- output -----------------------------------------------------------------
if [ "$JSON" -eq 1 ]; then
printf '{"checkout":"%s","expected":%s,"findings":[%s]}\n' \
"$CHECKOUT" "$EXPECTED" "${report%,}"
printf '{"checkout":"%s","expected":%s,"plugin":{"installed":"%s","available":"%s"},"findings":[%s]}\n' \
"$CHECKOUT" "$EXPECTED" "$installed_v" "$available_v" "${report%,}"
elif [ "$findings" -gt 0 ]; then
printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \
"$findings" "$CHECKOUT" "$report"