返回 DeepSeek-Reasonix
describe.go
根目录 / internal / pluginpkg / describe.go
1 package pluginpkg
2
3 import (
4 "fmt"
5 "path/filepath"
6 "sort"
7 "strings"
8 )
9
10 // InstalledNames returns installed plugin package names sorted for completion
11 // menus and lightweight management views.
12 func InstalledNames(reasonixHome string) ([]string, error) {
13 st, err := LoadState(reasonixHome)
14 if err != nil {
15 return nil, err
16 }
17 names := make([]string, 0, len(st.Plugins))
18 for _, p := range st.Plugins {
19 names = append(names, p.Name)
20 }
21 sort.Strings(names)
22 return names, nil
23 }
24
25 // InstalledListText returns a compact session-facing view of installed plugins.
26 func InstalledListText(reasonixHome string) (string, error) {
27 st, err := LoadState(reasonixHome)
28 if err != nil {
29 return "", err
30 }
31 if len(st.Plugins) == 0 {
32 return "plugins: none installed\ninstall: reasonix plugin install <source> --yes, or use Settings -> Plugins", nil
33 }
34 var b strings.Builder
35 fmt.Fprintf(&b, "plugins (%d):\n", len(st.Plugins))
36 for _, p := range st.Plugins {
37 state := "disabled"
38 if p.Enabled {
39 state = "enabled"
40 }
41 summary := p.Description
42 counts := pluginCapabilityText(reasonixHome, p)
43 if summary != "" && counts != "" {
44 summary = counts + " - " + oneLine(summary)
45 } else if counts != "" {
46 summary = counts
47 } else if summary != "" {
48 summary = oneLine(summary)
49 }
50 if summary != "" {
51 fmt.Fprintf(&b, " %s [%s] - %s\n", p.Name, state, summary)
52 } else {
53 fmt.Fprintf(&b, " %s [%s]\n", p.Name, state)
54 }
55 }
56 b.WriteString("show details: /plugins show <name>")
57 return strings.TrimRight(b.String(), "\n"), nil
58 }
59
60 // InstalledShowText returns the usage-oriented details for one installed plugin.
61 func InstalledShowText(reasonixHome, name string) (string, error) {
62 p, ok, err := FindInstalled(reasonixHome, name)
63 if err != nil {
64 return "", err
65 }
66 if !ok {
67 return fmt.Sprintf("plugin %q is not installed", name), nil
68 }
69 root := ResolveRoot(reasonixHome, p.Root)
70 pkg, warnings, err := ParseDir(root)
71 if err != nil {
72 return "", err
73 }
74 summary := pkg.CapabilitySummary()
75 state := "disabled"
76 if p.Enabled {
77 state = "enabled"
78 }
79 version := p.Version
80 if version == "" {
81 version = "-"
82 }
83 var b strings.Builder
84 fmt.Fprintf(&b, "plugin %s [%s]\n", p.Name, state)
85 fmt.Fprintf(&b, "version: %s\nkind: %s\nroot: %s\nsource: %s\ncapabilities: %d skills, %d commands, %d prompts, %d hooks, %d MCP servers, %d themes\n", version, p.ManifestKind, filepath.Clean(root), p.Source, summary.Skills, summary.Commands, summary.Prompts, summary.Hooks, summary.MCPServers, summary.Themes)
86 if summary.Runtime {
87 b.WriteString(RuntimeTrustText(pkg.Manifest.Runtime))
88 }
89 if p.Enabled {
90 b.WriteString("usage: enabled plugins load into new sessions; use /skills, invoke /<plugin>:<skill> or /<plugin>:<command>, or ask naturally.\n")
91 } else {
92 b.WriteString("usage: enable this plugin before its skills, commands, hooks, or MCP servers participate in sessions.\n")
93 }
94 appendInventoryText(&b, p.Name, pkg.Inventory())
95 for _, warning := range warnings {
96 fmt.Fprintf(&b, "warning: %s\n", warning)
97 }
98 return strings.TrimRight(b.String(), "\n"), nil
99 }
100
101 func FindInstalled(reasonixHome, name string) (InstalledPlugin, bool, error) {
102 st, err := LoadState(reasonixHome)
103 if err != nil {
104 return InstalledPlugin{}, false, err
105 }
106 for _, p := range st.Plugins {
107 if p.Name == name {
108 return p, true, nil
109 }
110 }
111 return InstalledPlugin{}, false, nil
112 }
113
114 func pluginCapabilityText(reasonixHome string, p InstalledPlugin) string {
115 root := ResolveRoot(reasonixHome, p.Root)
116 pkg, _, err := ParseDir(root)
117 if err != nil {
118 return "invalid: " + err.Error()
119 }
120 summary := pkg.CapabilitySummary()
121 parts := []string{}
122 if summary.Skills > 0 {
123 parts = append(parts, fmt.Sprintf("%d skills", summary.Skills))
124 }
125 if summary.Commands > 0 {
126 parts = append(parts, fmt.Sprintf("%d commands", summary.Commands))
127 }
128 if summary.Prompts > 0 {
129 parts = append(parts, fmt.Sprintf("%d prompts", summary.Prompts))
130 }
131 if summary.Hooks > 0 {
132 parts = append(parts, fmt.Sprintf("%d hooks", summary.Hooks))
133 }
134 if summary.MCPServers > 0 {
135 parts = append(parts, fmt.Sprintf("%d MCP", summary.MCPServers))
136 }
137 if summary.Themes > 0 {
138 parts = append(parts, fmt.Sprintf("%d themes", summary.Themes))
139 }
140 if summary.Runtime {
141 parts = append(parts, "FULL TRUST runtime")
142 }
143 if len(parts) == 0 {
144 return "no exported capabilities"
145 }
146 return strings.Join(parts, " / ")
147 }
148
149 // RuntimeTrustText renders the prominent FULL TRUST block for a package that
150 // declares a runtime process. CLI and session-facing views share it so the
151 // risk statement reads identically everywhere.
152 func RuntimeTrustText(rt *RuntimeSpec) string {
153 if rt == nil {
154 return ""
155 }
156 var b strings.Builder
157 b.WriteString("runtime: FULL TRUST\n")
158 fmt.Fprintf(&b, " command: %s\n", RuntimeCommandLine(rt))
159 if len(rt.Intercepts) > 0 {
160 fmt.Fprintf(&b, " intercepts: %s\n", strings.Join(rt.Intercepts, ", "))
161 }
162 if len(rt.Replaces) > 0 {
163 fmt.Fprintf(&b, " replaces: %s\n", strings.Join(rt.Replaces, ", "))
164 }
165 if len(rt.Capabilities) > 0 {
166 fmt.Fprintf(&b, " capabilities: %s\n", strings.Join(rt.Capabilities, ", "))
167 }
168 b.WriteString(" risk: the runtime process runs inside Reasonix — it can read the full session and environment, bypass permissions, and operate this machine directly.\n")
169 return b.String()
170 }
171
172 // RuntimeCommandLine renders the exec-form runtime command plus args as one
173 // display line. The command is never executed through a shell.
174 func RuntimeCommandLine(rt *RuntimeSpec) string {
175 if rt == nil {
176 return ""
177 }
178 return strings.Join(append([]string{rt.Command}, rt.Args...), " ")
179 }
180
181 func appendInventoryText(b *strings.Builder, pluginName string, inv Inventory) {
182 if len(inv.Commands) > 0 {
183 b.WriteString("commands:\n")
184 for _, cmd := range inv.Commands {
185 desc := oneLine(cmd.Description)
186 if desc == "" {
187 desc = "(no description)"
188 }
189 invocation := "/" + pluginName + ":" + cmd.Name
190 if cmd.ArgHint != "" {
191 fmt.Fprintf(b, " %s %s - %s\n", invocation, cmd.ArgHint, desc)
192 } else {
193 fmt.Fprintf(b, " %s - %s\n", invocation, desc)
194 }
195 }
196 }
197 if len(inv.Skills) > 0 {
198 b.WriteString("skills:\n")
199 for _, sk := range inv.Skills {
200 desc := oneLine(sk.Description)
201 if desc == "" {
202 desc = "(no description)"
203 }
204 invocation := "/" + pluginName + ":" + sk.Name
205 if sk.RunAs != "" {
206 fmt.Fprintf(b, " %s [%s] - %s\n", invocation, sk.RunAs, desc)
207 } else {
208 fmt.Fprintf(b, " %s - %s\n", invocation, desc)
209 }
210 }
211 }
212 if len(inv.Prompts) > 0 {
213 b.WriteString("prompts:\n")
214 for _, pr := range inv.Prompts {
215 desc := oneLine(pr.Description)
216 if desc == "" {
217 desc = "(no description)"
218 }
219 if pr.ArgHint != "" {
220 fmt.Fprintf(b, " %s %s - %s\n", pr.Name, pr.ArgHint, desc)
221 } else {
222 fmt.Fprintf(b, " %s - %s\n", pr.Name, desc)
223 }
224 }
225 }
226 if len(inv.Themes) > 0 {
227 b.WriteString("themes:\n")
228 for _, theme := range inv.Themes {
229 fmt.Fprintf(b, " %s - %s\n", theme.Name, theme.Path)
230 }
231 }
232 if len(inv.Hooks) > 0 {
233 b.WriteString("hooks:\n")
234 for _, hook := range inv.Hooks {
235 target := hook.Command
236 if target == "" {
237 target = hook.ContextFile
238 }
239 match := hook.Match
240 if match == "" {
241 match = "*"
242 }
243 desc := oneLine(hook.Description)
244 if desc != "" {
245 fmt.Fprintf(b, " %s match=%s - %s - %s\n", hook.Event, match, target, desc)
246 } else {
247 fmt.Fprintf(b, " %s match=%s - %s\n", hook.Event, match, target)
248 }
249 }
250 }
251 if len(inv.MCPServers) > 0 {
252 b.WriteString("mcpServers:\n")
253 for _, server := range inv.MCPServers {
254 target := server.Command
255 if target == "" {
256 target = server.URL
257 }
258 fmt.Fprintf(b, " %s [%s] - %s\n", server.Name, server.Transport, target)
259 }
260 }
261 if len(inv.Skills) == 0 && len(inv.Commands) == 0 && len(inv.Prompts) == 0 && len(inv.Themes) == 0 && len(inv.Hooks) == 0 && len(inv.MCPServers) == 0 {
262 b.WriteString("capabilities: no detailed inventory available\n")
263 }
264 }
265
266 func oneLine(s string) string {
267 return strings.Join(strings.Fields(s), " ")
268 }
269
269 lines GO