Files
2026-07-13 13:00:08 +08:00

205 lines
5.6 KiB
Go

package pluginpkg
import (
"fmt"
"path/filepath"
"sort"
"strings"
)
// InstalledNames returns installed plugin package names sorted for completion
// menus and lightweight management views.
func InstalledNames(reasonixHome string) ([]string, error) {
st, err := LoadState(reasonixHome)
if err != nil {
return nil, err
}
names := make([]string, 0, len(st.Plugins))
for _, p := range st.Plugins {
names = append(names, p.Name)
}
sort.Strings(names)
return names, nil
}
// InstalledListText returns a compact session-facing view of installed plugins.
func InstalledListText(reasonixHome string) (string, error) {
st, err := LoadState(reasonixHome)
if err != nil {
return "", err
}
if len(st.Plugins) == 0 {
return "plugins: none installed\ninstall: reasonix plugin install <source> --yes, or use Settings -> Plugins", nil
}
var b strings.Builder
fmt.Fprintf(&b, "plugins (%d):\n", len(st.Plugins))
for _, p := range st.Plugins {
state := "disabled"
if p.Enabled {
state = "enabled"
}
summary := p.Description
counts := pluginCapabilityText(reasonixHome, p)
if summary != "" && counts != "" {
summary = counts + " - " + oneLine(summary)
} else if counts != "" {
summary = counts
} else if summary != "" {
summary = oneLine(summary)
}
if summary != "" {
fmt.Fprintf(&b, " %s [%s] - %s\n", p.Name, state, summary)
} else {
fmt.Fprintf(&b, " %s [%s]\n", p.Name, state)
}
}
b.WriteString("show details: /plugins show <name>")
return strings.TrimRight(b.String(), "\n"), nil
}
// InstalledShowText returns the usage-oriented details for one installed plugin.
func InstalledShowText(reasonixHome, name string) (string, error) {
p, ok, err := FindInstalled(reasonixHome, name)
if err != nil {
return "", err
}
if !ok {
return fmt.Sprintf("plugin %q is not installed", name), nil
}
root := ResolveRoot(reasonixHome, p.Root)
pkg, warnings, err := ParseDir(root)
if err != nil {
return "", err
}
skills, commands, hooks, mcp := pkg.CapabilityCounts()
state := "disabled"
if p.Enabled {
state = "enabled"
}
version := p.Version
if version == "" {
version = "-"
}
var b strings.Builder
fmt.Fprintf(&b, "plugin %s [%s]\n", p.Name, state)
fmt.Fprintf(&b, "version: %s\nkind: %s\nroot: %s\nsource: %s\ncapabilities: %d skills, %d commands, %d hooks, %d MCP servers\n", version, p.ManifestKind, filepath.Clean(root), p.Source, skills, commands, hooks, mcp)
if p.Enabled {
b.WriteString("usage: enabled plugins load into new sessions; use /skills, invoke /<plugin>:<skill> or /<plugin>:<command>, or ask naturally.\n")
} else {
b.WriteString("usage: enable this plugin before its skills, commands, hooks, or MCP servers participate in sessions.\n")
}
appendInventoryText(&b, p.Name, pkg.Inventory())
for _, warning := range warnings {
fmt.Fprintf(&b, "warning: %s\n", warning)
}
return strings.TrimRight(b.String(), "\n"), nil
}
func FindInstalled(reasonixHome, name string) (InstalledPlugin, bool, error) {
st, err := LoadState(reasonixHome)
if err != nil {
return InstalledPlugin{}, false, err
}
for _, p := range st.Plugins {
if p.Name == name {
return p, true, nil
}
}
return InstalledPlugin{}, false, nil
}
func pluginCapabilityText(reasonixHome string, p InstalledPlugin) string {
root := ResolveRoot(reasonixHome, p.Root)
pkg, _, err := ParseDir(root)
if err != nil {
return "invalid: " + err.Error()
}
skills, commands, hooks, mcp := pkg.CapabilityCounts()
parts := []string{}
if skills > 0 {
parts = append(parts, fmt.Sprintf("%d skills", skills))
}
if commands > 0 {
parts = append(parts, fmt.Sprintf("%d commands", commands))
}
if hooks > 0 {
parts = append(parts, fmt.Sprintf("%d hooks", hooks))
}
if mcp > 0 {
parts = append(parts, fmt.Sprintf("%d MCP", mcp))
}
if len(parts) == 0 {
return "no exported capabilities"
}
return strings.Join(parts, " / ")
}
func appendInventoryText(b *strings.Builder, pluginName string, inv Inventory) {
if len(inv.Commands) > 0 {
b.WriteString("commands:\n")
for _, cmd := range inv.Commands {
desc := oneLine(cmd.Description)
if desc == "" {
desc = "(no description)"
}
invocation := "/" + pluginName + ":" + cmd.Name
if cmd.ArgHint != "" {
fmt.Fprintf(b, " %s %s - %s\n", invocation, cmd.ArgHint, desc)
} else {
fmt.Fprintf(b, " %s - %s\n", invocation, desc)
}
}
}
if len(inv.Skills) > 0 {
b.WriteString("skills:\n")
for _, sk := range inv.Skills {
desc := oneLine(sk.Description)
if desc == "" {
desc = "(no description)"
}
invocation := "/" + pluginName + ":" + sk.Name
if sk.RunAs != "" {
fmt.Fprintf(b, " %s [%s] - %s\n", invocation, sk.RunAs, desc)
} else {
fmt.Fprintf(b, " %s - %s\n", invocation, desc)
}
}
}
if len(inv.Hooks) > 0 {
b.WriteString("hooks:\n")
for _, hook := range inv.Hooks {
target := hook.Command
if target == "" {
target = hook.ContextFile
}
match := hook.Match
if match == "" {
match = "*"
}
desc := oneLine(hook.Description)
if desc != "" {
fmt.Fprintf(b, " %s match=%s - %s - %s\n", hook.Event, match, target, desc)
} else {
fmt.Fprintf(b, " %s match=%s - %s\n", hook.Event, match, target)
}
}
}
if len(inv.MCPServers) > 0 {
b.WriteString("mcpServers:\n")
for _, server := range inv.MCPServers {
target := server.Command
if target == "" {
target = server.URL
}
fmt.Fprintf(b, " %s [%s] - %s\n", server.Name, server.Transport, target)
}
}
if len(inv.Skills) == 0 && len(inv.Commands) == 0 && len(inv.Hooks) == 0 && len(inv.MCPServers) == 0 {
b.WriteString("capabilities: no detailed inventory available\n")
}
}
func oneLine(s string) string {
return strings.Join(strings.Fields(s), " ")
}