feat(doctor): report the installed plugin generation, not just its presence
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:
@@ -133,5 +133,63 @@ mkdir -p "$f/home/.claude/skills"
|
||||
ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished"
|
||||
assert_fails "$f" 'dangling' 'dangling link inside a harness dir is reported'
|
||||
|
||||
# --- 8. Claude Code plugin pinned to an older version than is available -----
|
||||
# The plugin is the ONLY delivery channel for Claude Code, and Claude Code
|
||||
# gates updates on plugin.json's version field. A host that never picked the
|
||||
# new one up loads every skill successfully — just the wrong generation. Found
|
||||
# live on koala 2026-08-29: installed 0.6.0, marketplace 0.7.0, repo 0.8.5,
|
||||
# and doctor said "clean" (#30).
|
||||
|
||||
# installed_state <fixture> <version> — the Claude Code install record.
|
||||
installed_state() {
|
||||
mkdir -p "$1/home/.claude/plugins"
|
||||
cat > "$1/home/.claude/plugins/installed_plugins.json" <<JSON
|
||||
{
|
||||
"version": 2,
|
||||
"plugins": {
|
||||
"skills@mathias": [
|
||||
{
|
||||
"scope": "user",
|
||||
"installPath": "/somewhere/$2",
|
||||
"version": "$2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
# manifest <dir> <version> — a .claude-plugin/plugin.json under <dir>.
|
||||
manifest() {
|
||||
mkdir -p "$1/.claude-plugin"
|
||||
printf '{\n "name": "skills",\n "version": "%s"\n}\n' "$2" > "$1/.claude-plugin/plugin.json"
|
||||
}
|
||||
|
||||
f=$(new_fixture pluginmatch); wire_clean "$f"
|
||||
installed_state "$f" 0.8.0
|
||||
manifest "$f/ck" 0.8.0
|
||||
assert_clean "$f" 'plugin on the current version stays silent'
|
||||
|
||||
f=$(new_fixture pluginstale); wire_clean "$f"
|
||||
installed_state "$f" 0.7.0
|
||||
manifest "$f/ck" 0.8.0
|
||||
assert_fails "$f" '0.7.0' 'stale plugin version is reported'
|
||||
assert_fails "$f" '0.8.0' 'stale plugin report names the available version'
|
||||
|
||||
# A host that only uses the symlink path has no install record at all. That is
|
||||
# a valid configuration and must not fail.
|
||||
f=$(new_fixture pluginabsent); wire_clean "$f"
|
||||
manifest "$f/ck" 0.8.0
|
||||
assert_clean "$f" 'no Claude Code install record is not a fault'
|
||||
|
||||
# The marketplace clone is what Claude Code compares against, so a newer
|
||||
# version there means an update is waiting even when the local checkout agrees
|
||||
# with what is installed.
|
||||
f=$(new_fixture pluginmarket); wire_clean "$f"
|
||||
installed_state "$f" 0.7.0
|
||||
manifest "$f/ck" 0.7.0
|
||||
manifest "$f/home/.claude/plugins/marketplaces/mathias" 0.9.0
|
||||
assert_fails "$f" '0.9.0' 'a newer marketplace clone is reported'
|
||||
|
||||
printf '\ndoctor: %d passed, %d failed\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
|
||||
Reference in New Issue
Block a user