mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 14:49:09 +02:00
140 lines
3.7 KiB
Go
140 lines
3.7 KiB
Go
package daemon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type claudePluginInstall struct {
|
|
ID string
|
|
Name string
|
|
InstallPath string
|
|
}
|
|
|
|
type claudeInstalledPluginsFile struct {
|
|
Plugins map[string][]struct {
|
|
Scope string `json:"scope"`
|
|
InstallPath string `json:"installPath"`
|
|
} `json:"plugins"`
|
|
}
|
|
|
|
type claudeSettingsFile struct {
|
|
EnabledPlugins map[string]bool `json:"enabledPlugins"`
|
|
}
|
|
|
|
type claudePluginManifest struct {
|
|
Name string `json:"name"`
|
|
Skills json.RawMessage `json:"skills"`
|
|
MCPServers json.RawMessage `json:"mcpServers"`
|
|
}
|
|
|
|
// listEnabledClaudePlugins resolves the current user-scope plugin installs
|
|
// that Claude Code itself has enabled. Reading the install registry is
|
|
// deliberate: recursively scanning ~/.claude/plugins would surface both the
|
|
// marketplace checkout and every cached version of the same plugin.
|
|
func listEnabledClaudePlugins(home string) []claudePluginInstall {
|
|
settingsRaw, err := os.ReadFile(filepath.Join(home, ".claude", "settings.json"))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var settings claudeSettingsFile
|
|
if json.Unmarshal(settingsRaw, &settings) != nil || len(settings.EnabledPlugins) == 0 {
|
|
return nil
|
|
}
|
|
|
|
installedRaw, err := os.ReadFile(filepath.Join(home, ".claude", "plugins", "installed_plugins.json"))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var installed claudeInstalledPluginsFile
|
|
if json.Unmarshal(installedRaw, &installed) != nil {
|
|
return nil
|
|
}
|
|
|
|
pluginIDs := make([]string, 0, len(settings.EnabledPlugins))
|
|
for id, enabled := range settings.EnabledPlugins {
|
|
if enabled {
|
|
pluginIDs = append(pluginIDs, id)
|
|
}
|
|
}
|
|
sort.Strings(pluginIDs)
|
|
|
|
plugins := make([]claudePluginInstall, 0, len(pluginIDs))
|
|
for _, id := range pluginIDs {
|
|
installs := installed.Plugins[id]
|
|
if len(installs) == 0 {
|
|
continue
|
|
}
|
|
selected := installs[len(installs)-1]
|
|
for _, install := range installs {
|
|
if install.Scope == "user" {
|
|
selected = install
|
|
}
|
|
}
|
|
installPath := strings.TrimSpace(selected.InstallPath)
|
|
if installPath == "" {
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSpace(strings.SplitN(id, "@", 2)[0])
|
|
if manifest, ok := readClaudePluginManifest(installPath); ok && strings.TrimSpace(manifest.Name) != "" {
|
|
name = strings.TrimSpace(manifest.Name)
|
|
}
|
|
if name == "" {
|
|
continue
|
|
}
|
|
plugins = append(plugins, claudePluginInstall{ID: id, Name: name, InstallPath: installPath})
|
|
}
|
|
return plugins
|
|
}
|
|
|
|
func readClaudePluginManifest(installPath string) (claudePluginManifest, bool) {
|
|
raw, err := os.ReadFile(filepath.Join(installPath, ".claude-plugin", "plugin.json"))
|
|
if err != nil {
|
|
return claudePluginManifest{}, false
|
|
}
|
|
var manifest claudePluginManifest
|
|
if json.Unmarshal(raw, &manifest) != nil {
|
|
return claudePluginManifest{}, false
|
|
}
|
|
return manifest, true
|
|
}
|
|
|
|
func claudePluginComponentPaths(installPath string, raw json.RawMessage, defaults ...string) []string {
|
|
paths := append([]string(nil), defaults...)
|
|
var one string
|
|
if json.Unmarshal(raw, &one) == nil && strings.TrimSpace(one) != "" {
|
|
paths = append(paths, one)
|
|
} else {
|
|
var many []string
|
|
if json.Unmarshal(raw, &many) == nil {
|
|
paths = append(paths, many...)
|
|
}
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
out := make([]string, 0, len(paths))
|
|
for _, candidate := range paths {
|
|
candidate = strings.TrimSpace(candidate)
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
if !filepath.IsAbs(candidate) {
|
|
candidate = filepath.Join(installPath, filepath.FromSlash(candidate))
|
|
}
|
|
candidate = filepath.Clean(candidate)
|
|
rel, err := filepath.Rel(installPath, candidate)
|
|
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
continue
|
|
}
|
|
if !seen[candidate] {
|
|
seen[candidate] = true
|
|
out = append(out, candidate)
|
|
}
|
|
}
|
|
return out
|
|
}
|