| 1 | package capdiag |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // RenderText formats a human-readable report: Summary → Issues → subsystems. |
| 10 | func RenderText(r Report) string { |
| 11 | var b strings.Builder |
| 12 | fmt.Fprintf(&b, "reasonix doctor capabilities\n") |
| 13 | fmt.Fprintf(&b, "root: %s live: %v schema: %d\n\n", r.Root, r.Live, r.SchemaVersion) |
| 14 | |
| 15 | fmt.Fprintf(&b, "Summary\n") |
| 16 | fmt.Fprintf(&b, " errors=%d warnings=%d infos=%d\n", r.Summary.Errors, r.Summary.Warnings, r.Summary.Infos) |
| 17 | fmt.Fprintf(&b, " instructions=%d skills=%d commands=%d hooks=%d plugins=%d mcp=%d\n\n", |
| 18 | r.Summary.Instructions, r.Summary.Skills, r.Summary.Commands, |
| 19 | r.Summary.Hooks, r.Summary.Plugins, r.Summary.MCPServers) |
| 20 | |
| 21 | fmt.Fprintf(&b, "Issues (%d)\n", len(r.Issues)) |
| 22 | if len(r.Issues) == 0 { |
| 23 | b.WriteString(" (none)\n") |
| 24 | } |
| 25 | for _, is := range r.Issues { |
| 26 | fmt.Fprintf(&b, " [%s] %s", is.Severity, is.Code) |
| 27 | if is.Name != "" { |
| 28 | fmt.Fprintf(&b, " %s", is.Name) |
| 29 | } |
| 30 | fmt.Fprintf(&b, ": %s\n", is.Message) |
| 31 | if is.Source != "" { |
| 32 | fmt.Fprintf(&b, " source: %s\n", is.Source) |
| 33 | } |
| 34 | if is.Remediation != "" { |
| 35 | fmt.Fprintf(&b, " fix: %s\n", is.Remediation) |
| 36 | } |
| 37 | } |
| 38 | b.WriteByte('\n') |
| 39 | |
| 40 | // Instructions |
| 41 | fmt.Fprintf(&b, "Instructions (%d)\n", len(r.Instructions.Docs)) |
| 42 | for _, d := range r.Instructions.Docs { |
| 43 | fmt.Fprintf(&b, " %d. [%s depth=%d] %s", d.Order, d.Scope, d.Depth, d.Path) |
| 44 | if d.Directory != "" { |
| 45 | fmt.Fprintf(&b, " (applies to %s)", d.Directory) |
| 46 | } |
| 47 | b.WriteByte('\n') |
| 48 | } |
| 49 | if len(r.Instructions.Docs) == 0 { |
| 50 | b.WriteString(" (none)\n") |
| 51 | } |
| 52 | b.WriteByte('\n') |
| 53 | |
| 54 | writeAsset(&b, "Skills", r.Skills) |
| 55 | writeAsset(&b, "Commands", r.Commands) |
| 56 | |
| 57 | fmt.Fprintf(&b, "Hooks (project_defines=%v entries=%d)\n", |
| 58 | r.Hooks.ProjectDefines, len(r.Hooks.Entries)) |
| 59 | for _, s := range r.Hooks.Sources { |
| 60 | fmt.Fprintf(&b, " source [%s] %s status=%s hooks=%d\n", s.Scope, s.Path, s.Status, s.HookCount) |
| 61 | } |
| 62 | for _, e := range r.Hooks.Entries { |
| 63 | fmt.Fprintf(&b, " - %s scope=%s match=%q blocking=%v\n", e.Event, e.Scope, e.Match, e.Blocking) |
| 64 | } |
| 65 | b.WriteByte('\n') |
| 66 | |
| 67 | fmt.Fprintf(&b, "Plugins (%d)\n", len(r.Plugins.Packages)) |
| 68 | for _, p := range r.Plugins.Packages { |
| 69 | fmt.Fprintf(&b, " - %s enabled=%v status=%s skills=%d commands=%d prompts=%d hooks=%d mcp=%d themes=%d\n", |
| 70 | p.Name, p.Enabled, p.Status, p.Skills, p.Commands, p.Prompts, p.Hooks, p.MCPServers, p.Themes) |
| 71 | if p.Runtime { |
| 72 | b.WriteString(" runtime: FULL TRUST (declares a runtime process)\n") |
| 73 | } |
| 74 | fmt.Fprintf(&b, " root: %s\n", p.Root) |
| 75 | } |
| 76 | if len(r.Plugins.Packages) == 0 { |
| 77 | b.WriteString(" (none)\n") |
| 78 | } |
| 79 | b.WriteByte('\n') |
| 80 | |
| 81 | fmt.Fprintf(&b, "MCP (%d)\n", len(r.MCP.Servers)) |
| 82 | for _, s := range r.MCP.Servers { |
| 83 | fmt.Fprintf(&b, " - %s transport=%s intent=%s source=%s effective=%v", s.Name, s.Transport, s.StartIntent, s.Source, s.Effective) |
| 84 | if s.RuntimeStatus != "" { |
| 85 | fmt.Fprintf(&b, " runtime=%s", s.RuntimeStatus) |
| 86 | } |
| 87 | if s.ToolCount > 0 { |
| 88 | fmt.Fprintf(&b, " tools=%d", s.ToolCount) |
| 89 | } |
| 90 | b.WriteByte('\n') |
| 91 | if s.SourcePath != "" { |
| 92 | fmt.Fprintf(&b, " source_path: %s\n", s.SourcePath) |
| 93 | } |
| 94 | if s.StartupStage != "" { |
| 95 | fmt.Fprintf(&b, " startup: stage=%s elapsed_ms=%d\n", s.StartupStage, s.StartupElapsedMS) |
| 96 | } |
| 97 | if s.Error != "" { |
| 98 | fmt.Fprintf(&b, " error: %s\n", s.Error) |
| 99 | } |
| 100 | if s.Stderr != "" { |
| 101 | fmt.Fprintf(&b, " stderr: %s\n", s.Stderr) |
| 102 | } |
| 103 | } |
| 104 | if len(r.MCP.Servers) == 0 { |
| 105 | b.WriteString(" (none)\n") |
| 106 | } |
| 107 | return b.String() |
| 108 | } |
| 109 | |
| 110 | func writeAsset(b *strings.Builder, title string, a AssetReport) { |
| 111 | fmt.Fprintf(b, "%s (winners=%d shadowed=%d", title, a.Winners, a.Shadowed) |
| 112 | if a.Disabled > 0 { |
| 113 | fmt.Fprintf(b, " disabled=%d", a.Disabled) |
| 114 | } |
| 115 | if a.ParseErrors > 0 { |
| 116 | fmt.Fprintf(b, " errors=%d", a.ParseErrors) |
| 117 | } |
| 118 | b.WriteString(")\n") |
| 119 | for _, r := range a.Roots { |
| 120 | fmt.Fprintf(b, " root %s status=%s", r.Path, r.Status) |
| 121 | if r.Scope != "" { |
| 122 | fmt.Fprintf(b, " scope=%s", r.Scope) |
| 123 | } |
| 124 | b.WriteByte('\n') |
| 125 | } |
| 126 | for _, e := range a.Entries { |
| 127 | if e.Status != "winner" && e.Status != "error" { |
| 128 | continue // keep text output compact; full list is in JSON |
| 129 | } |
| 130 | fmt.Fprintf(b, " - %s [%s] %s\n", e.Name, e.Status, e.Path) |
| 131 | } |
| 132 | b.WriteByte('\n') |
| 133 | } |
| 134 | |
| 135 | // RenderJSON writes indented JSON to a string (for tests / copy). |
| 136 | func RenderJSON(r Report) (string, error) { |
| 137 | b, err := json.MarshalIndent(r, "", " ") |
| 138 | if err != nil { |
| 139 | return "", err |
| 140 | } |
| 141 | return string(b) + "\n", nil |
| 142 | } |
| 143 |