返回 DeepSeek-Reasonix
doctor.go
根目录 / internal / cli / doctor.go
1 package cli
2
3 import (
4 "encoding/json"
5 "flag"
6 "fmt"
7 "os"
8 "strings"
9
10 "reasonix/internal/doctor"
11 "reasonix/internal/repair"
12 )
13
14 func doctorCommand(args []string, version string) int {
15 if len(args) > 0 && args[0] == "quality" {
16 return doctorQualityCommand(args[1:], version)
17 }
18 if len(args) > 0 && args[0] == "session" {
19 return doctorSessionCommand(args[1:], version)
20 }
21 if len(args) > 0 && args[0] == "redact-sessions" {
22 return doctorRedactSessionsCommand(args[1:])
23 }
24 if len(args) > 0 && args[0] == "capabilities" {
25 return doctorCapabilitiesCommand(args[1:])
26 }
27 if len(args) > 0 && args[0] == "repair" {
28 return doctorRepairCommand(args[1:])
29 }
30 fs := flag.NewFlagSet("doctor", flag.ContinueOnError)
31 jsonOut := fs.Bool("json", false, "print diagnostics as JSON")
32 if code, ok := parseCommandFlags(fs, args); !ok {
33 return code
34 }
35
36 report := doctor.Collect(doctor.Options{Version: version})
37 if *jsonOut {
38 enc := json.NewEncoder(os.Stdout)
39 enc.SetIndent("", " ")
40 if err := enc.Encode(report); err != nil {
41 fmt.Fprintln(os.Stderr, err)
42 return 1
43 }
44 return 0
45 }
46 fmt.Print(doctor.RenderText(report))
47 return 0
48 }
49
50 func doctorRepairCommand(args []string) int {
51 fs := flag.NewFlagSet("doctor repair", flag.ContinueOnError)
52 root := fs.String("root", ".", "project root to inspect")
53 apply := fs.Bool("apply", false, "quarantine invalid config and restore the last-known-good global snapshot")
54 includeProject := fs.Bool("project", false, "allow --apply to quarantine an invalid project reasonix.toml")
55 jsonOut := fs.Bool("json", false, "print result as JSON")
56 if code, ok := parseCommandFlags(fs, args); !ok {
57 return code
58 }
59 if fs.NArg() != 0 {
60 fmt.Fprintln(os.Stderr, "usage: reasonix doctor repair [--root PATH] [--apply] [--project] [--json]")
61 return 2
62 }
63 report, err := repair.InspectAndRepairConfig(repair.ConfigOptions{
64 Root: *root,
65 Apply: *apply,
66 IncludeProject: *includeProject,
67 })
68 if err != nil {
69 fmt.Fprintln(os.Stderr, "error:", err)
70 return 1
71 }
72 if *jsonOut {
73 enc := json.NewEncoder(os.Stdout)
74 enc.SetIndent("", " ")
75 if err := enc.Encode(report); err != nil {
76 fmt.Fprintln(os.Stderr, err)
77 return 1
78 }
79 } else {
80 fmt.Println("Reasonix repair report")
81 for _, check := range report.Checks {
82 status := "ok"
83 if !check.Exists {
84 status = "missing (defaults apply)"
85 } else if !check.Valid {
86 status = "invalid: " + check.Error
87 }
88 fmt.Printf(" %-8s %s\n %s\n", check.Scope, status, check.Path)
89 }
90 for _, action := range report.Applied {
91 fmt.Println(" applied:", action)
92 }
93 if !*apply {
94 fmt.Println(" dry run; pass --apply to repair the global config")
95 }
96 }
97 for _, check := range report.Checks {
98 if check.Exists && !check.Valid {
99 return 1
100 }
101 }
102 return 0
103 }
104
105 func doctorQualityCommand(args []string, version string) int {
106 ref := ""
107 jsonOut := false
108 for _, arg := range args {
109 switch arg {
110 case "-h", "--help":
111 fmt.Fprintln(os.Stdout, "usage: reasonix doctor quality <branch-id-or-path> [--json]")
112 fmt.Fprintln(os.Stdout, "Prints a public-safe, content-free coding-quality summary for one session.")
113 return 0
114 case "--json":
115 jsonOut = true
116 default:
117 if strings.HasPrefix(arg, "-") || ref != "" {
118 fmt.Fprintln(os.Stderr, "usage: reasonix doctor quality <branch-id-or-path> [--json]")
119 return 2
120 }
121 ref = arg
122 }
123 }
124 if ref == "" {
125 fmt.Fprintln(os.Stderr, "usage: reasonix doctor quality <branch-id-or-path> [--json]")
126 return 2
127 }
128 report, err := doctor.CollectQuality(doctor.QualityOptions{Version: version, SessionRef: ref})
129 if err != nil {
130 fmt.Fprintln(os.Stderr, "error:", err)
131 return 1
132 }
133 if jsonOut {
134 enc := json.NewEncoder(os.Stdout)
135 enc.SetIndent("", " ")
136 if err := enc.Encode(report); err != nil {
137 fmt.Fprintln(os.Stderr, err)
138 return 1
139 }
140 return 0
141 }
142 fmt.Print(doctor.RenderQualityText(report))
143 return 0
144 }
145
146 type stringListFlag []string
147
148 func (f *stringListFlag) String() string {
149 if f == nil {
150 return ""
151 }
152 return strings.Join(*f, string(os.PathListSeparator))
153 }
154
155 func (f *stringListFlag) Set(value string) error {
156 value = strings.TrimSpace(value)
157 if value == "" {
158 return fmt.Errorf("empty path")
159 }
160 *f = append(*f, value)
161 return nil
162 }
163
164 func doctorRedactSessionsCommand(args []string) int {
165 fs := flag.NewFlagSet("doctor redact-sessions", flag.ContinueOnError)
166 var dirs stringListFlag
167 dryRun := fs.Bool("dry-run", false, "show how many session files would be redacted without writing")
168 jsonOut := fs.Bool("json", false, "print result as JSON")
169 fs.Var(&dirs, "dir", "session directory to scan; repeat to scan multiple directories")
170 if code, ok := parseCommandFlags(fs, args); !ok {
171 return code
172 }
173 if fs.NArg() != 0 {
174 fmt.Fprintln(os.Stderr, "usage: reasonix doctor redact-sessions [--dry-run] [--json] [--dir PATH]")
175 return 2
176 }
177 res := doctor.RedactSessions(doctor.RedactSessionsOptions{
178 Dirs: []string(dirs),
179 DryRun: *dryRun,
180 })
181 if *jsonOut {
182 enc := json.NewEncoder(os.Stdout)
183 enc.SetIndent("", " ")
184 if err := enc.Encode(res); err != nil {
185 fmt.Fprintln(os.Stderr, err)
186 return 1
187 }
188 } else {
189 action := "redacted"
190 if *dryRun {
191 action = "would redact"
192 }
193 fmt.Fprintf(os.Stdout, "session secret cleanup %s %d/%d files", action, res.FilesChanged, res.FilesScanned)
194 if res.FilesSkipped > 0 {
195 fmt.Fprintf(os.Stdout, " (%d skipped: active lease held)", res.FilesSkipped)
196 }
197 fmt.Fprintln(os.Stdout)
198 }
199 for _, msg := range res.Errors {
200 fmt.Fprintln(os.Stderr, "warning:", msg)
201 }
202 if len(res.Errors) > 0 {
203 return 1
204 }
205 return 0
206 }
207
208 func doctorSessionCommand(args []string, version string) int {
209 ref := ""
210 outPath := ""
211 for i := 0; i < len(args); i++ {
212 arg := args[i]
213 switch arg {
214 case "-h", "--help":
215 fmt.Fprintln(os.Stdout, "usage: reasonix doctor session <branch-id-or-path> [--zip] [--out PATH]")
216 fmt.Fprintln(os.Stdout, "")
217 fmt.Fprintln(os.Stdout, "Bundles the session transcript, persistence sidecars, conflict diagnostics,")
218 fmt.Fprintln(os.Stdout, "and the recovery parent chain into a zip for support. Unlike `reasonix doctor`,")
219 fmt.Fprintln(os.Stdout, "bundled transcripts are NOT redacted; share only with a trusted support channel.")
220 return 0
221 case "--zip":
222 // The subcommand currently writes a zip by default. Keep --zip as an
223 // explicit, script-friendly marker so support replies can say exactly
224 // what to run.
225 case "--out":
226 i++
227 if i >= len(args) {
228 fmt.Fprintln(os.Stderr, "error: --out requires a path")
229 return 2
230 }
231 outPath = args[i]
232 default:
233 if v, ok := strings.CutPrefix(arg, "--out="); ok {
234 if v == "" {
235 fmt.Fprintln(os.Stderr, "error: --out requires a path")
236 return 2
237 }
238 outPath = v
239 continue
240 }
241 if strings.HasPrefix(arg, "-") {
242 fmt.Fprintf(os.Stderr, "error: unknown doctor session flag %s\n", arg)
243 return 2
244 }
245 if ref != "" {
246 fmt.Fprintln(os.Stderr, "usage: reasonix doctor session <branch-id-or-path> [--zip] [--out PATH]")
247 return 2
248 }
249 ref = arg
250 }
251 }
252 if ref == "" {
253 fmt.Fprintln(os.Stderr, "usage: reasonix doctor session <branch-id-or-path> [--zip] [--out PATH]")
254 return 2
255 }
256 result, err := doctor.WriteSessionBundle(doctor.SessionBundleOptions{
257 Version: version,
258 SessionRef: ref,
259 OutputPath: outPath,
260 })
261 if err != nil {
262 fmt.Fprintln(os.Stderr, "error:", err)
263 return 1
264 }
265 fmt.Println(result.Path)
266 fmt.Fprintln(os.Stderr, "note: the bundle contains full session transcripts without redaction; share it only with a trusted support channel")
267 return 0
268 }
269
269 lines GO