From a9c8c952f6c07849e1046d0226d1e34957b8652c Mon Sep 17 00:00:00 2001 From: beads/crew/emma Date: Fri, 23 Jan 2026 21:14:59 -0800 Subject: [PATCH] feat(show): add --id flag for IDs that look like flags When an issue ID starts with dashes (e.g., gt--kzx), it can be misinterpreted by Cobra's argument parser. The new --id flag allows these IDs to be passed safely: bd show --id=gt--xyz Multiple --id flags can be used, and they can be combined with positional arguments. Closes: bd-ix0ak Co-Authored-By: Claude Opus 4.5 --- cmd/bd/show.go | 15 +++++++-- cmd/bd/show_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/cmd/bd/show.go b/cmd/bd/show.go index 8b8c915c..6cb326a9 100644 --- a/cmd/bd/show.go +++ b/cmd/bd/show.go @@ -16,19 +16,29 @@ import ( ) var showCmd = &cobra.Command{ - Use: "show [id...]", + Use: "show [id...] [--id=...]", Aliases: []string{"view"}, GroupID: "issues", Short: "Show issue details", - Args: cobra.MinimumNArgs(1), + Args: cobra.ArbitraryArgs, // Allow zero positional args when --id is used Run: func(cmd *cobra.Command, args []string) { showThread, _ := cmd.Flags().GetBool("thread") shortMode, _ := cmd.Flags().GetBool("short") showRefs, _ := cmd.Flags().GetBool("refs") showChildren, _ := cmd.Flags().GetBool("children") asOfRef, _ := cmd.Flags().GetString("as-of") + idFlags, _ := cmd.Flags().GetStringArray("id") ctx := rootCtx + // Merge --id flag values with positional args + // This allows IDs that look like flags (e.g., --xyz or gt--abc) to be passed safely + args = append(args, idFlags...) + + // Validate that at least one ID is provided + if len(args) == 0 { + FatalErrorRespectJSON("at least one issue ID is required (use positional args or --id flag)") + } + // Handle --as-of flag: show issue at a specific point in history if asOfRef != "" { showIssueAsOf(ctx, args, asOfRef, shortMode) @@ -1103,6 +1113,7 @@ func init() { showCmd.Flags().Bool("refs", false, "Show issues that reference this issue (reverse lookup)") showCmd.Flags().Bool("children", false, "Show only the children of this issue") showCmd.Flags().String("as-of", "", "Show issue as it existed at a specific commit hash or branch (requires Dolt)") + showCmd.Flags().StringArray("id", nil, "Issue ID (use for IDs that look like flags, e.g., --id=gt--xyz)") showCmd.ValidArgsFunction = issueIDCompletion rootCmd.AddCommand(showCmd) } diff --git a/cmd/bd/show_test.go b/cmd/bd/show_test.go index b4d93e43..4811893c 100644 --- a/cmd/bd/show_test.go +++ b/cmd/bd/show_test.go @@ -114,3 +114,83 @@ func TestShow_NoExternalRef(t *testing.T) { t.Errorf("expected no 'External:' line for issue without external ref, got: %s", out) } } + +func TestShow_IDFlag(t *testing.T) { + if testing.Short() { + t.Skip("skipping CLI test in short mode") + } + + // Build bd binary + tmpBin := filepath.Join(t.TempDir(), "bd") + buildCmd := exec.Command("go", "build", "-o", tmpBin, "./") + buildCmd.Dir = "." + if out, err := buildCmd.CombinedOutput(); err != nil { + t.Fatalf("failed to build bd: %v\n%s", err, out) + } + + tmpDir := t.TempDir() + + // Initialize beads + initCmd := exec.Command(tmpBin, "init", "--prefix", "test", "--quiet") + initCmd.Dir = tmpDir + if out, err := initCmd.CombinedOutput(); err != nil { + t.Fatalf("init failed: %v\n%s", err, out) + } + + // Create an issue + createCmd := exec.Command(tmpBin, "--no-daemon", "create", "ID flag test", "-p", "1", "--json", "--repo", ".") + createCmd.Dir = tmpDir + createOut, err := createCmd.CombinedOutput() + if err != nil { + t.Fatalf("create failed: %v\n%s", err, createOut) + } + + var issue map[string]interface{} + if err := json.Unmarshal(createOut, &issue); err != nil { + t.Fatalf("failed to parse create output: %v, output: %s", err, createOut) + } + id := issue["id"].(string) + + // Test 1: Using --id flag works + showCmd := exec.Command(tmpBin, "--no-daemon", "show", "--id="+id, "--short") + showCmd.Dir = tmpDir + showOut, err := showCmd.CombinedOutput() + if err != nil { + t.Fatalf("show with --id flag failed: %v\n%s", err, showOut) + } + if !strings.Contains(string(showOut), id) { + t.Errorf("expected issue ID in output, got: %s", showOut) + } + + // Test 2: Multiple --id flags work + showCmd2 := exec.Command(tmpBin, "--no-daemon", "show", "--id="+id, "--id="+id, "--short") + showCmd2.Dir = tmpDir + showOut2, err := showCmd2.CombinedOutput() + if err != nil { + t.Fatalf("show with multiple --id flags failed: %v\n%s", err, showOut2) + } + // Should see the ID twice (one for each --id flag) + if strings.Count(string(showOut2), id) != 2 { + t.Errorf("expected issue ID twice in output, got: %s", showOut2) + } + + // Test 3: Combining positional and --id flag + showCmd3 := exec.Command(tmpBin, "--no-daemon", "show", id, "--id="+id, "--short") + showCmd3.Dir = tmpDir + showOut3, err := showCmd3.CombinedOutput() + if err != nil { + t.Fatalf("show with positional + --id failed: %v\n%s", err, showOut3) + } + // Should see the ID twice + if strings.Count(string(showOut3), id) != 2 { + t.Errorf("expected issue ID twice in output, got: %s", showOut3) + } + + // Test 4: No args at all should fail + showCmd4 := exec.Command(tmpBin, "--no-daemon", "show") + showCmd4.Dir = tmpDir + _, err = showCmd4.CombinedOutput() + if err == nil { + t.Error("expected error when no ID provided, but command succeeded") + } +}