From aa3c4cb3eb287cd2e008f0347a37defa1d837401 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 27 Dec 2025 21:27:44 -0800 Subject: [PATCH] feat: add 'bd where' command to show active beads location (bd-8x43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new command that shows the active .beads directory path, including redirect information for debugging. Supports --json output. Example: bd where → /Users/stevey/gt/beads/mayor/rig/.beads (via redirect from /Users/stevey/gt/beads/crew/emma/.beads) prefix: bd database: /Users/stevey/gt/beads/mayor/rig/.beads/beads.db 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/where.go | 173 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 cmd/bd/where.go diff --git a/cmd/bd/where.go b/cmd/bd/where.go new file mode 100644 index 00000000..9ad567b0 --- /dev/null +++ b/cmd/bd/where.go @@ -0,0 +1,173 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/steveyegge/beads/internal/beads" + "github.com/steveyegge/beads/internal/utils" +) + +// WhereResult contains information about the active beads location +type WhereResult struct { + Path string `json:"path"` // Active .beads directory path + RedirectedFrom string `json:"redirected_from,omitempty"` // Original path if redirected + Prefix string `json:"prefix,omitempty"` // Issue prefix (if detectable) + DatabasePath string `json:"database_path,omitempty"` // Full path to database file +} + +var whereCmd = &cobra.Command{ + Use: "where", + GroupID: "setup", + Short: "Show active beads location", + Long: `Show the active beads database location, including redirect information. + +This command is useful for debugging when using redirects, to understand +which .beads directory is actually being used. + +Examples: + bd where # Show active beads location + bd where --json # Output in JSON format +`, + Run: func(cmd *cobra.Command, args []string) { + result := WhereResult{} + + // Find the beads directory (this follows redirects) + beadsDir := beads.FindBeadsDir() + if beadsDir == "" { + if jsonOutput { + outputJSON(map[string]string{"error": "no beads directory found"}) + } else { + fmt.Fprintln(os.Stderr, "Error: no beads directory found") + fmt.Fprintln(os.Stderr, "Hint: run 'bd init' to create a database in the current directory") + } + os.Exit(1) + } + + result.Path = beadsDir + + // Check if we got here via redirect by looking for the original .beads directory + // Walk up from cwd to find any .beads with a redirect file + originalBeadsDir := findOriginalBeadsDir() + if originalBeadsDir != "" && originalBeadsDir != beadsDir { + result.RedirectedFrom = originalBeadsDir + } + + // Find the database path + dbPath := beads.FindDatabasePath() + if dbPath != "" { + result.DatabasePath = dbPath + + // Try to get the prefix from the database if we have a store + if store != nil { + ctx := rootCtx + if prefix, err := store.GetConfig(ctx, "issue_prefix"); err == nil && prefix != "" { + result.Prefix = prefix + } + } + } + + // If we don't have the prefix from DB, try to detect it from JSONL + if result.Prefix == "" { + result.Prefix = detectPrefixFromDir(beadsDir) + } + + // Output results + if jsonOutput { + outputJSON(result) + } else { + fmt.Println(result.Path) + if result.RedirectedFrom != "" { + fmt.Printf(" (via redirect from %s)\n", result.RedirectedFrom) + } + if result.Prefix != "" { + fmt.Printf(" prefix: %s\n", result.Prefix) + } + if result.DatabasePath != "" { + fmt.Printf(" database: %s\n", result.DatabasePath) + } + } + }, +} + +// findOriginalBeadsDir walks up from cwd looking for a .beads directory with a redirect file +// Returns the original .beads path if found, empty string otherwise +func findOriginalBeadsDir() string { + cwd, err := os.Getwd() + if err != nil { + return "" + } + + // Canonicalize cwd to handle symlinks + if resolved, err := filepath.EvalSymlinks(cwd); err == nil { + cwd = resolved + } + + // Check BEADS_DIR first + if envDir := os.Getenv("BEADS_DIR"); envDir != "" { + envDir = utils.CanonicalizePath(envDir) + redirectFile := filepath.Join(envDir, beads.RedirectFileName) + if _, err := os.Stat(redirectFile); err == nil { + return envDir + } + return "" + } + + // Walk up directory tree looking for .beads with redirect + for dir := cwd; dir != "/" && dir != "."; dir = filepath.Dir(dir) { + beadsDir := filepath.Join(dir, ".beads") + if info, err := os.Stat(beadsDir); err == nil && info.IsDir() { + redirectFile := filepath.Join(beadsDir, beads.RedirectFileName) + if _, err := os.Stat(redirectFile); err == nil { + return beadsDir + } + // Found .beads without redirect - this is the actual location + return "" + } + } + + return "" +} + +// detectPrefixFromDir tries to detect the issue prefix from files in the beads directory +func detectPrefixFromDir(beadsDir string) string { + // Try to read from issues.jsonl and extract prefix from first issue ID + jsonlPath := filepath.Join(beadsDir, "issues.jsonl") + // #nosec G304 -- jsonlPath is constructed from trusted beadsDir + data, err := os.ReadFile(jsonlPath) + if err != nil { + return "" + } + + // Find first line that looks like an issue + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Quick JSON parse to get ID + var issue struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(line), &issue); err != nil { + continue + } + + // Extract prefix from ID (e.g., "bd-123" -> "bd") + if idx := strings.LastIndex(issue.ID, "-"); idx > 0 { + return issue.ID[:idx] + } + } + + return "" +} + +func init() { + rootCmd.AddCommand(whereCmd) +}