Add bd edit mode for human usage (#152)
Co-authored-by: Claude Code <claude@anthropic.com>
This commit is contained in:
195
cmd/bd/main.go
195
cmd/bd/main.go
@@ -2127,6 +2127,201 @@ func init() {
|
|||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var editCmd = &cobra.Command{
|
||||||
|
Use: "edit [id]",
|
||||||
|
Short: "Edit an issue field in $EDITOR",
|
||||||
|
Long: `Edit an issue field using your configured $EDITOR.
|
||||||
|
|
||||||
|
By default, edits the description. Use flags to edit other fields.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
bd edit bd-42 # Edit description
|
||||||
|
bd edit bd-42 --title # Edit title
|
||||||
|
bd edit bd-42 --design # Edit design notes
|
||||||
|
bd edit bd-42 --notes # Edit notes
|
||||||
|
bd edit bd-42 --acceptance # Edit acceptance criteria`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
id := args[0]
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Determine which field to edit
|
||||||
|
fieldToEdit := "description"
|
||||||
|
if cmd.Flags().Changed("title") {
|
||||||
|
fieldToEdit = "title"
|
||||||
|
} else if cmd.Flags().Changed("design") {
|
||||||
|
fieldToEdit = "design"
|
||||||
|
} else if cmd.Flags().Changed("notes") {
|
||||||
|
fieldToEdit = "notes"
|
||||||
|
} else if cmd.Flags().Changed("acceptance") {
|
||||||
|
fieldToEdit = "acceptance_criteria"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the editor from environment
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
editor = os.Getenv("VISUAL")
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
// Try common defaults
|
||||||
|
for _, defaultEditor := range []string{"vim", "vi", "nano", "emacs"} {
|
||||||
|
if _, err := exec.LookPath(defaultEditor); err == nil {
|
||||||
|
editor = defaultEditor
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: No editor found. Set $EDITOR or $VISUAL environment variable.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current issue
|
||||||
|
var issue *types.Issue
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if daemonClient != nil {
|
||||||
|
// Daemon mode
|
||||||
|
showArgs := &rpc.ShowArgs{ID: id}
|
||||||
|
resp, err := daemonClient.Show(showArgs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error fetching issue %s: %v\n", id, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
issue = &types.Issue{}
|
||||||
|
if err := json.Unmarshal(resp.Data, issue); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing issue data: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Direct mode
|
||||||
|
issue, err = store.GetIssue(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error fetching issue %s: %v\n", id, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if issue == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current field value
|
||||||
|
var currentValue string
|
||||||
|
switch fieldToEdit {
|
||||||
|
case "title":
|
||||||
|
currentValue = issue.Title
|
||||||
|
case "description":
|
||||||
|
currentValue = issue.Description
|
||||||
|
case "design":
|
||||||
|
currentValue = issue.Design
|
||||||
|
case "notes":
|
||||||
|
currentValue = issue.Notes
|
||||||
|
case "acceptance_criteria":
|
||||||
|
currentValue = issue.AcceptanceCriteria
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary file with the current value
|
||||||
|
tmpFile, err := os.CreateTemp("", fmt.Sprintf("bd-edit-%s-*.txt", fieldToEdit))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error creating temp file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
|
// Write current value to temp file
|
||||||
|
if _, err := tmpFile.WriteString(currentValue); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing to temp file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
// Open the editor
|
||||||
|
editorCmd := exec.Command(editor, tmpPath)
|
||||||
|
editorCmd.Stdin = os.Stdin
|
||||||
|
editorCmd.Stdout = os.Stdout
|
||||||
|
editorCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := editorCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error running editor: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the edited content
|
||||||
|
editedContent, err := os.ReadFile(tmpPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
newValue := string(editedContent)
|
||||||
|
|
||||||
|
// Check if the value changed
|
||||||
|
if newValue == currentValue {
|
||||||
|
fmt.Println("No changes made")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate title if editing title
|
||||||
|
if fieldToEdit == "title" && strings.TrimSpace(newValue) == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: title cannot be empty\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the issue
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
fieldToEdit: newValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
if daemonClient != nil {
|
||||||
|
// Daemon mode
|
||||||
|
updateArgs := &rpc.UpdateArgs{ID: id}
|
||||||
|
|
||||||
|
switch fieldToEdit {
|
||||||
|
case "title":
|
||||||
|
updateArgs.Title = &newValue
|
||||||
|
case "description":
|
||||||
|
updateArgs.Description = &newValue
|
||||||
|
case "design":
|
||||||
|
updateArgs.Design = &newValue
|
||||||
|
case "notes":
|
||||||
|
updateArgs.Notes = &newValue
|
||||||
|
case "acceptance_criteria":
|
||||||
|
updateArgs.AcceptanceCriteria = &newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := daemonClient.Update(updateArgs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error updating issue: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Direct mode
|
||||||
|
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error updating issue: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
markDirtyAndScheduleFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
|
fieldName := strings.ReplaceAll(fieldToEdit, "_", " ")
|
||||||
|
fmt.Printf("%s Updated %s for issue: %s\n", green("✓"), fieldName, id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
editCmd.Flags().Bool("title", false, "Edit the title")
|
||||||
|
editCmd.Flags().Bool("description", false, "Edit the description (default)")
|
||||||
|
editCmd.Flags().Bool("design", false, "Edit the design notes")
|
||||||
|
editCmd.Flags().Bool("notes", false, "Edit the notes")
|
||||||
|
editCmd.Flags().Bool("acceptance", false, "Edit the acceptance criteria")
|
||||||
|
rootCmd.AddCommand(editCmd)
|
||||||
|
}
|
||||||
|
|
||||||
var closeCmd = &cobra.Command{
|
var closeCmd = &cobra.Command{
|
||||||
Use: "close [id...]",
|
Use: "close [id...]",
|
||||||
Short: "Close one or more issues",
|
Short: "Close one or more issues",
|
||||||
|
|||||||
Reference in New Issue
Block a user