|
43 | 43 | // seams so tests can substitute them without touching the environment. |
44 | 44 | doctorCopilotVersionFn = defaultCopilotVersion |
45 | 45 | doctorSessionCountFn = defaultSessionCount |
| 46 | + doctorWorkspacesFn = defaultDoctorWorkspaces |
46 | 47 | ) |
47 | 48 |
|
48 | 49 | type versionOutput struct { |
@@ -588,15 +589,25 @@ type doctorEntry struct { |
588 | 589 | // Both the text and JSON renderers consume this struct so their outputs stay |
589 | 590 | // in sync. |
590 | 591 | type doctorReport struct { |
591 | | - Version string `json:"version"` |
592 | | - OS string `json:"os"` |
593 | | - OK bool `json:"ok"` |
594 | | - Config doctorEntry `json:"config"` |
595 | | - SessionStore doctorEntry `json:"session_store"` |
596 | | - SessionState doctorEntry `json:"session_state"` |
597 | | - CopilotCLI doctorEntry `json:"copilot_cli"` |
598 | | - CopilotVersion string `json:"copilot_version"` |
599 | | - SessionCount int `json:"session_count"` |
| 592 | + Version string `json:"version"` |
| 593 | + OS string `json:"os"` |
| 594 | + OK bool `json:"ok"` |
| 595 | + Config doctorEntry `json:"config"` |
| 596 | + SessionStore doctorEntry `json:"session_store"` |
| 597 | + SessionState doctorEntry `json:"session_state"` |
| 598 | + CopilotCLI doctorEntry `json:"copilot_cli"` |
| 599 | + CopilotVersion string `json:"copilot_version"` |
| 600 | + SessionCount int `json:"session_count"` |
| 601 | + Workspaces workspaceReport `json:"workspaces"` |
| 602 | +} |
| 603 | + |
| 604 | +// workspaceReport summarizes whether stored session working directories still |
| 605 | +// exist on disk. |
| 606 | +type workspaceReport struct { |
| 607 | + Total int `json:"total"` |
| 608 | + Missing int `json:"missing"` |
| 609 | + Samples []string `json:"samples,omitempty"` |
| 610 | + Error string `json:"error,omitempty"` |
600 | 611 | } |
601 | 612 |
|
602 | 613 | // collectDoctorReport gathers the environment diagnostics once so they can be |
@@ -633,6 +644,7 @@ func collectDoctorReport() doctorReport { |
633 | 644 | } |
634 | 645 |
|
635 | 646 | r.SessionCount = doctorSessionCountFn() |
| 647 | + r.Workspaces = doctorWorkspacesFn() |
636 | 648 | r.OK = doctorEntryOK(r.Config) && |
637 | 649 | doctorEntryOK(r.SessionStore) && |
638 | 650 | doctorEntryOK(r.SessionState) && |
@@ -712,6 +724,35 @@ func defaultSessionCount() int { |
712 | 724 | return n |
713 | 725 | } |
714 | 726 |
|
| 727 | +func defaultDoctorWorkspaces() workspaceReport { |
| 728 | + store, err := data.Open() |
| 729 | + if err != nil { |
| 730 | + return workspaceReport{Error: err.Error()} |
| 731 | + } |
| 732 | + defer store.Close() //nolint:errcheck // read-only, best-effort close |
| 733 | + |
| 734 | + folders, err := store.ListFolders(context.Background()) |
| 735 | + if err != nil { |
| 736 | + return workspaceReport{Error: err.Error()} |
| 737 | + } |
| 738 | + |
| 739 | + r := workspaceReport{Total: len(folders)} |
| 740 | + for _, folder := range folders { |
| 741 | + if strings.TrimSpace(folder) == "" { |
| 742 | + continue |
| 743 | + } |
| 744 | + if _, err := os.Stat(folder); err != nil { |
| 745 | + if errors.Is(err, os.ErrNotExist) { |
| 746 | + r.Missing++ |
| 747 | + if len(r.Samples) < 5 { |
| 748 | + r.Samples = append(r.Samples, folder) |
| 749 | + } |
| 750 | + } |
| 751 | + } |
| 752 | + } |
| 753 | + return r |
| 754 | +} |
| 755 | + |
715 | 756 | // pathStatus stats a path and reports whether it is found, missing, or the |
716 | 757 | // wrong type (a file where a directory is expected, or vice versa). |
717 | 758 | func pathStatus(path string, wantDir bool) string { |
@@ -748,6 +789,7 @@ func runDoctor(w io.Writer) doctorReport { |
748 | 789 | fmt.Fprintf(w, "Copilot CLI version: not detected\n") |
749 | 790 | } |
750 | 791 | fmt.Fprintf(w, "Stored sessions: %d\n", r.SessionCount) |
| 792 | + writeWorkspaceLine(w, r.Workspaces) |
751 | 793 | fmt.Fprintf(w, "OK: %t\n", r.OK) |
752 | 794 | return r |
753 | 795 | } |
@@ -791,6 +833,17 @@ func writeDoctorLine(w io.Writer, label string, e doctorEntry, wantDir bool) { |
791 | 833 | } |
792 | 834 | } |
793 | 835 |
|
| 836 | +func writeWorkspaceLine(w io.Writer, r workspaceReport) { |
| 837 | + if r.Error != "" { |
| 838 | + fmt.Fprintf(w, "Missing workspaces: unknown (%s)\n", r.Error) |
| 839 | + return |
| 840 | + } |
| 841 | + fmt.Fprintf(w, "Missing workspaces: %d of %d folders\n", r.Missing, r.Total) |
| 842 | + for _, sample := range r.Samples { |
| 843 | + fmt.Fprintf(w, " - %s\n", sample) |
| 844 | + } |
| 845 | +} |
| 846 | + |
794 | 847 | // setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and |
795 | 848 | // redirects stderr to it. When no log file is configured, stderr is sent to |
796 | 849 | // os.DevNull to keep Bubble Tea's alt-screen clean. Returns the writer for |
|
0 commit comments