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:
@@ -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)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user