Add multi-ID support to update, show, and label commands
Implements GitHub issue #58: Allow multiple issue IDs for batch operations. Changes: - update: Now accepts multiple IDs for batch status/priority updates - show: Displays multiple issues with separators between them - label add/remove: Apply labels to multiple issues at once - All commands return arrays in JSON mode for consistency Commands already supporting multiple IDs: - close (already implemented) - reopen (already implemented) Updated AGENTS.md with correct multi-ID syntax examples. Amp-Thread-ID: https://ampcode.com/threads/T-518a7593-6e16-4b08-8cf8-741992b5e3b6 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
22
AGENTS.md
22
AGENTS.md
@@ -110,8 +110,9 @@ bd create "Issue title" -t bug -p 1 -l bug,critical --json
|
|||||||
# Create multiple issues from markdown file
|
# Create multiple issues from markdown file
|
||||||
bd create -f feature-plan.md --json
|
bd create -f feature-plan.md --json
|
||||||
|
|
||||||
# Update issue status
|
# Update one or more issues
|
||||||
bd update <id> --status in_progress --json
|
bd update <id> [<id>...] --status in_progress --json
|
||||||
|
bd update <id> [<id>...] --priority 1 --json
|
||||||
|
|
||||||
# Link discovered work (old way)
|
# Link discovered work (old way)
|
||||||
bd dep add <discovered-id> <parent-id> --type discovered-from
|
bd dep add <discovered-id> <parent-id> --type discovered-from
|
||||||
@@ -119,23 +120,26 @@ bd dep add <discovered-id> <parent-id> --type discovered-from
|
|||||||
# Create and link in one command (new way)
|
# Create and link in one command (new way)
|
||||||
bd create "Issue title" -t bug -p 1 --deps discovered-from:<parent-id> --json
|
bd create "Issue title" -t bug -p 1 --deps discovered-from:<parent-id> --json
|
||||||
|
|
||||||
# Label management
|
# Label management (supports multiple IDs)
|
||||||
bd label add <id> <label> --json
|
bd label add <id> [<id>...] <label> --json
|
||||||
bd label remove <id> <label> --json
|
bd label remove <id> [<id>...] <label> --json
|
||||||
bd label list <id> --json
|
bd label list <id> --json
|
||||||
bd label list-all --json
|
bd label list-all --json
|
||||||
|
|
||||||
# Filter issues by label
|
# Filter issues by label
|
||||||
bd list --label bug,critical --json
|
bd list --label bug,critical --json
|
||||||
|
|
||||||
# Complete work
|
# Complete work (supports multiple IDs)
|
||||||
bd close <id> --reason "Done" --json
|
bd close <id> [<id>...] --reason "Done" --json
|
||||||
|
|
||||||
|
# Reopen closed issues (supports multiple IDs)
|
||||||
|
bd reopen <id> [<id>...] --reason "Reopening" --json
|
||||||
|
|
||||||
# Show dependency tree
|
# Show dependency tree
|
||||||
bd dep tree <id>
|
bd dep tree <id>
|
||||||
|
|
||||||
# Get issue details
|
# Get issue details (supports multiple IDs)
|
||||||
bd show <id> --json
|
bd show <id> [<id>...] --json
|
||||||
|
|
||||||
# Rename issue prefix (e.g., from 'knowledge-work-' to 'kw-')
|
# Rename issue prefix (e.g., from 'knowledge-work-' to 'kw-')
|
||||||
bd rename-prefix kw- --dry-run # Preview changes
|
bd rename-prefix kw- --dry-run # Preview changes
|
||||||
|
|||||||
102
cmd/bd/label.go
102
cmd/bd/label.go
@@ -70,24 +70,102 @@ func executeLabelCommand(issueID, label, operation string, operationFunc func(co
|
|||||||
}
|
}
|
||||||
|
|
||||||
var labelAddCmd = &cobra.Command{
|
var labelAddCmd = &cobra.Command{
|
||||||
Use: "add [issue-id] [label]",
|
Use: "add [issue-id...] [label]",
|
||||||
Short: "Add a label to an issue",
|
Short: "Add a label to one or more issues",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
executeLabelCommand(args[0], args[1], "added", func(ctx context.Context, issueID, label, actor string) error {
|
// Last arg is the label, everything before is issue IDs
|
||||||
return store.AddLabel(ctx, issueID, label, actor)
|
label := args[len(args)-1]
|
||||||
})
|
issueIDs := args[:len(args)-1]
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
results := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, issueID := range issueIDs {
|
||||||
|
var err error
|
||||||
|
if daemonClient != nil {
|
||||||
|
_, err = daemonClient.AddLabel(&rpc.LabelAddArgs{
|
||||||
|
ID: issueID,
|
||||||
|
Label: label,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
err = store.AddLabel(ctx, issueID, label, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error adding label to %s: %v\n", issueID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
results = append(results, map[string]interface{}{
|
||||||
|
"status": "added",
|
||||||
|
"issue_id": issueID,
|
||||||
|
"label": label,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
|
fmt.Printf("%s Added label '%s' to %s\n", green("✓"), label, issueID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issueIDs) > 0 && daemonClient == nil {
|
||||||
|
markDirtyAndScheduleFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput && len(results) > 0 {
|
||||||
|
outputJSON(results)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelRemoveCmd = &cobra.Command{
|
var labelRemoveCmd = &cobra.Command{
|
||||||
Use: "remove [issue-id] [label]",
|
Use: "remove [issue-id...] [label]",
|
||||||
Short: "Remove a label from an issue",
|
Short: "Remove a label from one or more issues",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
executeLabelCommand(args[0], args[1], "removed", func(ctx context.Context, issueID, label, actor string) error {
|
// Last arg is the label, everything before is issue IDs
|
||||||
return store.RemoveLabel(ctx, issueID, label, actor)
|
label := args[len(args)-1]
|
||||||
})
|
issueIDs := args[:len(args)-1]
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
results := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, issueID := range issueIDs {
|
||||||
|
var err error
|
||||||
|
if daemonClient != nil {
|
||||||
|
_, err = daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{
|
||||||
|
ID: issueID,
|
||||||
|
Label: label,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
err = store.RemoveLabel(ctx, issueID, label, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error removing label from %s: %v\n", issueID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
results = append(results, map[string]interface{}{
|
||||||
|
"status": "removed",
|
||||||
|
"issue_id": issueID,
|
||||||
|
"label": label,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
|
fmt.Printf("%s Removed label '%s' from %s\n", green("✓"), label, issueID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issueIDs) > 0 && daemonClient == nil {
|
||||||
|
markDirtyAndScheduleFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput && len(results) > 0 {
|
||||||
|
outputJSON(results)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
246
cmd/bd/main.go
246
cmd/bd/main.go
@@ -1679,28 +1679,42 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var showCmd = &cobra.Command{
|
var showCmd = &cobra.Command{
|
||||||
Use: "show [id]",
|
Use: "show [id...]",
|
||||||
Short: "Show issue details",
|
Short: "Show issue details",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
showArgs := &rpc.ShowArgs{ID: args[0]}
|
allDetails := []interface{}{}
|
||||||
resp, err := daemonClient.Show(showArgs)
|
for idx, id := range args {
|
||||||
if err != nil {
|
showArgs := &rpc.ShowArgs{ID: id}
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
resp, err := daemonClient.Show(showArgs)
|
||||||
os.Exit(1)
|
if err != nil {
|
||||||
}
|
fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", id, err)
|
||||||
|
continue
|
||||||
if jsonOutput {
|
|
||||||
fmt.Println(string(resp.Data))
|
|
||||||
} else {
|
|
||||||
// Check if issue exists (daemon returns null for non-existent issues)
|
|
||||||
if string(resp.Data) == "null" || len(resp.Data) == 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, "Issue %s not found\n", args[0])
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
type IssueDetails struct {
|
||||||
|
types.Issue
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
Dependencies []*types.Issue `json:"dependencies,omitempty"`
|
||||||
|
Dependents []*types.Issue `json:"dependents,omitempty"`
|
||||||
|
}
|
||||||
|
var details IssueDetails
|
||||||
|
if err := json.Unmarshal(resp.Data, &details); err == nil {
|
||||||
|
allDetails = append(allDetails, details)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if issue exists (daemon returns null for non-existent issues)
|
||||||
|
if string(resp.Data) == "null" || len(resp.Data) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx > 0 {
|
||||||
|
fmt.Println("\n" + strings.Repeat("─", 60))
|
||||||
|
}
|
||||||
|
|
||||||
// Parse response and use existing formatting code
|
// Parse response and use existing formatting code
|
||||||
type IssueDetails struct {
|
type IssueDetails struct {
|
||||||
types.Issue
|
types.Issue
|
||||||
@@ -1796,40 +1810,51 @@ var showCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput && len(allDetails) > 0 {
|
||||||
|
outputJSON(allDetails)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct mode
|
// Direct mode
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
issue, err := store.GetIssue(ctx, args[0])
|
allDetails := []interface{}{}
|
||||||
if err != nil {
|
for idx, id := range args {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
issue, err := store.GetIssue(ctx, id)
|
||||||
os.Exit(1)
|
if err != nil {
|
||||||
}
|
fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", id, err)
|
||||||
if issue == nil {
|
continue
|
||||||
fmt.Fprintf(os.Stderr, "Issue %s not found\n", args[0])
|
}
|
||||||
os.Exit(1)
|
if issue == nil {
|
||||||
}
|
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
|
||||||
|
continue
|
||||||
if jsonOutput {
|
}
|
||||||
// Include labels, dependencies, and comments in JSON output
|
|
||||||
type IssueDetails struct {
|
if jsonOutput {
|
||||||
*types.Issue
|
// Include labels, dependencies, and comments in JSON output
|
||||||
Labels []string `json:"labels,omitempty"`
|
type IssueDetails struct {
|
||||||
Dependencies []*types.Issue `json:"dependencies,omitempty"`
|
*types.Issue
|
||||||
Dependents []*types.Issue `json:"dependents,omitempty"`
|
Labels []string `json:"labels,omitempty"`
|
||||||
Comments []*types.Comment `json:"comments,omitempty"`
|
Dependencies []*types.Issue `json:"dependencies,omitempty"`
|
||||||
|
Dependents []*types.Issue `json:"dependents,omitempty"`
|
||||||
|
Comments []*types.Comment `json:"comments,omitempty"`
|
||||||
|
}
|
||||||
|
details := &IssueDetails{Issue: issue}
|
||||||
|
details.Labels, _ = store.GetLabels(ctx, issue.ID)
|
||||||
|
details.Dependencies, _ = store.GetDependencies(ctx, issue.ID)
|
||||||
|
details.Dependents, _ = store.GetDependents(ctx, issue.ID)
|
||||||
|
details.Comments, _ = store.GetIssueComments(ctx, issue.ID)
|
||||||
|
allDetails = append(allDetails, details)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx > 0 {
|
||||||
|
fmt.Println("\n" + strings.Repeat("─", 60))
|
||||||
}
|
}
|
||||||
details := &IssueDetails{Issue: issue}
|
|
||||||
details.Labels, _ = store.GetLabels(ctx, issue.ID)
|
|
||||||
details.Dependencies, _ = store.GetDependencies(ctx, issue.ID)
|
|
||||||
details.Dependents, _ = store.GetDependents(ctx, issue.ID)
|
|
||||||
details.Comments, _ = store.GetIssueComments(ctx, issue.ID)
|
|
||||||
outputJSON(details)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cyan := color.New(color.FgCyan).SprintFunc()
|
cyan := color.New(color.FgCyan).SprintFunc()
|
||||||
|
|
||||||
@@ -1920,16 +1945,21 @@ var showCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show comments
|
// Show comments
|
||||||
comments, _ := store.GetIssueComments(ctx, issue.ID)
|
comments, _ := store.GetIssueComments(ctx, issue.ID)
|
||||||
if len(comments) > 0 {
|
if len(comments) > 0 {
|
||||||
fmt.Printf("\nComments (%d):\n", len(comments))
|
fmt.Printf("\nComments (%d):\n", len(comments))
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
fmt.Printf(" [%s at %s]\n %s\n\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"), comment.Text)
|
fmt.Printf(" [%s at %s]\n %s\n\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"), comment.Text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
if jsonOutput && len(allDetails) > 0 {
|
||||||
|
outputJSON(allDetails)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1938,9 +1968,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var updateCmd = &cobra.Command{
|
var updateCmd = &cobra.Command{
|
||||||
Use: "update [id]",
|
Use: "update [id...]",
|
||||||
Short: "Update an issue",
|
Short: "Update one or more issues",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]interface{})
|
||||||
|
|
||||||
@@ -1984,63 +2014,83 @@ var updateCmd = &cobra.Command{
|
|||||||
|
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
updateArgs := &rpc.UpdateArgs{ID: args[0]}
|
updatedIssues := []*types.Issue{}
|
||||||
|
for _, id := range args {
|
||||||
|
updateArgs := &rpc.UpdateArgs{ID: id}
|
||||||
|
|
||||||
// Map updates to RPC args
|
// Map updates to RPC args
|
||||||
if status, ok := updates["status"].(string); ok {
|
if status, ok := updates["status"].(string); ok {
|
||||||
updateArgs.Status = &status
|
updateArgs.Status = &status
|
||||||
}
|
}
|
||||||
if priority, ok := updates["priority"].(int); ok {
|
if priority, ok := updates["priority"].(int); ok {
|
||||||
updateArgs.Priority = &priority
|
updateArgs.Priority = &priority
|
||||||
}
|
}
|
||||||
if title, ok := updates["title"].(string); ok {
|
if title, ok := updates["title"].(string); ok {
|
||||||
updateArgs.Title = &title
|
updateArgs.Title = &title
|
||||||
}
|
}
|
||||||
if assignee, ok := updates["assignee"].(string); ok {
|
if assignee, ok := updates["assignee"].(string); ok {
|
||||||
updateArgs.Assignee = &assignee
|
updateArgs.Assignee = &assignee
|
||||||
}
|
}
|
||||||
if design, ok := updates["design"].(string); ok {
|
if design, ok := updates["design"].(string); ok {
|
||||||
updateArgs.Design = &design
|
updateArgs.Design = &design
|
||||||
}
|
}
|
||||||
if notes, ok := updates["notes"].(string); ok {
|
if notes, ok := updates["notes"].(string); ok {
|
||||||
updateArgs.Notes = ¬es
|
updateArgs.Notes = ¬es
|
||||||
}
|
}
|
||||||
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
||||||
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := daemonClient.Update(updateArgs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
var issue types.Issue
|
||||||
|
if err := json.Unmarshal(resp.Data, &issue); err == nil {
|
||||||
|
updatedIssues = append(updatedIssues, &issue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
|
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := daemonClient.Update(updateArgs)
|
if jsonOutput && len(updatedIssues) > 0 {
|
||||||
if err != nil {
|
outputJSON(updatedIssues)
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if jsonOutput {
|
|
||||||
fmt.Println(string(resp.Data))
|
|
||||||
} else {
|
|
||||||
green := color.New(color.FgGreen).SprintFunc()
|
|
||||||
fmt.Printf("%s Updated issue: %s\n", green("✓"), args[0])
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct mode
|
// Direct mode
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := store.UpdateIssue(ctx, args[0], updates, actor); err != nil {
|
updatedIssues := []*types.Issue{}
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
for _, id := range args {
|
||||||
os.Exit(1)
|
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
issue, _ := store.GetIssue(ctx, id)
|
||||||
|
if issue != nil {
|
||||||
|
updatedIssues = append(updatedIssues, issue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
|
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule auto-flush
|
// Schedule auto-flush if any issues were updated
|
||||||
markDirtyAndScheduleFlush()
|
if len(args) > 0 {
|
||||||
|
markDirtyAndScheduleFlush()
|
||||||
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput && len(updatedIssues) > 0 {
|
||||||
// Fetch updated issue and output
|
outputJSON(updatedIssues)
|
||||||
issue, _ := store.GetIssue(ctx, args[0])
|
|
||||||
outputJSON(issue)
|
|
||||||
} else {
|
|
||||||
green := color.New(color.FgGreen).SprintFunc()
|
|
||||||
fmt.Printf("%s Updated issue: %s\n", green("✓"), args[0])
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user