Skip to content

Commit 274b2aa

Browse files
jongioCopilot
andauthored
Add doctor diagnostics command (#166)
* Add doctor diagnostics command Closes #163 Co-authored-by: Copilot <[email protected]> * Fix doctor lint warning Closes #163 Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 608557d commit 274b2aa

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ dispatch
129129
7. Press `s` to cycle sort fields, `S` to flip direction
130130
8. Press `,` to open settings — change theme, launch mode, model, and more
131131

132+
### Diagnostics
133+
134+
Run `dispatch doctor` to print setup checks for the config file, session store, session-state directory, and Copilot CLI binary.
135+
132136
### Key Bindings
133137

134138
#### Navigation

cmd/dispatch/cli.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
"io"
88
"os"
99
"path/filepath"
10+
"runtime"
1011
"strings"
1112

1213
"github.com/jongio/dispatch/internal/config"
1314
"github.com/jongio/dispatch/internal/data"
15+
"github.com/jongio/dispatch/internal/platform"
1416
"github.com/jongio/dispatch/internal/update"
1517
"github.com/jongio/dispatch/internal/version"
1618
)
@@ -50,6 +52,11 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
5052
}
5153
return true, cleanup, nil
5254

55+
case "doctor":
56+
runDoctor(os.Stdout)
57+
showUpdateNotification(origStderr, updateCh)
58+
return true, cleanup, nil
59+
5360
case "--demo":
5461
c, demoErr := setupDemo()
5562
if demoErr != nil {
@@ -99,6 +106,58 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
99106
return false, cleanup, nil
100107
}
101108

109+
func runDoctor(w io.Writer) {
110+
if w == nil {
111+
w = io.Discard
112+
}
113+
114+
fmt.Fprintf(w, "Dispatch doctor\n")
115+
fmt.Fprintf(w, "Version: %s\n", version.Version)
116+
fmt.Fprintf(w, "OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)
117+
fmt.Fprintf(w, "\n")
118+
119+
if p, err := config.ConfigPath(); err != nil {
120+
fmt.Fprintf(w, "Config: error: %v\n", err)
121+
} else {
122+
printPathStatus(w, "Config", p, false)
123+
}
124+
125+
if p, err := platform.SessionStorePath(); err != nil {
126+
fmt.Fprintf(w, "Session store: error: %v\n", err)
127+
} else {
128+
printPathStatus(w, "Session store", p, false)
129+
}
130+
131+
if p := data.SessionStatePath(); p == "" {
132+
fmt.Fprintf(w, "Session state: missing\n")
133+
} else {
134+
printPathStatus(w, "Session state", p, true)
135+
}
136+
137+
if p := platform.FindCLIBinary(); p == "" {
138+
fmt.Fprintf(w, "Copilot CLI: missing\n")
139+
} else {
140+
fmt.Fprintf(w, "Copilot CLI: found (%s)\n", p)
141+
}
142+
}
143+
144+
func printPathStatus(w io.Writer, label, path string, wantDir bool) {
145+
info, err := os.Stat(path)
146+
if err != nil {
147+
fmt.Fprintf(w, "%s: missing (%s)\n", label, path)
148+
return
149+
}
150+
if wantDir && !info.IsDir() {
151+
fmt.Fprintf(w, "%s: wrong type, expected directory (%s)\n", label, path)
152+
return
153+
}
154+
if !wantDir && info.IsDir() {
155+
fmt.Fprintf(w, "%s: wrong type, expected file (%s)\n", label, path)
156+
return
157+
}
158+
fmt.Fprintf(w, "%s: found (%s)\n", label, path)
159+
}
160+
102161
// setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and
103162
// redirects stderr to it. When no log file is configured, stderr is sent to
104163
// os.DevNull to keep Bubble Tea's alt-screen clean. Returns the writer for

cmd/dispatch/cli_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"io"
@@ -83,6 +84,49 @@ func TestHandleArgs_VersionCommand(t *testing.T) {
8384
}
8485
}
8586

87+
func TestRunDoctor_PrintsDiagnostics(t *testing.T) {
88+
db := filepath.Join(t.TempDir(), "session-store.db")
89+
if err := os.WriteFile(db, []byte("sqlite"), 0o600); err != nil {
90+
t.Fatal(err)
91+
}
92+
stateDir := t.TempDir()
93+
t.Setenv("DISPATCH_DB", db)
94+
t.Setenv("DISPATCH_SESSION_STATE", stateDir)
95+
96+
var buf bytes.Buffer
97+
runDoctor(&buf)
98+
out := buf.String()
99+
for _, want := range []string{
100+
"Dispatch doctor",
101+
"Version:",
102+
"OS:",
103+
"Config:",
104+
"Session store: found",
105+
"Session state: found",
106+
"Copilot CLI:",
107+
} {
108+
if !strings.Contains(out, want) {
109+
t.Errorf("doctor output missing %q:\n%s", want, out)
110+
}
111+
}
112+
}
113+
114+
func TestHandleArgs_Doctor(t *testing.T) {
115+
ch := make(chan *update.UpdateInfo, 1)
116+
ch <- nil
117+
118+
done, cleanup, err := handleArgs([]string{"doctor"}, io.Discard, ch)
119+
if err != nil {
120+
t.Fatalf("unexpected error: %v", err)
121+
}
122+
if !done {
123+
t.Error("expected done=true for doctor")
124+
}
125+
if cleanup != nil {
126+
t.Error("expected cleanup=nil for doctor")
127+
}
128+
}
129+
86130
func TestHandleArgs_UnknownFlag(t *testing.T) {
87131
ch := make(chan *update.UpdateInfo, 1)
88132

cmd/dispatch/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Usage:
9090
Commands:
9191
help Show this help message
9292
version Print the version
93+
doctor Print environment diagnostics
9394
update Update dispatch to the latest release
9495
9596
Flags:

internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ func configPath() (string, error) {
438438
return filepath.Join(dir, configFileName), nil
439439
}
440440

441+
// ConfigPath returns the full path to the configuration file.
442+
func ConfigPath() (string, error) {
443+
return configPath()
444+
}
445+
441446
// Load reads the configuration file from disk and returns the parsed Config.
442447
// If the file does not exist, Load returns [Default] values with a nil error.
443448
func Load() (*Config, error) {

internal/data/plans.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ func PlanFilePath(sessionID string) (string, error) {
118118
return path, nil
119119
}
120120

121+
// SessionStatePath returns the resolved Copilot CLI session-state directory
122+
// used for plan and attention scans. It returns an empty string when the
123+
// directory cannot be resolved.
124+
func SessionStatePath() string {
125+
return sessionStatePath()
126+
}
127+
121128
// ---------------------------------------------------------------------------
122129
// Continuation plan writing
123130
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)