feat(dep): add --blocked-by and --depends-on flag aliases (bd-09kt)
Add flag-based alternatives to the positional argument for `bd dep add`: - `--blocked-by <id>`: Specify the blocking issue via flag - `--depends-on <id>`: Alias for --blocked-by This reduces token waste when Claude guesses flag-based syntax, which is a common pattern. Previously, Claude would attempt commands like: bd dep add issue-123 --blocked-by issue-456 This would fail with "unknown flag" and require retry. Now both: bd dep add issue-123 issue-456 bd dep add issue-123 --blocked-by issue-456 work identically. Closes GH#888 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,14 @@ var depAddCmd = &cobra.Command{
|
|||||||
Short: "Add a dependency",
|
Short: "Add a dependency",
|
||||||
Long: `Add a dependency between two issues.
|
Long: `Add a dependency between two issues.
|
||||||
|
|
||||||
|
The depends-on-id can be provided as:
|
||||||
|
- A positional argument: bd dep add issue-123 issue-456
|
||||||
|
- A flag: bd dep add issue-123 --blocked-by issue-456
|
||||||
|
- A flag: bd dep add issue-123 --depends-on issue-456
|
||||||
|
|
||||||
|
The --blocked-by and --depends-on flags are aliases and both mean "issue-123
|
||||||
|
depends on (is blocked by) the specified issue."
|
||||||
|
|
||||||
The depends-on-id can be:
|
The depends-on-id can be:
|
||||||
- A local issue ID (e.g., bd-xyz)
|
- A local issue ID (e.g., bd-xyz)
|
||||||
- An external reference: external:<project>:<capability>
|
- An external reference: external:<project>:<capability>
|
||||||
@@ -62,20 +70,55 @@ the external_projects config. They block the issue until the capability
|
|||||||
is "shipped" in the target project.
|
is "shipped" in the target project.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bd dep add bd-42 bd-41 # Local dependency
|
bd dep add bd-42 bd-41 # Positional args
|
||||||
|
bd dep add bd-42 --blocked-by bd-41 # Flag syntax (same effect)
|
||||||
|
bd dep add bd-42 --depends-on bd-41 # Alias (same effect)
|
||||||
bd dep add gt-xyz external:beads:mol-run-assignee # Cross-project dependency`,
|
bd dep add gt-xyz external:beads:mol-run-assignee # Cross-project dependency`,
|
||||||
Args: cobra.ExactArgs(2),
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
blockedBy, _ := cmd.Flags().GetString("blocked-by")
|
||||||
|
dependsOn, _ := cmd.Flags().GetString("depends-on")
|
||||||
|
hasFlag := blockedBy != "" || dependsOn != ""
|
||||||
|
|
||||||
|
if hasFlag {
|
||||||
|
// If a flag is provided, we only need 1 positional arg (the dependent issue)
|
||||||
|
if len(args) < 1 {
|
||||||
|
return fmt.Errorf("requires at least 1 arg(s), only received %d", len(args))
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
return fmt.Errorf("cannot use both positional depends-on-id and --blocked-by/--depends-on flag")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// No flag provided, need exactly 2 positional args
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("requires 2 arg(s), only received %d (or use --blocked-by/--depends-on flag)", len(args))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
CheckReadonly("dep add")
|
CheckReadonly("dep add")
|
||||||
depType, _ := cmd.Flags().GetString("type")
|
depType, _ := cmd.Flags().GetString("type")
|
||||||
|
|
||||||
|
// Get the dependency target from flag or positional arg
|
||||||
|
blockedBy, _ := cmd.Flags().GetString("blocked-by")
|
||||||
|
dependsOn, _ := cmd.Flags().GetString("depends-on")
|
||||||
|
|
||||||
|
var dependsOnArg string
|
||||||
|
if blockedBy != "" {
|
||||||
|
dependsOnArg = blockedBy
|
||||||
|
} else if dependsOn != "" {
|
||||||
|
dependsOnArg = dependsOn
|
||||||
|
} else {
|
||||||
|
dependsOnArg = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
|
|
||||||
// Resolve partial IDs first
|
// Resolve partial IDs first
|
||||||
var fromID, toID string
|
var fromID, toID string
|
||||||
|
|
||||||
// Check if toID is an external reference (don't resolve it)
|
// Check if toID is an external reference (don't resolve it)
|
||||||
isExternalRef := strings.HasPrefix(args[1], "external:")
|
isExternalRef := strings.HasPrefix(dependsOnArg, "external:")
|
||||||
|
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
||||||
@@ -89,22 +132,22 @@ Examples:
|
|||||||
|
|
||||||
if isExternalRef {
|
if isExternalRef {
|
||||||
// External references are stored as-is
|
// External references are stored as-is
|
||||||
toID = args[1]
|
toID = dependsOnArg
|
||||||
// Validate format: external:<project>:<capability>
|
// Validate format: external:<project>:<capability>
|
||||||
if err := validateExternalRef(toID); err != nil {
|
if err := validateExternalRef(toID); err != nil {
|
||||||
FatalErrorRespectJSON("%v", err)
|
FatalErrorRespectJSON("%v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
|
resolveArgs = &rpc.ResolveIDArgs{ID: dependsOnArg}
|
||||||
resp, err = daemonClient.ResolveID(resolveArgs)
|
resp, err = daemonClient.ResolveID(resolveArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Resolution failed - try auto-converting to external ref
|
// Resolution failed - try auto-converting to external ref
|
||||||
beadsDir := getBeadsDir()
|
beadsDir := getBeadsDir()
|
||||||
if extRef := routing.ResolveToExternalRef(args[1], beadsDir); extRef != "" {
|
if extRef := routing.ResolveToExternalRef(dependsOnArg, beadsDir); extRef != "" {
|
||||||
toID = extRef
|
toID = extRef
|
||||||
isExternalRef = true
|
isExternalRef = true
|
||||||
} else {
|
} else {
|
||||||
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
|
FatalErrorRespectJSON("resolving dependency ID %s: %v", dependsOnArg, err)
|
||||||
}
|
}
|
||||||
} else if err := json.Unmarshal(resp.Data, &toID); err != nil {
|
} else if err := json.Unmarshal(resp.Data, &toID); err != nil {
|
||||||
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
|
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
|
||||||
@@ -119,21 +162,21 @@ Examples:
|
|||||||
|
|
||||||
if isExternalRef {
|
if isExternalRef {
|
||||||
// External references are stored as-is
|
// External references are stored as-is
|
||||||
toID = args[1]
|
toID = dependsOnArg
|
||||||
// Validate format: external:<project>:<capability>
|
// Validate format: external:<project>:<capability>
|
||||||
if err := validateExternalRef(toID); err != nil {
|
if err := validateExternalRef(toID); err != nil {
|
||||||
FatalErrorRespectJSON("%v", err)
|
FatalErrorRespectJSON("%v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toID, err = utils.ResolvePartialID(ctx, store, args[1])
|
toID, err = utils.ResolvePartialID(ctx, store, dependsOnArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Resolution failed - try auto-converting to external ref
|
// Resolution failed - try auto-converting to external ref
|
||||||
beadsDir := getBeadsDir()
|
beadsDir := getBeadsDir()
|
||||||
if extRef := routing.ResolveToExternalRef(args[1], beadsDir); extRef != "" {
|
if extRef := routing.ResolveToExternalRef(dependsOnArg, beadsDir); extRef != "" {
|
||||||
toID = extRef
|
toID = extRef
|
||||||
isExternalRef = true
|
isExternalRef = true
|
||||||
} else {
|
} else {
|
||||||
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
|
FatalErrorRespectJSON("resolving dependency ID %s: %v", dependsOnArg, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +207,7 @@ Examples:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Added dependency: %s depends on %s (%s)\n",
|
fmt.Printf("%s Added dependency: %s depends on %s (%s)\n",
|
||||||
ui.RenderPass("✓"), args[0], args[1], depType)
|
ui.RenderPass("✓"), args[0], dependsOnArg, depType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,8 +1018,8 @@ func ParseExternalRef(ref string) (project, capability string) {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|tracks|related|parent-child|discovered-from|until|caused-by|validates|relates-to|supersedes)")
|
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|tracks|related|parent-child|discovered-from|until|caused-by|validates|relates-to|supersedes)")
|
||||||
// Note: --json flag is defined as a persistent flag in main.go, not here
|
depAddCmd.Flags().String("blocked-by", "", "Issue ID that blocks the first issue (alternative to positional arg)")
|
||||||
|
depAddCmd.Flags().String("depends-on", "", "Issue ID that the first issue depends on (alias for --blocked-by)")
|
||||||
// Note: --json flag is defined as a persistent flag in main.go, not here
|
// Note: --json flag is defined as a persistent flag in main.go, not here
|
||||||
|
|
||||||
depTreeCmd.Flags().Bool("show-all-paths", false, "Show all paths to nodes (no deduplication for diamond dependencies)")
|
depTreeCmd.Flags().Bool("show-all-paths", false, "Show all paths to nodes (no deduplication for diamond dependencies)")
|
||||||
|
|||||||
@@ -250,6 +250,35 @@ func TestDepCommandsInit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDepAddFlagAliases(t *testing.T) {
|
||||||
|
// Test that --blocked-by flag exists on depAddCmd
|
||||||
|
blockedByFlag := depAddCmd.Flags().Lookup("blocked-by")
|
||||||
|
if blockedByFlag == nil {
|
||||||
|
t.Fatal("depAddCmd should have --blocked-by flag")
|
||||||
|
}
|
||||||
|
if blockedByFlag.DefValue != "" {
|
||||||
|
t.Errorf("Expected default blocked-by='', got %q", blockedByFlag.DefValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that --depends-on flag exists on depAddCmd
|
||||||
|
dependsOnFlag := depAddCmd.Flags().Lookup("depends-on")
|
||||||
|
if dependsOnFlag == nil {
|
||||||
|
t.Fatal("depAddCmd should have --depends-on flag")
|
||||||
|
}
|
||||||
|
if dependsOnFlag.DefValue != "" {
|
||||||
|
t.Errorf("Expected default depends-on='', got %q", dependsOnFlag.DefValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the help text mentions the flags
|
||||||
|
longDesc := depAddCmd.Long
|
||||||
|
if !strings.Contains(longDesc, "--blocked-by") {
|
||||||
|
t.Error("Expected Long description to mention --blocked-by flag")
|
||||||
|
}
|
||||||
|
if !strings.Contains(longDesc, "--depends-on") {
|
||||||
|
t.Error("Expected Long description to mention --depends-on flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDepTreeFormatFlag(t *testing.T) {
|
func TestDepTreeFormatFlag(t *testing.T) {
|
||||||
// Test that the --format flag exists on depTreeCmd
|
// Test that the --format flag exists on depTreeCmd
|
||||||
flag := depTreeCmd.Flags().Lookup("format")
|
flag := depTreeCmd.Flags().Lookup("format")
|
||||||
|
|||||||
Reference in New Issue
Block a user