Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ dispatch
7. Press `s` to cycle sort fields, `S` to flip direction
8. Press `,` to open settings — change theme, launch mode, model, and more

### Diagnostics

Run `dispatch doctor` to print setup checks for the config file, session store, session-state directory, and Copilot CLI binary.

### Key Bindings

#### Navigation
Expand Down
59 changes: 59 additions & 0 deletions cmd/dispatch/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/jongio/dispatch/internal/config"
"github.com/jongio/dispatch/internal/data"
"github.com/jongio/dispatch/internal/platform"
"github.com/jongio/dispatch/internal/update"
"github.com/jongio/dispatch/internal/version"
)
Expand Down Expand Up @@ -50,6 +52,11 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
}
return true, cleanup, nil

case "doctor":
runDoctor(os.Stdout)
showUpdateNotification(origStderr, updateCh)
return true, cleanup, nil

case "--demo":
c, demoErr := setupDemo()
if demoErr != nil {
Expand Down Expand Up @@ -99,6 +106,58 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
return false, cleanup, nil
}

func runDoctor(w io.Writer) {
if w == nil {
w = io.Discard
}

fmt.Fprintf(w, "Dispatch doctor\n")
fmt.Fprintf(w, "Version: %s\n", version.Version)
fmt.Fprintf(w, "OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)
fmt.Fprintf(w, "\n")

if p, err := config.ConfigPath(); err != nil {
fmt.Fprintf(w, "Config: error: %v\n", err)
} else {
printPathStatus(w, "Config", p, false)
}

if p, err := platform.SessionStorePath(); err != nil {
fmt.Fprintf(w, "Session store: error: %v\n", err)
} else {
printPathStatus(w, "Session store", p, false)
}

if p := data.SessionStatePath(); p == "" {
fmt.Fprintf(w, "Session state: missing\n")
} else {
printPathStatus(w, "Session state", p, true)
}

if p := platform.FindCLIBinary(); p == "" {
fmt.Fprintf(w, "Copilot CLI: missing\n")
} else {
fmt.Fprintf(w, "Copilot CLI: found (%s)\n", p)
}
}

func printPathStatus(w io.Writer, label, path string, wantDir bool) {
info, err := os.Stat(path)
if err != nil {
fmt.Fprintf(w, "%s: missing (%s)\n", label, path)
return
}
if wantDir && !info.IsDir() {
fmt.Fprintf(w, "%s: wrong type, expected directory (%s)\n", label, path)
return
}
if !wantDir && info.IsDir() {
fmt.Fprintf(w, "%s: wrong type, expected file (%s)\n", label, path)
return
}
fmt.Fprintf(w, "%s: found (%s)\n", label, path)
}

// setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and
// redirects stderr to it. When no log file is configured, stderr is sent to
// os.DevNull to keep Bubble Tea's alt-screen clean. Returns the writer for
Expand Down
44 changes: 44 additions & 0 deletions cmd/dispatch/cli_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bytes"
"context"
"fmt"
"io"
Expand Down Expand Up @@ -83,6 +84,49 @@ func TestHandleArgs_VersionCommand(t *testing.T) {
}
}

func TestRunDoctor_PrintsDiagnostics(t *testing.T) {
db := filepath.Join(t.TempDir(), "session-store.db")
if err := os.WriteFile(db, []byte("sqlite"), 0o600); err != nil {
t.Fatal(err)
}
stateDir := t.TempDir()
t.Setenv("DISPATCH_DB", db)
t.Setenv("DISPATCH_SESSION_STATE", stateDir)

var buf bytes.Buffer
runDoctor(&buf)
out := buf.String()
for _, want := range []string{
"Dispatch doctor",
"Version:",
"OS:",
"Config:",
"Session store: found",
"Session state: found",
"Copilot CLI:",
} {
if !strings.Contains(out, want) {
t.Errorf("doctor output missing %q:\n%s", want, out)
}
}
}

func TestHandleArgs_Doctor(t *testing.T) {
ch := make(chan *update.UpdateInfo, 1)
ch <- nil

done, cleanup, err := handleArgs([]string{"doctor"}, io.Discard, ch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !done {
t.Error("expected done=true for doctor")
}
if cleanup != nil {
t.Error("expected cleanup=nil for doctor")
}
}

func TestHandleArgs_UnknownFlag(t *testing.T) {
ch := make(chan *update.UpdateInfo, 1)

Expand Down
1 change: 1 addition & 0 deletions cmd/dispatch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Usage:
Commands:
help Show this help message
version Print the version
doctor Print environment diagnostics
update Update dispatch to the latest release

Flags:
Expand Down
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ func configPath() (string, error) {
return filepath.Join(dir, configFileName), nil
}

// ConfigPath returns the full path to the configuration file.
func ConfigPath() (string, error) {
return configPath()
}

// Load reads the configuration file from disk and returns the parsed Config.
// If the file does not exist, Load returns [Default] values with a nil error.
func Load() (*Config, error) {
Expand Down
7 changes: 7 additions & 0 deletions internal/data/plans.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ func PlanFilePath(sessionID string) (string, error) {
return path, nil
}

// SessionStatePath returns the resolved Copilot CLI session-state directory
// used for plan and attention scans. It returns an empty string when the
// directory cannot be resolved.
func SessionStatePath() string {
return sessionStatePath()
}

// ---------------------------------------------------------------------------
// Continuation plan writing
// ---------------------------------------------------------------------------
Expand Down
Loading