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 <noreply@anthropic.com>
This commit is contained in:
beads/crew/emma
2026-01-23 21:14:59 -08:00
committed by Steve Yegge
parent 03400fbdbc
commit a9c8c952f6
2 changed files with 93 additions and 2 deletions

View File

@@ -16,19 +16,29 @@ import (
) )
var showCmd = &cobra.Command{ var showCmd = &cobra.Command{
Use: "show [id...]", Use: "show [id...] [--id=<id>...]",
Aliases: []string{"view"}, Aliases: []string{"view"},
GroupID: "issues", GroupID: "issues",
Short: "Show issue details", 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) { Run: func(cmd *cobra.Command, args []string) {
showThread, _ := cmd.Flags().GetBool("thread") showThread, _ := cmd.Flags().GetBool("thread")
shortMode, _ := cmd.Flags().GetBool("short") shortMode, _ := cmd.Flags().GetBool("short")
showRefs, _ := cmd.Flags().GetBool("refs") showRefs, _ := cmd.Flags().GetBool("refs")
showChildren, _ := cmd.Flags().GetBool("children") showChildren, _ := cmd.Flags().GetBool("children")
asOfRef, _ := cmd.Flags().GetString("as-of") asOfRef, _ := cmd.Flags().GetString("as-of")
idFlags, _ := cmd.Flags().GetStringArray("id")
ctx := rootCtx 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 // Handle --as-of flag: show issue at a specific point in history
if asOfRef != "" { if asOfRef != "" {
showIssueAsOf(ctx, args, asOfRef, shortMode) 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("refs", false, "Show issues that reference this issue (reverse lookup)")
showCmd.Flags().Bool("children", false, "Show only the children of this issue") 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().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 showCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(showCmd) rootCmd.AddCommand(showCmd)
} }

View File

@@ -114,3 +114,83 @@ func TestShow_NoExternalRef(t *testing.T) {
t.Errorf("expected no 'External:' line for issue without external ref, got: %s", out) 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")
}
}