feat(doctor): add --output flag to export diagnostics (bd-9cc)

Add ability to save doctor diagnostics to a JSON file for historical
analysis and bug reporting. The export includes timestamp and platform
info (OS, Go version, SQLite version) for tracking intermittent issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-03 11:41:00 -08:00
parent e1e3427d9b
commit e5de1db585
2 changed files with 57 additions and 10 deletions

View File

@@ -41,16 +41,19 @@ type doctorCheck struct {
} }
type doctorResult struct { type doctorResult struct {
Path string `json:"path"` Path string `json:"path"`
Checks []doctorCheck `json:"checks"` Checks []doctorCheck `json:"checks"`
OverallOK bool `json:"overall_ok"` OverallOK bool `json:"overall_ok"`
CLIVersion string `json:"cli_version"` CLIVersion string `json:"cli_version"`
Timestamp string `json:"timestamp,omitempty"` // bd-9cc: ISO8601 timestamp for historical tracking
Platform map[string]string `json:"platform,omitempty"` // bd-9cc: platform info for debugging
} }
var ( var (
doctorFix bool doctorFix bool
doctorYes bool doctorYes bool
doctorDryRun bool // bd-a5z: preview fixes without applying doctorDryRun bool // bd-a5z: preview fixes without applying
doctorOutput string // bd-9cc: export diagnostics to file
perfMode bool perfMode bool
checkHealthMode bool checkHealthMode bool
) )
@@ -86,6 +89,10 @@ Performance Mode (--perf):
- Generates CPU profile for analysis - Generates CPU profile for analysis
- Outputs shareable report for bug reports - Outputs shareable report for bug reports
Export Mode (--output):
Save diagnostics to a JSON file for historical analysis and bug reporting.
Includes timestamp and platform info for tracking intermittent issues.
Examples: Examples:
bd doctor # Check current directory bd doctor # Check current directory
bd doctor /path/to/repo # Check specific repository bd doctor /path/to/repo # Check specific repository
@@ -93,7 +100,8 @@ Examples:
bd doctor --fix # Automatically fix issues (with confirmation) bd doctor --fix # Automatically fix issues (with confirmation)
bd doctor --fix --yes # Automatically fix issues (no confirmation) bd doctor --fix --yes # Automatically fix issues (no confirmation)
bd doctor --dry-run # Preview what --fix would do without making changes bd doctor --dry-run # Preview what --fix would do without making changes
bd doctor --perf # Performance diagnostics`, bd doctor --perf # Performance diagnostics
bd doctor --output diagnostics.json # Export diagnostics to file`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Use global jsonOutput set by PersistentPreRun // Use global jsonOutput set by PersistentPreRun
@@ -134,10 +142,26 @@ Examples:
result = runDiagnostics(absPath) result = runDiagnostics(absPath)
} }
// bd-9cc: Add timestamp and platform info for export
if doctorOutput != "" || jsonOutput {
result.Timestamp = time.Now().UTC().Format(time.RFC3339)
result.Platform = doctor.CollectPlatformInfo(absPath)
}
// bd-9cc: Export to file if --output specified
if doctorOutput != "" {
if err := exportDiagnostics(result, doctorOutput); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to export diagnostics: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Diagnostics exported to %s\n", doctorOutput)
}
// Output results // Output results
if jsonOutput { if jsonOutput {
outputJSON(result) outputJSON(result)
} else { } else if doctorOutput == "" {
// Only print to console if not exporting (to avoid duplicate output)
printDiagnostics(result) printDiagnostics(result)
} }
@@ -1166,6 +1190,24 @@ func fetchLatestGitHubRelease() (string, error) {
return version, nil return version, nil
} }
// exportDiagnostics writes the doctor result to a JSON file (bd-9cc)
func exportDiagnostics(result doctorResult, outputPath string) error {
// #nosec G304 - outputPath is a user-provided flag value for file generation
f, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
if err := encoder.Encode(result); err != nil {
return fmt.Errorf("failed to write JSON: %w", err)
}
return nil
}
func printDiagnostics(result doctorResult) { func printDiagnostics(result doctorResult) {
// Print header // Print header
fmt.Println("\nDiagnostics") fmt.Println("\nDiagnostics")
@@ -2590,4 +2632,5 @@ func init() {
rootCmd.AddCommand(doctorCmd) rootCmd.AddCommand(doctorCmd)
doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile") doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile")
doctorCmd.Flags().BoolVar(&checkHealthMode, "check-health", false, "Quick health check for git hooks (silent on success)") doctorCmd.Flags().BoolVar(&checkHealthMode, "check-health", false, "Quick health check for git hooks (silent on success)")
doctorCmd.Flags().StringVarP(&doctorOutput, "output", "o", "", "Export diagnostics to JSON file (bd-9cc)")
} }

View File

@@ -36,7 +36,7 @@ func RunPerformanceDiagnostics(path string) {
} }
// Collect platform info // Collect platform info
platformInfo := collectPlatformInfo(dbPath) platformInfo := CollectPlatformInfo(path)
fmt.Printf("\nPlatform: %s\n", platformInfo["os_arch"]) fmt.Printf("\nPlatform: %s\n", platformInfo["os_arch"])
fmt.Printf("Go: %s\n", platformInfo["go_version"]) fmt.Printf("Go: %s\n", platformInfo["go_version"])
fmt.Printf("SQLite: %s\n", platformInfo["sqlite_version"]) fmt.Printf("SQLite: %s\n", platformInfo["sqlite_version"])
@@ -95,7 +95,9 @@ func RunPerformanceDiagnostics(path string) {
fmt.Printf(" go tool pprof -http=:8080 %s\n\n", profilePath) fmt.Printf(" go tool pprof -http=:8080 %s\n\n", profilePath)
} }
func collectPlatformInfo(dbPath string) map[string]string { // CollectPlatformInfo gathers platform information for diagnostics.
// bd-9cc: Exported for use by --output flag.
func CollectPlatformInfo(path string) map[string]string {
info := make(map[string]string) info := make(map[string]string)
// OS and architecture // OS and architecture
@@ -104,7 +106,9 @@ func collectPlatformInfo(dbPath string) map[string]string {
// Go version // Go version
info["go_version"] = runtime.Version() info["go_version"] = runtime.Version()
// SQLite version // SQLite version - try to find database
beadsDir := filepath.Join(path, ".beads")
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro") db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro")
if err == nil { if err == nil {
defer db.Close() defer db.Close()