@@ -2,12 +2,14 @@ package main
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
78 "io"
89 "os"
910 "path/filepath"
1011 "runtime"
12+ "slices"
1113 "strings"
1214
1315 "github.com/jongio/dispatch/internal/config"
@@ -65,7 +67,14 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
6567 return true , cleanup , nil
6668
6769 case "doctor" :
68- runDoctor (os .Stdout )
70+ if slices .Contains (args , "--json" ) {
71+ if jErr := runDoctorJSON (os .Stdout ); jErr != nil {
72+ fmt .Fprintf (os .Stderr , "doctor: %v\n " , jErr )
73+ return true , cleanup , jErr
74+ }
75+ } else {
76+ runDoctor (os .Stdout )
77+ }
6978 showUpdateNotification (origStderr , updateCh )
7079 return true , cleanup , nil
7180
@@ -207,56 +216,136 @@ Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock {
207216}
208217`
209218
210- func runDoctor (w io.Writer ) {
211- if w == nil {
212- w = io .Discard
213- }
219+ // doctorStatus values describe the result of a single diagnostic path check.
220+ const (
221+ statusFound = "found"
222+ statusMissing = "missing"
223+ statusWrongType = "wrong_type"
224+ statusError = "error"
225+ )
214226
215- fmt .Fprintf (w , "Dispatch doctor\n " )
216- fmt .Fprintf (w , "Version: %s\n " , version .Version )
217- fmt .Fprintf (w , "OS: %s/%s\n " , runtime .GOOS , runtime .GOARCH )
218- fmt .Fprintf (w , "\n " )
227+ // doctorEntry is the diagnostic result for one path. The err field is used
228+ // only by the text renderer and is not serialized to JSON.
229+ type doctorEntry struct {
230+ Path string `json:"path"`
231+ Status string `json:"status"`
232+ err error
233+ }
234+
235+ // doctorReport is the full set of diagnostics gathered by the doctor command.
236+ // Both the text and JSON renderers consume this struct so their outputs stay
237+ // in sync.
238+ type doctorReport struct {
239+ Version string `json:"version"`
240+ OS string `json:"os"`
241+ Config doctorEntry `json:"config"`
242+ SessionStore doctorEntry `json:"session_store"`
243+ SessionState doctorEntry `json:"session_state"`
244+ CopilotCLI doctorEntry `json:"copilot_cli"`
245+ }
246+
247+ // collectDoctorReport gathers the environment diagnostics once so they can be
248+ // rendered as text or JSON without drifting apart.
249+ func collectDoctorReport () doctorReport {
250+ r := doctorReport {
251+ Version : version .Version ,
252+ OS : runtime .GOOS + "/" + runtime .GOARCH ,
253+ }
219254
220255 if p , err := config .ConfigPath (); err != nil {
221- fmt . Fprintf ( w , " Config: error: %v \n " , err )
256+ r . Config = doctorEntry { Status : statusError , err : err }
222257 } else {
223- printPathStatus ( w , " Config" , p , false )
258+ r . Config = doctorEntry { Path : p , Status : pathStatus ( p , false )}
224259 }
225260
226261 if p , err := platform .SessionStorePath (); err != nil {
227- fmt . Fprintf ( w , "Session store: error: %v \n " , err )
262+ r . SessionStore = doctorEntry { Status : statusError , err : err }
228263 } else {
229- printPathStatus ( w , "Session store" , p , false )
264+ r . SessionStore = doctorEntry { Path : p , Status : pathStatus ( p , false )}
230265 }
231266
232267 if p := data .SessionStatePath (); p == "" {
233- fmt . Fprintf ( w , "Session state: missing \n " )
268+ r . SessionState = doctorEntry { Status : statusMissing }
234269 } else {
235- printPathStatus ( w , "Session state" , p , true )
270+ r . SessionState = doctorEntry { Path : p , Status : pathStatus ( p , true )}
236271 }
237272
238273 if p := platform .FindCLIBinary (); p == "" {
239- fmt . Fprintf ( w , "Copilot CLI: missing \n " )
274+ r . CopilotCLI = doctorEntry { Status : statusMissing }
240275 } else {
241- fmt . Fprintf ( w , "Copilot CLI: found (%s) \n " , p )
276+ r . CopilotCLI = doctorEntry { Path : p , Status : statusFound }
242277 }
278+
279+ return r
243280}
244281
245- func printPathStatus (w io.Writer , label , path string , wantDir bool ) {
282+ // pathStatus stats a path and reports whether it is found, missing, or the
283+ // wrong type (a file where a directory is expected, or vice versa).
284+ func pathStatus (path string , wantDir bool ) string {
246285 info , err := os .Stat (path )
247286 if err != nil {
248- fmt .Fprintf (w , "%s: missing (%s)\n " , label , path )
249- return
287+ return statusMissing
250288 }
251- if wantDir && ! info .IsDir () {
252- fmt .Fprintf (w , "%s: wrong type, expected directory (%s)\n " , label , path )
253- return
289+ if wantDir != info .IsDir () {
290+ return statusWrongType
291+ }
292+ return statusFound
293+ }
294+
295+ func runDoctor (w io.Writer ) {
296+ if w == nil {
297+ w = io .Discard
254298 }
255- if ! wantDir && info .IsDir () {
256- fmt .Fprintf (w , "%s: wrong type, expected file (%s)\n " , label , path )
299+
300+ r := collectDoctorReport ()
301+
302+ fmt .Fprintf (w , "Dispatch doctor\n " )
303+ fmt .Fprintf (w , "Version: %s\n " , r .Version )
304+ fmt .Fprintf (w , "OS: %s\n " , r .OS )
305+ fmt .Fprintf (w , "\n " )
306+
307+ writeDoctorLine (w , "Config" , r .Config , false )
308+ writeDoctorLine (w , "Session store" , r .SessionStore , false )
309+ writeDoctorLine (w , "Session state" , r .SessionState , true )
310+ writeDoctorLine (w , "Copilot CLI" , r .CopilotCLI , false )
311+ }
312+
313+ // runDoctorJSON writes the diagnostics as a single JSON object followed by a
314+ // newline.
315+ func runDoctorJSON (w io.Writer ) error {
316+ if w == nil {
317+ w = io .Discard
318+ }
319+ b , err := json .MarshalIndent (collectDoctorReport (), "" , " " )
320+ if err != nil {
321+ return err
322+ }
323+ fmt .Fprintf (w , "%s\n " , b )
324+ return nil
325+ }
326+
327+ // writeDoctorLine renders one diagnostic entry as human-readable text.
328+ func writeDoctorLine (w io.Writer , label string , e doctorEntry , wantDir bool ) {
329+ if e .err != nil {
330+ fmt .Fprintf (w , "%s: error: %v\n " , label , e .err )
257331 return
258332 }
259- fmt .Fprintf (w , "%s: found (%s)\n " , label , path )
333+ switch e .Status {
334+ case statusMissing :
335+ if e .Path == "" {
336+ fmt .Fprintf (w , "%s: missing\n " , label )
337+ } else {
338+ fmt .Fprintf (w , "%s: missing (%s)\n " , label , e .Path )
339+ }
340+ case statusWrongType :
341+ if wantDir {
342+ fmt .Fprintf (w , "%s: wrong type, expected directory (%s)\n " , label , e .Path )
343+ } else {
344+ fmt .Fprintf (w , "%s: wrong type, expected file (%s)\n " , label , e .Path )
345+ }
346+ default :
347+ fmt .Fprintf (w , "%s: found (%s)\n " , label , e .Path )
348+ }
260349}
261350
262351// setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and
0 commit comments