fix(doctor): handle installed_plugins.json v2 format (GH#741)

Claude Code's installed_plugins.json changed from v1 (plugins as structs)
to v2 (plugins as arrays). Update GetClaudePluginVersion() to handle both.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-25 16:19:42 -08:00
parent a42fac8cb9
commit 3034fc392e
2 changed files with 71 additions and 6 deletions

View File

@@ -433,20 +433,46 @@ func GetClaudePluginVersion() (version string, installed bool, err error) {
return "", false, fmt.Errorf("unable to read plugin file: %w", err) return "", false, fmt.Errorf("unable to read plugin file: %w", err)
} }
// Parse JSON - handle nested structure // First, determine the format version
var pluginData struct { var versionCheck struct {
Version int `json:"version"` Version int `json:"version"`
}
if err := json.Unmarshal(data, &versionCheck); err != nil {
return "", false, fmt.Errorf("unable to parse plugin file: %w", err)
}
// Handle version 2 format (GH#741): plugins map contains arrays
if versionCheck.Version == 2 {
var pluginDataV2 struct {
Plugins map[string][]struct {
Version string `json:"version"`
Scope string `json:"scope"`
} `json:"plugins"`
}
if err := json.Unmarshal(data, &pluginDataV2); err != nil {
return "", false, fmt.Errorf("unable to parse plugin file v2: %w", err)
}
// Look for beads plugin - take first entry from the array
if entries, ok := pluginDataV2.Plugins["beads@beads-marketplace"]; ok && len(entries) > 0 {
return entries[0].Version, true, nil
}
return "", false, nil
}
// Handle version 1 format (original): plugins map contains structs directly
var pluginDataV1 struct {
Plugins map[string]struct { Plugins map[string]struct {
Version string `json:"version"` Version string `json:"version"`
} `json:"plugins"` } `json:"plugins"`
} }
if err := json.Unmarshal(data, &pluginData); err != nil { if err := json.Unmarshal(data, &pluginDataV1); err != nil {
return "", false, fmt.Errorf("unable to parse plugin file: %w", err) return "", false, fmt.Errorf("unable to parse plugin file: %w", err)
} }
// Look for beads plugin // Look for beads plugin
if plugin, ok := pluginData.Plugins["beads@beads-marketplace"]; ok { if plugin, ok := pluginDataV1.Plugins["beads@beads-marketplace"]; ok {
return plugin.Version, true, nil return plugin.Version, true, nil
} }

View File

@@ -804,7 +804,7 @@ func TestGetClaudePluginVersion(t *testing.T) {
expectError bool expectError bool
}{ }{
{ {
name: "plugin installed", name: "plugin installed v1 format",
pluginJSON: `{ pluginJSON: `{
"version": 1, "version": 1,
"plugins": { "plugins": {
@@ -818,7 +818,46 @@ func TestGetClaudePluginVersion(t *testing.T) {
expectError: false, expectError: false,
}, },
{ {
name: "plugin not installed", name: "plugin installed v2 format (GH#741)",
pluginJSON: `{
"version": 2,
"plugins": {
"beads@beads-marketplace": [
{
"scope": "user",
"installPath": "/path/to/plugin",
"version": "1.0.0",
"installedAt": "2025-11-25T19:20:27.889Z",
"lastUpdated": "2025-11-25T19:20:27.889Z",
"gitCommitSha": "abc123",
"isLocal": true
}
]
}
}`,
expectInstalled: true,
expectVersion: "1.0.0",
expectError: false,
},
{
name: "plugin not installed v2 format",
pluginJSON: `{
"version": 2,
"plugins": {
"other-plugin@marketplace": [
{
"scope": "user",
"version": "2.0.0"
}
]
}
}`,
expectInstalled: false,
expectVersion: "",
expectError: false,
},
{
name: "plugin not installed v1 format",
pluginJSON: `{ pluginJSON: `{
"version": 1, "version": 1,
"plugins": { "plugins": {