|
|
|
|
@@ -1,4 +1,5 @@
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
@@ -6,36 +7,22 @@ import (
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/fatih/color"
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
"github.com/steveyegge/beads/internal/rpc"
|
|
|
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
|
|
|
"github.com/steveyegge/beads/internal/utils"
|
|
|
|
|
)
|
|
|
|
|
// formatDependencyType converts a dependency type to a human-readable label
|
|
|
|
|
func formatDependencyType(depType types.DependencyType) string {
|
|
|
|
|
switch depType {
|
|
|
|
|
case types.DepBlocks:
|
|
|
|
|
return "blocks"
|
|
|
|
|
case types.DepRelated:
|
|
|
|
|
return "related"
|
|
|
|
|
case types.DepParentChild:
|
|
|
|
|
return "parent-child"
|
|
|
|
|
case types.DepDiscoveredFrom:
|
|
|
|
|
return "discovered-from"
|
|
|
|
|
default:
|
|
|
|
|
return string(depType)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var showCmd = &cobra.Command{
|
|
|
|
|
Use: "show [id...]",
|
|
|
|
|
Short: "Show issue details",
|
|
|
|
|
Args: cobra.MinimumNArgs(1),
|
|
|
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
|
// Use global jsonOutput set by PersistentPreRun
|
|
|
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
// Resolve partial IDs first
|
|
|
|
|
var resolvedIDs []string
|
|
|
|
|
if daemonClient != nil {
|
|
|
|
|
@@ -47,12 +34,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
var resolvedID string
|
|
|
|
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
|
|
|
|
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// In direct mode, resolve via storage
|
|
|
|
|
@@ -63,6 +45,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If daemon is running, use RPC
|
|
|
|
|
if daemonClient != nil {
|
|
|
|
|
allDetails := []interface{}{}
|
|
|
|
|
@@ -73,12 +56,13 @@ var showCmd = &cobra.Command{
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", id, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput {
|
|
|
|
|
type IssueDetails struct {
|
|
|
|
|
types.Issue
|
|
|
|
|
Labels []string `json:"labels,omitempty"`
|
|
|
|
|
Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
|
|
|
|
Dependents []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
|
|
|
|
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 {
|
|
|
|
|
@@ -93,12 +77,13 @@ var showCmd = &cobra.Command{
|
|
|
|
|
if idx > 0 {
|
|
|
|
|
fmt.Println("\n" + strings.Repeat("─", 60))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse response and use existing formatting code
|
|
|
|
|
type IssueDetails struct {
|
|
|
|
|
types.Issue
|
|
|
|
|
Labels []string `json:"labels,omitempty"`
|
|
|
|
|
Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
|
|
|
|
Dependents []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
|
|
|
|
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 {
|
|
|
|
|
@@ -106,7 +91,9 @@ var showCmd = &cobra.Command{
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
issue := &details.Issue
|
|
|
|
|
|
|
|
|
|
cyan := color.New(color.FgCyan).SprintFunc()
|
|
|
|
|
|
|
|
|
|
// Format output (same as direct mode below)
|
|
|
|
|
tierEmoji := ""
|
|
|
|
|
statusSuffix := ""
|
|
|
|
|
@@ -118,6 +105,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
tierEmoji = " 📦"
|
|
|
|
|
statusSuffix = " (compacted L2)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
|
|
|
|
|
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
|
|
|
|
fmt.Printf("Priority: P%d\n", issue.Priority)
|
|
|
|
|
@@ -130,6 +118,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("Created: %s\n", issue.CreatedAt.Format("2006-01-02 15:04"))
|
|
|
|
|
fmt.Printf("Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))
|
|
|
|
|
|
|
|
|
|
// Show compaction status
|
|
|
|
|
if issue.CompactionLevel > 0 {
|
|
|
|
|
fmt.Println()
|
|
|
|
|
@@ -152,6 +141,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%s Compacted: %s (Tier %d)\n", tierEmoji2, compactedDate, issue.CompactionLevel)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if issue.Description != "" {
|
|
|
|
|
fmt.Printf("\nDescription:\n%s\n", issue.Description)
|
|
|
|
|
}
|
|
|
|
|
@@ -164,33 +154,35 @@ var showCmd = &cobra.Command{
|
|
|
|
|
if issue.AcceptanceCriteria != "" {
|
|
|
|
|
fmt.Printf("\nAcceptance Criteria:\n%s\n", issue.AcceptanceCriteria)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(details.Labels) > 0 {
|
|
|
|
|
fmt.Printf("\nLabels: %v\n", details.Labels)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(details.Dependencies) > 0 {
|
|
|
|
|
fmt.Printf("\nDependencies (%d):\n", len(details.Dependencies))
|
|
|
|
|
fmt.Printf("\nDepends on (%d):\n", len(details.Dependencies))
|
|
|
|
|
for _, dep := range details.Dependencies {
|
|
|
|
|
fmt.Printf(" [%s] %s (%s): %s [P%d]\n",
|
|
|
|
|
formatDependencyType(dep.DependencyType),
|
|
|
|
|
dep.ID, dep.Status, dep.Title, dep.Priority)
|
|
|
|
|
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(details.Dependents) > 0 {
|
|
|
|
|
fmt.Printf("\nDependents (%d):\n", len(details.Dependents))
|
|
|
|
|
fmt.Printf("\nBlocks (%d):\n", len(details.Dependents))
|
|
|
|
|
for _, dep := range details.Dependents {
|
|
|
|
|
fmt.Printf(" [%s] %s (%s): %s [P%d]\n",
|
|
|
|
|
formatDependencyType(dep.DependencyType),
|
|
|
|
|
dep.ID, dep.Status, dep.Title, dep.Priority)
|
|
|
|
|
fmt.Printf(" ← %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput && len(allDetails) > 0 {
|
|
|
|
|
outputJSON(allDetails)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Direct mode
|
|
|
|
|
allDetails := []interface{}{}
|
|
|
|
|
for idx, id := range resolvedIDs {
|
|
|
|
|
@@ -203,40 +195,31 @@ var showCmd = &cobra.Command{
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput {
|
|
|
|
|
// Include labels, dependencies (with metadata), dependents (with metadata), and comments in JSON output
|
|
|
|
|
// Include labels, dependencies, and comments in JSON output
|
|
|
|
|
type IssueDetails struct {
|
|
|
|
|
*types.Issue
|
|
|
|
|
Labels []string `json:"labels,omitempty"`
|
|
|
|
|
Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
|
|
|
|
Dependents []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
|
|
|
|
Comments []*types.Comment `json:"comments,omitempty"`
|
|
|
|
|
Labels []string `json:"labels,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)
|
|
|
|
|
// Get dependencies with metadata (type, created_at, created_by)
|
|
|
|
|
if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
|
|
|
|
|
details.Dependencies, _ = sqliteStore.GetDependenciesWithMetadata(ctx, issue.ID)
|
|
|
|
|
details.Dependents, _ = sqliteStore.GetDependentsWithMetadata(ctx, issue.ID)
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to regular methods without metadata for other storage backends
|
|
|
|
|
deps, _ := store.GetDependencies(ctx, issue.ID)
|
|
|
|
|
for _, dep := range deps {
|
|
|
|
|
details.Dependencies = append(details.Dependencies, &types.IssueWithDependencyMetadata{Issue: *dep})
|
|
|
|
|
}
|
|
|
|
|
dependents, _ := store.GetDependents(ctx, issue.ID)
|
|
|
|
|
for _, dependent := range dependents {
|
|
|
|
|
details.Dependents = append(details.Dependents, &types.IssueWithDependencyMetadata{Issue: *dependent})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cyan := color.New(color.FgCyan).SprintFunc()
|
|
|
|
|
|
|
|
|
|
// Add compaction emoji to title line
|
|
|
|
|
tierEmoji := ""
|
|
|
|
|
statusSuffix := ""
|
|
|
|
|
@@ -248,6 +231,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
tierEmoji = " 📦"
|
|
|
|
|
statusSuffix = " (compacted L2)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
|
|
|
|
|
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
|
|
|
|
fmt.Printf("Priority: P%d\n", issue.Priority)
|
|
|
|
|
@@ -260,6 +244,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("Created: %s\n", issue.CreatedAt.Format("2006-01-02 15:04"))
|
|
|
|
|
fmt.Printf("Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))
|
|
|
|
|
|
|
|
|
|
// Show compaction status footer
|
|
|
|
|
if issue.CompactionLevel > 0 {
|
|
|
|
|
tierEmoji := "🗜️"
|
|
|
|
|
@@ -267,6 +252,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
tierEmoji = "📦"
|
|
|
|
|
}
|
|
|
|
|
tierName := fmt.Sprintf("Tier %d", issue.CompactionLevel)
|
|
|
|
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
if issue.OriginalSize > 0 {
|
|
|
|
|
currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
|
|
|
|
|
@@ -283,6 +269,7 @@ var showCmd = &cobra.Command{
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%s Compacted: %s (%s)\n", tierEmoji, compactedDate, tierName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if issue.Description != "" {
|
|
|
|
|
fmt.Printf("\nDescription:\n%s\n", issue.Description)
|
|
|
|
|
}
|
|
|
|
|
@@ -295,56 +282,31 @@ var showCmd = &cobra.Command{
|
|
|
|
|
if issue.AcceptanceCriteria != "" {
|
|
|
|
|
fmt.Printf("\nAcceptance Criteria:\n%s\n", issue.AcceptanceCriteria)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show labels
|
|
|
|
|
labels, _ := store.GetLabels(ctx, issue.ID)
|
|
|
|
|
if len(labels) > 0 {
|
|
|
|
|
fmt.Printf("\nLabels: %v\n", labels)
|
|
|
|
|
}
|
|
|
|
|
// Show dependencies with metadata (including type)
|
|
|
|
|
var depsWithMeta []*types.IssueWithDependencyMetadata
|
|
|
|
|
if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
|
|
|
|
|
depsWithMeta, _ = sqliteStore.GetDependenciesWithMetadata(ctx, issue.ID)
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback for non-SQLite storage
|
|
|
|
|
deps, _ := store.GetDependencies(ctx, issue.ID)
|
|
|
|
|
|
|
|
|
|
// Show dependencies
|
|
|
|
|
deps, _ := store.GetDependencies(ctx, issue.ID)
|
|
|
|
|
if len(deps) > 0 {
|
|
|
|
|
fmt.Printf("\nDepends on (%d):\n", len(deps))
|
|
|
|
|
for _, dep := range deps {
|
|
|
|
|
depsWithMeta = append(depsWithMeta, &types.IssueWithDependencyMetadata{
|
|
|
|
|
Issue: *dep,
|
|
|
|
|
DependencyType: types.DepBlocks, // default
|
|
|
|
|
})
|
|
|
|
|
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(depsWithMeta) > 0 {
|
|
|
|
|
fmt.Printf("\nDependencies (%d):\n", len(depsWithMeta))
|
|
|
|
|
for _, dep := range depsWithMeta {
|
|
|
|
|
fmt.Printf(" [%s] %s (%s): %s [P%d]\n",
|
|
|
|
|
formatDependencyType(dep.DependencyType),
|
|
|
|
|
dep.ID, dep.Status, dep.Title, dep.Priority)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show dependents with metadata (including type)
|
|
|
|
|
var dependentsWithMeta []*types.IssueWithDependencyMetadata
|
|
|
|
|
if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
|
|
|
|
|
dependentsWithMeta, _ = sqliteStore.GetDependentsWithMetadata(ctx, issue.ID)
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback for non-SQLite storage
|
|
|
|
|
dependents, _ := store.GetDependents(ctx, issue.ID)
|
|
|
|
|
for _, dependent := range dependents {
|
|
|
|
|
dependentsWithMeta = append(dependentsWithMeta, &types.IssueWithDependencyMetadata{
|
|
|
|
|
Issue: *dependent,
|
|
|
|
|
DependencyType: types.DepBlocks, // default
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(dependentsWithMeta) > 0 {
|
|
|
|
|
fmt.Printf("\nDependents (%d):\n", len(dependentsWithMeta))
|
|
|
|
|
for _, dep := range dependentsWithMeta {
|
|
|
|
|
fmt.Printf(" [%s] %s (%s): %s [P%d]\n",
|
|
|
|
|
formatDependencyType(dep.DependencyType),
|
|
|
|
|
dep.ID, dep.Status, dep.Title, dep.Priority)
|
|
|
|
|
|
|
|
|
|
// Show dependents
|
|
|
|
|
dependents, _ := store.GetDependents(ctx, issue.ID)
|
|
|
|
|
if len(dependents) > 0 {
|
|
|
|
|
fmt.Printf("\nBlocks (%d):\n", len(dependents))
|
|
|
|
|
for _, dep := range dependents {
|
|
|
|
|
fmt.Printf(" ← %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show comments
|
|
|
|
|
comments, _ := store.GetIssueComments(ctx, issue.ID)
|
|
|
|
|
if len(comments) > 0 {
|
|
|
|
|
@@ -353,31 +315,30 @@ var showCmd = &cobra.Command{
|
|
|
|
|
fmt.Printf(" [%s at %s]\n %s\n\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"), comment.Text)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput && len(allDetails) > 0 {
|
|
|
|
|
outputJSON(allDetails)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var updateCmd = &cobra.Command{
|
|
|
|
|
Use: "update [id...]",
|
|
|
|
|
Short: "Update one or more issues",
|
|
|
|
|
Args: cobra.MinimumNArgs(1),
|
|
|
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
|
// Use global jsonOutput set by PersistentPreRun
|
|
|
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
|
updates := make(map[string]interface{})
|
|
|
|
|
|
|
|
|
|
if cmd.Flags().Changed("status") {
|
|
|
|
|
status, _ := cmd.Flags().GetString("status")
|
|
|
|
|
updates["status"] = status
|
|
|
|
|
}
|
|
|
|
|
if cmd.Flags().Changed("priority") {
|
|
|
|
|
priorityStr, _ := cmd.Flags().GetString("priority")
|
|
|
|
|
priority := parsePriority(priorityStr)
|
|
|
|
|
if priority == -1 {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error: invalid priority %q (expected 0-4 or P0-P4)\n", priorityStr)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
priority, _ := cmd.Flags().GetInt("priority")
|
|
|
|
|
updates["priority"] = priority
|
|
|
|
|
}
|
|
|
|
|
if cmd.Flags().Changed("title") {
|
|
|
|
|
@@ -413,11 +374,14 @@ var updateCmd = &cobra.Command{
|
|
|
|
|
externalRef, _ := cmd.Flags().GetString("external-ref")
|
|
|
|
|
updates["external_ref"] = externalRef
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(updates) == 0 {
|
|
|
|
|
fmt.Println("No updates specified")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
// Resolve partial IDs first
|
|
|
|
|
var resolvedIDs []string
|
|
|
|
|
if daemonClient != nil {
|
|
|
|
|
@@ -428,12 +392,7 @@ var updateCmd = &cobra.Command{
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
var resolvedID string
|
|
|
|
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
|
|
|
|
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
var err error
|
|
|
|
|
@@ -443,11 +402,13 @@ var updateCmd = &cobra.Command{
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If daemon is running, use RPC
|
|
|
|
|
if daemonClient != nil {
|
|
|
|
|
updatedIssues := []*types.Issue{}
|
|
|
|
|
for _, id := range resolvedIDs {
|
|
|
|
|
updateArgs := &rpc.UpdateArgs{ID: id}
|
|
|
|
|
|
|
|
|
|
// Map updates to RPC args
|
|
|
|
|
if status, ok := updates["status"].(string); ok {
|
|
|
|
|
updateArgs.Status = &status
|
|
|
|
|
@@ -473,11 +434,16 @@ var updateCmd = &cobra.Command{
|
|
|
|
|
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
|
|
|
|
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
|
|
|
|
}
|
|
|
|
|
if externalRef, ok := updates["external_ref"].(string); ok { // NEW: Map external_ref
|
|
|
|
|
updateArgs.ExternalRef = &externalRef
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
@@ -488,19 +454,22 @@ var updateCmd = &cobra.Command{
|
|
|
|
|
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput && len(updatedIssues) > 0 {
|
|
|
|
|
outputJSON(updatedIssues)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Direct mode
|
|
|
|
|
updatedIssues := []*types.Issue{}
|
|
|
|
|
for _, id := range resolvedIDs {
|
|
|
|
|
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if jsonOutput {
|
|
|
|
|
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)
|
|
|
|
|
@@ -510,20 +479,25 @@ var updateCmd = &cobra.Command{
|
|
|
|
|
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Schedule auto-flush if any issues were updated
|
|
|
|
|
if len(args) > 0 {
|
|
|
|
|
markDirtyAndScheduleFlush()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput && len(updatedIssues) > 0 {
|
|
|
|
|
outputJSON(updatedIssues)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
@@ -534,6 +508,7 @@ Examples:
|
|
|
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
|
id := args[0]
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
// Resolve partial ID if in direct mode
|
|
|
|
|
if daemonClient == nil {
|
|
|
|
|
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
|
|
|
|
@@ -543,6 +518,7 @@ Examples:
|
|
|
|
|
}
|
|
|
|
|
id = fullID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine which field to edit
|
|
|
|
|
fieldToEdit := "description"
|
|
|
|
|
if cmd.Flags().Changed("title") {
|
|
|
|
|
@@ -554,6 +530,7 @@ Examples:
|
|
|
|
|
} else if cmd.Flags().Changed("acceptance") {
|
|
|
|
|
fieldToEdit = "acceptance_criteria"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the editor from environment
|
|
|
|
|
editor := os.Getenv("EDITOR")
|
|
|
|
|
if editor == "" {
|
|
|
|
|
@@ -572,9 +549,11 @@ Examples:
|
|
|
|
|
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}
|
|
|
|
|
@@ -583,6 +562,7 @@ Examples:
|
|
|
|
|
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)
|
|
|
|
|
@@ -600,6 +580,7 @@ Examples:
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the current field value
|
|
|
|
|
var currentValue string
|
|
|
|
|
switch fieldToEdit {
|
|
|
|
|
@@ -614,6 +595,7 @@ Examples:
|
|
|
|
|
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 {
|
|
|
|
|
@@ -622,47 +604,56 @@ Examples:
|
|
|
|
|
}
|
|
|
|
|
tmpPath := tmpFile.Name()
|
|
|
|
|
defer func() { _ = os.Remove(tmpPath) }()
|
|
|
|
|
|
|
|
|
|
// Write current value to temp file
|
|
|
|
|
if _, err := tmpFile.WriteString(currentValue); err != nil {
|
|
|
|
|
_ = tmpFile.Close() // nolint:gosec // G104: Error already handled above
|
|
|
|
|
tmpFile.Close()
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error writing to temp file: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
_ = tmpFile.Close() // nolint:gosec // G104: Defer close errors are non-critical
|
|
|
|
|
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
|
|
|
|
|
// nolint:gosec // G304: tmpPath is securely created temp file
|
|
|
|
|
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
|
|
|
|
|
@@ -675,6 +666,7 @@ Examples:
|
|
|
|
|
case "acceptance_criteria":
|
|
|
|
|
updateArgs.AcceptanceCriteria = &newValue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err := daemonClient.Update(updateArgs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error updating issue: %v\n", err)
|
|
|
|
|
@@ -688,11 +680,13 @@ Examples:
|
|
|
|
|
}
|
|
|
|
|
markDirtyAndScheduleFlush()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
green := color.New(color.FgGreen).SprintFunc()
|
|
|
|
|
fieldName := strings.ReplaceAll(fieldToEdit, "_", " ")
|
|
|
|
|
fmt.Printf("%s Updated %s for issue: %s\n", green("✓"), fieldName, id)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var closeCmd = &cobra.Command{
|
|
|
|
|
Use: "close [id...]",
|
|
|
|
|
Short: "Close one or more issues",
|
|
|
|
|
@@ -702,8 +696,10 @@ var closeCmd = &cobra.Command{
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = "Closed"
|
|
|
|
|
}
|
|
|
|
|
// Use global jsonOutput set by PersistentPreRun
|
|
|
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
// Resolve partial IDs first
|
|
|
|
|
var resolvedIDs []string
|
|
|
|
|
if daemonClient != nil {
|
|
|
|
|
@@ -714,12 +710,7 @@ var closeCmd = &cobra.Command{
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
var resolvedID string
|
|
|
|
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
|
|
|
|
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
var err error
|
|
|
|
|
@@ -729,6 +720,7 @@ var closeCmd = &cobra.Command{
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If daemon is running, use RPC
|
|
|
|
|
if daemonClient != nil {
|
|
|
|
|
closedIssues := []*types.Issue{}
|
|
|
|
|
@@ -742,6 +734,7 @@ var closeCmd = &cobra.Command{
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput {
|
|
|
|
|
var issue types.Issue
|
|
|
|
|
if err := json.Unmarshal(resp.Data, &issue); err == nil {
|
|
|
|
|
@@ -752,11 +745,13 @@ var closeCmd = &cobra.Command{
|
|
|
|
|
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput && len(closedIssues) > 0 {
|
|
|
|
|
outputJSON(closedIssues)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Direct mode
|
|
|
|
|
closedIssues := []*types.Issue{}
|
|
|
|
|
for _, id := range resolvedIDs {
|
|
|
|
|
@@ -774,19 +769,24 @@ var closeCmd = &cobra.Command{
|
|
|
|
|
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Schedule auto-flush if any issues were closed
|
|
|
|
|
if len(args) > 0 {
|
|
|
|
|
markDirtyAndScheduleFlush()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOutput && len(closedIssues) > 0 {
|
|
|
|
|
outputJSON(closedIssues)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
showCmd.Flags().Bool("json", false, "Output JSON format")
|
|
|
|
|
rootCmd.AddCommand(showCmd)
|
|
|
|
|
|
|
|
|
|
updateCmd.Flags().StringP("status", "s", "", "New status")
|
|
|
|
|
updateCmd.Flags().StringP("priority", "p", "", "New priority (0-4 or P0-P4)")
|
|
|
|
|
updateCmd.Flags().IntP("priority", "p", 0, "New priority")
|
|
|
|
|
updateCmd.Flags().String("title", "", "New title")
|
|
|
|
|
updateCmd.Flags().StringP("assignee", "a", "", "New assignee")
|
|
|
|
|
updateCmd.Flags().StringP("description", "d", "", "Issue description")
|
|
|
|
|
@@ -796,13 +796,17 @@ func init() {
|
|
|
|
|
updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
|
|
|
|
|
_ = updateCmd.Flags().MarkHidden("acceptance-criteria")
|
|
|
|
|
updateCmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
|
|
|
|
|
updateCmd.Flags().Bool("json", false, "Output JSON format")
|
|
|
|
|
rootCmd.AddCommand(updateCmd)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
|
|
|
|
|
closeCmd.Flags().Bool("json", false, "Output JSON format")
|
|
|
|
|
rootCmd.AddCommand(closeCmd)
|
|
|
|
|
}
|
|
|
|
|
|