package main import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/types" ) var infoCmd = &cobra.Command{ Use: "info", Short: "Show database and daemon information", Long: `Display information about the current database path and daemon status. This command helps debug issues where bd is using an unexpected database or daemon connection. It shows: - The absolute path to the database file - Daemon connection status (daemon or direct mode) - If using daemon: socket path, health status, version - Database statistics (issue count) - Schema information (with --schema flag) - What's new in recent versions (with --whats-new flag) Examples: bd info bd info --json bd info --schema --json bd info --whats-new bd info --whats-new --json`, Run: func(cmd *cobra.Command, args []string) { schemaFlag, _ := cmd.Flags().GetBool("schema") whatsNewFlag, _ := cmd.Flags().GetBool("whats-new") // Handle --whats-new flag if whatsNewFlag { showWhatsNew() return } // Get database path (absolute) absDBPath, err := filepath.Abs(dbPath) if err != nil { absDBPath = dbPath } // Build info structure info := map[string]interface{}{ "database_path": absDBPath, "mode": daemonStatus.Mode, } // Add daemon details if connected if daemonClient != nil { info["daemon_connected"] = true info["socket_path"] = daemonStatus.SocketPath // Get daemon health health, err := daemonClient.Health() if err == nil { info["daemon_version"] = health.Version info["daemon_status"] = health.Status info["daemon_compatible"] = health.Compatible info["daemon_uptime"] = health.Uptime } // Get issue count from daemon resp, err := daemonClient.Stats() if err == nil { var stats types.Statistics if jsonErr := json.Unmarshal(resp.Data, &stats); jsonErr == nil { info["issue_count"] = stats.TotalIssues } } } else { // Direct mode info["daemon_connected"] = false if daemonStatus.FallbackReason != "" && daemonStatus.FallbackReason != FallbackNone { info["daemon_fallback_reason"] = daemonStatus.FallbackReason } if daemonStatus.Detail != "" { info["daemon_detail"] = daemonStatus.Detail } // Get issue count from direct store if store != nil { ctx := rootCtx // Check database freshness before reading (bd-2q6d, bd-c4rq) // Skip check when using daemon (daemon auto-imports on staleness) if daemonClient == nil { if err := ensureDatabaseFresh(ctx); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } filter := types.IssueFilter{} issues, err := store.SearchIssues(ctx, "", filter) if err == nil { info["issue_count"] = len(issues) } } } // Add config to info output (requires direct mode to access config table) // Save current daemon state wasDaemon := daemonClient != nil var tempErr error if wasDaemon { // Temporarily switch to direct mode to read config tempErr = ensureDirectMode("info: reading config") } if store != nil { ctx := rootCtx configMap, err := store.GetAllConfig(ctx) if err == nil && len(configMap) > 0 { info["config"] = configMap } } // Note: We don't restore daemon mode since info is a read-only command // and the process will exit immediately after this _ = tempErr // silence unused warning // Add schema information if requested if schemaFlag && store != nil { ctx := rootCtx // Get schema version schemaVersion, err := store.GetMetadata(ctx, "bd_version") if err != nil { schemaVersion = "unknown" } // Get tables tables := []string{"issues", "dependencies", "labels", "config", "metadata"} // Get config configMap := make(map[string]string) prefix, _ := store.GetConfig(ctx, "issue_prefix") if prefix != "" { configMap["issue_prefix"] = prefix } // Get sample issue IDs filter := types.IssueFilter{} issues, err := store.SearchIssues(ctx, "", filter) sampleIDs := []string{} detectedPrefix := "" if err == nil && len(issues) > 0 { // Get first 3 issue IDs as samples maxSamples := 3 if len(issues) < maxSamples { maxSamples = len(issues) } for i := 0; i < maxSamples; i++ { sampleIDs = append(sampleIDs, issues[i].ID) } // Detect prefix from first issue if len(issues) > 0 { detectedPrefix = extractPrefix(issues[0].ID) } } info["schema"] = map[string]interface{}{ "tables": tables, "schema_version": schemaVersion, "config": configMap, "sample_issue_ids": sampleIDs, "detected_prefix": detectedPrefix, } } // JSON output if jsonOutput { outputJSON(info) return } // Human-readable output fmt.Println("\nBeads Database Information") fmt.Println("===========================") fmt.Printf("Database: %s\n", absDBPath) fmt.Printf("Mode: %s\n", daemonStatus.Mode) if daemonClient != nil { fmt.Println("\nDaemon Status:") fmt.Printf(" Connected: yes\n") fmt.Printf(" Socket: %s\n", daemonStatus.SocketPath) health, err := daemonClient.Health() if err == nil { fmt.Printf(" Version: %s\n", health.Version) fmt.Printf(" Health: %s\n", health.Status) if health.Compatible { fmt.Printf(" Compatible: āœ“ yes\n") } else { fmt.Printf(" Compatible: āœ— no (restart recommended)\n") } fmt.Printf(" Uptime: %.1fs\n", health.Uptime) } } else { fmt.Println("\nDaemon Status:") fmt.Printf(" Connected: no\n") if daemonStatus.FallbackReason != "" && daemonStatus.FallbackReason != FallbackNone { fmt.Printf(" Reason: %s\n", daemonStatus.FallbackReason) } if daemonStatus.Detail != "" { fmt.Printf(" Detail: %s\n", daemonStatus.Detail) } } // Show issue count if count, ok := info["issue_count"].(int); ok { fmt.Printf("\nIssue Count: %d\n", count) } // Show schema information if requested if schemaFlag { if schemaInfo, ok := info["schema"].(map[string]interface{}); ok { fmt.Println("\nSchema Information:") fmt.Printf(" Tables: %v\n", schemaInfo["tables"]) if version, ok := schemaInfo["schema_version"].(string); ok { fmt.Printf(" Schema Version: %s\n", version) } if prefix, ok := schemaInfo["detected_prefix"].(string); ok && prefix != "" { fmt.Printf(" Detected Prefix: %s\n", prefix) } if samples, ok := schemaInfo["sample_issue_ids"].([]string); ok && len(samples) > 0 { fmt.Printf(" Sample Issues: %v\n", samples) } } } // Check git hooks status hookStatuses := CheckGitHooks() if warning := FormatHookWarnings(hookStatuses); warning != "" { fmt.Printf("\n%s\n", warning) } fmt.Println() }, } // extractPrefix extracts the prefix from an issue ID (e.g., "bd-123" -> "bd") // Only considers the first hyphen, so "vc-baseline-test" -> "vc" func extractPrefix(issueID string) string { idx := strings.Index(issueID, "-") if idx <= 0 { return "" } return issueID[:idx] } // VersionChange represents agent-relevant changes for a specific version type VersionChange struct { Version string `json:"version"` Date string `json:"date"` Changes []string `json:"changes"` } // versionChanges contains agent-actionable changes for recent versions var versionChanges = []VersionChange{ { Version: "0.23.0", Date: "2025-11-08", Changes: []string{ "Agent Mail integration - Python adapter library with 98.5% reduction in git traffic", "`bd info --whats-new` - Quick upgrade summaries for agents (shows last 3 versions)", "`bd hooks install` - Embedded git hooks command (replaces external script)", "`bd cleanup` - Bulk deletion for agent-driven compaction", "`bd new` alias added - Agents often tried this instead of `bd create`", "`bd list` now one-line-per-issue by default - Prevents agent miscounting (use --long for old format)", "3-way JSONL merge auto-invoked on conflicts - No manual intervention needed", "Daemon crash recovery - Panic handler with socket cleanup prevents orphaned processes", "Auto-import when database missing - `bd import` now auto-initializes", "Stale database export prevention - ID-based staleness detection", }, }, { Version: "0.22.1", Date: "2025-11-06", Changes: []string{ "Native `bd merge` command vendored from beads-merge - no external binary needed", "`bd info` detects outdated git hooks - warns if version mismatch", "Multi-workspace deletion tracking fixed - deletions now propagate correctly", "Hash ID recognition improved - recognizes Base36 IDs without a-f letters", "Import/export deadlock fixed - no hanging when daemon running", }, }, { Version: "0.22.0", Date: "2025-11-05", Changes: []string{ "Intelligent merge driver auto-configured - eliminates most JSONL conflicts", "Onboarding wizards: `bd init --contributor` and `bd init --team`", "New `bd migrate-issues` command - migrate issues between repos with dependencies", "`bd show` displays blocker status - 'Blocked by N open issues' or 'Ready to work'", "SearchIssues N+1 query fixed - batch-loads labels for better performance", "Sync validation prevents infinite dirty loop - verifies JSONL export", }, }, { Version: "0.21.0", Date: "2025-11-04", Changes: []string{ "Hash-based IDs eliminate collisions - remove ID coordination workarounds", "Event-driven daemon mode (opt-in) - set BEADS_DAEMON_MODE=events", "Agent Mail integration - real-time multi-agent coordination (<100ms latency)", "`bd duplicates --auto-merge` - automated duplicate detection and merging", "Hierarchical children for epics - dotted IDs (bd-abc.1, bd-abc.2) up to 3 levels", "`--discovered-from` inline syntax - create with dependency in one command", }, }, } // showWhatsNew displays agent-relevant changes from recent versions func showWhatsNew() { currentVersion := Version // from version.go if jsonOutput { outputJSON(map[string]interface{}{ "current_version": currentVersion, "recent_changes": versionChanges, }) return } // Human-readable output fmt.Printf("\nšŸ†• What's New in bd (Current: v%s)\n", currentVersion) fmt.Println("=" + strings.Repeat("=", 60)) fmt.Println() for _, vc := range versionChanges { // Highlight if this is the current version versionMarker := "" if vc.Version == currentVersion { versionMarker = " ← current" } fmt.Printf("## v%s (%s)%s\n\n", vc.Version, vc.Date, versionMarker) for _, change := range vc.Changes { fmt.Printf(" • %s\n", change) } fmt.Println() } fmt.Println("šŸ’” Tip: Use `bd info --whats-new --json` for machine-readable output") fmt.Println() } func init() { infoCmd.Flags().Bool("schema", false, "Include schema information in output") infoCmd.Flags().Bool("whats-new", false, "Show agent-relevant changes from recent versions") infoCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output in JSON format") rootCmd.AddCommand(infoCmd) }