Fix lint errors: handle errors, use fmt.Fprintf, apply De Morgan's law, use switch statements

Amp-Thread-ID: https://ampcode.com/threads/T-afcf56b0-a8bc-4310-bb59-1b63e1d70c89
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-24 12:27:02 -07:00
parent 1d5e89b9bb
commit 9dcb86ebfb
17 changed files with 342 additions and 537 deletions

View File

@@ -1038,7 +1038,7 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p
}
return
case <-ctx.Done():
log("Context cancelled, shutting down")
log("Context canceled, shutting down")
if err := server.Stop(); err != nil {
log("Error stopping RPC server: %v", err)
}

View File

@@ -249,7 +249,7 @@ func TestDaemonLogFileCreation(t *testing.T) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
msg := "Test log message"
_, err = logF.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, msg))
_, err = fmt.Fprintf(logF, "[%s] %s\n", timestamp, msg)
if err != nil {
t.Fatalf("Failed to write to log file: %v", err)
}

View File

@@ -184,10 +184,10 @@ var closeEligibleEpicsCmd = &cobra.Command{
Reason: "All children completed",
})
if err != nil || !resp.Success {
errMsg := "unknown error"
errMsg := ""
if err != nil {
errMsg = err.Error()
} else {
} else if !resp.Success {
errMsg = resp.Error
}
fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", epicStatus.Epic.ID, errMsg)

View File

@@ -657,7 +657,7 @@ func replaceBoundaryAware(text, oldID, newID string) string {
func isBoundary(c byte) bool {
// Issue IDs contain: lowercase letters, digits, and hyphens
// Boundaries are anything else (space, punctuation, etc.)
return !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-')
return (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-'
}
// isNumeric returns true if the string contains only digits

View File

@@ -20,53 +20,50 @@ var labelCmd = &cobra.Command{
Short: "Manage issue labels",
}
// executeLabelCommand executes a label operation and handles output
func executeLabelCommand(issueID, label, operation string, operationFunc func(context.Context, string, string, string) error) {
// Helper function to process label operations for multiple issues
func processBatchLabelOperation(issueIDs []string, label string, operation string,
daemonFunc func(string, string) error, storeFunc func(context.Context, string, string, string) error) {
ctx := context.Background()
results := []map[string]interface{}{}
// Use daemon if available
if daemonClient != nil {
for _, issueID := range issueIDs {
var err error
if operation == "added" {
_, err = daemonClient.AddLabel(&rpc.LabelAddArgs{
ID: issueID,
Label: label,
})
if daemonClient != nil {
err = daemonFunc(issueID, label)
} else {
_, err = daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{
ID: issueID,
Label: label,
})
err = storeFunc(ctx, issueID, label, actor)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
} else {
// Direct mode
if err := operationFunc(ctx, issueID, label, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
fmt.Fprintf(os.Stderr, "Error %s label %s %s: %v\n", operation, operation, issueID, err)
continue
}
// Schedule auto-flush
if jsonOutput {
results = append(results, map[string]interface{}{
"status": operation,
"issue_id": issueID,
"label": label,
})
} else {
green := color.New(color.FgGreen).SprintFunc()
verb := "Added"
prep := "to"
if operation == "removed" {
verb = "Removed"
prep = "from"
}
fmt.Printf("%s %s label '%s' %s %s\n", green("✓"), verb, label, prep, issueID)
}
}
if len(issueIDs) > 0 && daemonClient == nil {
markDirtyAndScheduleFlush()
}
if jsonOutput {
outputJSON(map[string]interface{}{
"status": operation,
"issue_id": issueID,
"label": label,
})
return
if jsonOutput && len(results) > 0 {
outputJSON(results)
}
green := color.New(color.FgGreen).SprintFunc()
// Capitalize first letter manually (strings.Title is deprecated)
capitalizedOp := strings.ToUpper(operation[:1]) + operation[1:]
fmt.Printf("%s %s label '%s' to %s\n", green("✓"), capitalizedOp, label, issueID)
}
var labelAddCmd = &cobra.Command{
@@ -74,48 +71,17 @@ var labelAddCmd = &cobra.Command{
Short: "Add a label to one or more issues",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
// Last arg is the label, everything before is issue IDs
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)
}
processBatchLabelOperation(issueIDs, label, "added",
func(issueID, lbl string) error {
_, err := daemonClient.AddLabel(&rpc.LabelAddArgs{ID: issueID, Label: lbl})
return err
},
func(ctx context.Context, issueID, lbl, act string) error {
return store.AddLabel(ctx, issueID, lbl, act)
})
},
}
@@ -124,48 +90,17 @@ var labelRemoveCmd = &cobra.Command{
Short: "Remove a label from one or more issues",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
// Last arg is the label, everything before is issue IDs
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)
}
processBatchLabelOperation(issueIDs, label, "removed",
func(issueID, lbl string) error {
_, err := daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{ID: issueID, Label: lbl})
return err
},
func(ctx context.Context, issueID, lbl, act string) error {
return store.RemoveLabel(ctx, issueID, lbl, act)
})
},
}

View File

@@ -48,6 +48,9 @@ const (
FallbackFlagNoDaemon = "flag_no_daemon"
FallbackConnectFailed = "connect_failed"
FallbackHealthFailed = "health_failed"
cmdDaemon = "daemon"
cmdImport = "import"
statusHealthy = "healthy"
FallbackAutoStartDisabled = "auto_start_disabled"
FallbackAutoStartFailed = "auto_start_failed"
FallbackDaemonUnsupported = "daemon_unsupported"
@@ -87,7 +90,7 @@ var rootCmd = &cobra.Command{
// Apply viper configuration if flags weren't explicitly set
// Priority: flags > viper (config file + env vars) > defaults
// Do this BEFORE early-return so init/version/help respect config
// If flag wasn't explicitly set, use viper value
if !cmd.Flags().Changed("json") {
jsonOutput = config.GetBool("json")
@@ -109,7 +112,7 @@ var rootCmd = &cobra.Command{
}
// Skip database initialization for commands that don't need a database
if cmd.Name() == "init" || cmd.Name() == "daemon" || cmd.Name() == "help" || cmd.Name() == "version" || cmd.Name() == "quickstart" {
if cmd.Name() == "init" || cmd.Name() == cmdDaemon || cmd.Name() == "help" || cmd.Name() == "version" || cmd.Name() == "quickstart" {
return
}
@@ -133,14 +136,14 @@ var rootCmd = &cobra.Command{
if err == nil {
localBeadsDir = filepath.Join(cwd, ".beads")
}
// Use public API to find database (same logic as extensions)
if foundDB := beads.FindDatabasePath(); foundDB != "" {
dbPath = foundDB
// Special case for import: if we found a database but there's a local .beads/
// directory without a database, prefer creating a local database
if cmd.Name() == "import" && localBeadsDir != "" {
if cmd.Name() == cmdImport && localBeadsDir != "" {
if _, err := os.Stat(localBeadsDir); err == nil {
// Check if found database is NOT in the local .beads/ directory
if !strings.HasPrefix(dbPath, localBeadsDir+string(filepath.Separator)) {
@@ -151,13 +154,13 @@ var rootCmd = &cobra.Command{
}
} else {
// For import command, allow creating database if .beads/ directory exists
if cmd.Name() == "import" && localBeadsDir != "" {
if cmd.Name() == cmdImport && localBeadsDir != "" {
if _, err := os.Stat(localBeadsDir); err == nil {
// .beads/ directory exists - set dbPath for import to create
dbPath = filepath.Join(localBeadsDir, "vc.db")
}
}
// If dbPath still not set, error out
if dbPath == "" {
// No database found - error out instead of falling back to ~/.beads
@@ -208,18 +211,18 @@ var rootCmd = &cobra.Command{
absDBPath, _ := filepath.Abs(dbPath)
client.SetDatabasePath(absDBPath)
}
// Perform health check
health, healthErr := client.Health()
if healthErr == nil && health.Status == "healthy" {
if healthErr == nil && health.Status == statusHealthy {
// Check version compatibility
if !health.Compatible {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: daemon version mismatch (daemon: %s, client: %s), restarting daemon\n",
fmt.Fprintf(os.Stderr, "Debug: daemon version mismatch (daemon: %s, client: %s), restarting daemon\n",
health.Version, Version)
}
client.Close()
// Kill old daemon and restart with new version
if restartDaemonForVersionMismatch() {
// Retry connection after restart
@@ -230,9 +233,9 @@ var rootCmd = &cobra.Command{
client.SetDatabasePath(absDBPath)
}
health, healthErr = client.Health()
if healthErr == nil && health.Status == "healthy" {
if healthErr == nil && health.Status == statusHealthy {
daemonClient = client
daemonStatus.Mode = "daemon"
daemonStatus.Mode = cmdDaemon
daemonStatus.Connected = true
daemonStatus.Degraded = false
daemonStatus.Health = health.Status
@@ -246,12 +249,12 @@ var rootCmd = &cobra.Command{
}
// If restart failed, fall through to direct mode
daemonStatus.FallbackReason = FallbackHealthFailed
daemonStatus.Detail = fmt.Sprintf("version mismatch (daemon: %s, client: %s) and restart failed",
daemonStatus.Detail = fmt.Sprintf("version mismatch (daemon: %s, client: %s) and restart failed",
health.Version, Version)
} else {
// Daemon is healthy and compatible - use it
daemonClient = client
daemonStatus.Mode = "daemon"
daemonStatus.Mode = cmdDaemon
daemonStatus.Connected = true
daemonStatus.Degraded = false
daemonStatus.Health = health.Status
@@ -306,12 +309,12 @@ var rootCmd = &cobra.Command{
absDBPath, _ := filepath.Abs(dbPath)
client.SetDatabasePath(absDBPath)
}
// Check health of auto-started daemon
health, healthErr := client.Health()
if healthErr == nil && health.Status == "healthy" {
if healthErr == nil && health.Status == statusHealthy {
daemonClient = client
daemonStatus.Mode = "daemon"
daemonStatus.Mode = cmdDaemon
daemonStatus.Connected = true
daemonStatus.Degraded = false
daemonStatus.AutoStartSucceeded = true
@@ -487,96 +490,9 @@ func shouldAutoStartDaemon() bool {
func shouldUseGlobalDaemon() bool {
// Global daemon support is deprecated
// Always use local daemon (per-project .beads/ socket)
return false
// Previously supported BEADS_PREFER_GLOBAL_DAEMON env var, but global
// daemon has issues with multi-workspace git workflows
// Heuristic: detect multiple beads repositories
home, err := os.UserHomeDir()
if err != nil {
return false
}
// Count .beads directories under home
repoCount := 0
maxDepth := 5 // Don't scan too deep
var countRepos func(string, int) error
countRepos = func(dir string, depth int) error {
if depth > maxDepth || repoCount > 1 {
return filepath.SkipDir
}
entries, err := os.ReadDir(dir)
if err != nil {
return nil // Skip directories we can't read
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
name := entry.Name()
// Skip hidden dirs except .beads
if strings.HasPrefix(name, ".") && name != ".beads" {
continue
}
// Skip common large directories
if name == "node_modules" || name == "vendor" || name == "target" || name == ".git" {
continue
}
path := filepath.Join(dir, name)
// Check if this is a .beads directory with a database
if name == ".beads" {
dbPath := filepath.Join(path, "db.sqlite")
if _, err := os.Stat(dbPath); err == nil {
repoCount++
if repoCount > 1 {
return filepath.SkipDir
}
}
continue
}
// Recurse into subdirectories
if depth < maxDepth {
_ = countRepos(path, depth+1)
}
}
return nil
}
// Scan common project directories
projectDirs := []string{
filepath.Join(home, "src"),
filepath.Join(home, "projects"),
filepath.Join(home, "code"),
filepath.Join(home, "workspace"),
filepath.Join(home, "dev"),
}
for _, dir := range projectDirs {
if _, err := os.Stat(dir); err == nil {
_ = countRepos(dir, 0)
if repoCount > 1 {
break
}
}
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: found %d beads repositories, prefer global: %v\n", repoCount, repoCount > 1)
}
// Use global daemon if we found more than 1 repository (multi-repo workflow)
// This prevents concurrency issues when multiple repos are being worked on
return repoCount > 1
return false
}
// restartDaemonForVersionMismatch stops the old daemon and starts a new one
@@ -599,7 +515,7 @@ func restartDaemonForVersionMismatch() bool {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: stopping old daemon (PID %d)\n", pid)
}
process, err := os.FindProcess(pid)
if err != nil {
if os.Getenv("BD_DEBUG") != "" {
@@ -629,11 +545,11 @@ func restartDaemonForVersionMismatch() bool {
// Force kill if still running
if isRunning, _ := isDaemonRunning(pidFile); isRunning {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: force killing old daemon\n")
}
_ = process.Kill()
forcedKill = true
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: force killing old daemon\n")
}
_ = process.Kill()
forcedKill = true
}
}
@@ -738,7 +654,7 @@ func tryAutoStartDaemon(socketPath string) bool {
if lockPID, err := readPIDFromFile(lockPath); err == nil {
if !isPIDAlive(lockPID) {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: lock is stale (PID %d dead), removing and retrying\n", lockPID)
fmt.Fprintf(os.Stderr, "Debug: lock is stale (PID %d dead), removing and retrying\n", lockPID)
}
_ = os.Remove(lockPath)
// Retry once
@@ -778,11 +694,11 @@ func tryAutoStartDaemon(socketPath string) bool {
// Socket is stale (connect failed and PID dead/missing) - safe to remove
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: socket is stale, cleaning up\n")
fmt.Fprintf(os.Stderr, "Debug: socket is stale, cleaning up\n")
}
_ = os.Remove(socketPath)
if pidFile != "" {
_ = os.Remove(pidFile)
_ = os.Remove(pidFile)
}
}
@@ -943,7 +859,7 @@ func recordDaemonStartFailure() {
func getSocketPath() string {
// Always use local socket (same directory as database: .beads/bd.sock)
localSocket := filepath.Join(filepath.Dir(dbPath), "bd.sock")
// Warn if old global socket exists
if home, err := os.UserHomeDir(); err == nil {
globalSocket := filepath.Join(home, ".beads", "bd.sock")
@@ -953,7 +869,7 @@ func getSocketPath() string {
fmt.Fprintf(os.Stderr, "To migrate: Stop the global daemon and restart with 'bd daemon' in each project.\n")
}
}
return localSocket
}
@@ -1100,10 +1016,10 @@ func autoImportIfNewer() {
// Use shared import logic (bd-157)
opts := ImportOptions{
ResolveCollisions: true, // Auto-import always resolves collisions
DryRun: false,
SkipUpdate: false,
Strict: false,
ResolveCollisions: true, // Auto-import always resolves collisions
DryRun: false,
SkipUpdate: false,
Strict: false,
SkipPrefixValidation: true, // Auto-import is lenient about prefixes
}
@@ -1928,100 +1844,100 @@ var showCmd = &cobra.Command{
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.Issue `json:"dependencies,omitempty"`
Dependents []*types.Issue `json:"dependents,omitempty"`
}
var details IssueDetails
if err := json.Unmarshal(resp.Data, &details); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
issue := &details.Issue
// Parse response and use existing formatting code
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 {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
issue := &details.Issue
cyan := color.New(color.FgCyan).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
// Format output (same as direct mode below)
tierEmoji := ""
statusSuffix := ""
if issue.CompactionLevel == 1 {
tierEmoji = " 🗜️"
} else if issue.CompactionLevel == 2 {
tierEmoji = " 📦"
}
if issue.CompactionLevel > 0 {
statusSuffix = fmt.Sprintf(" (compacted L%d)", issue.CompactionLevel)
}
// Format output (same as direct mode below)
tierEmoji := ""
statusSuffix := ""
switch issue.CompactionLevel {
case 1:
tierEmoji = " 🗜️"
statusSuffix = " (compacted L1)"
case 2:
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)
fmt.Printf("Type: %s\n", issue.IssueType)
if issue.Assignee != "" {
fmt.Printf("Assignee: %s\n", issue.Assignee)
}
if issue.EstimatedMinutes != nil {
fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
}
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"))
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)
fmt.Printf("Type: %s\n", issue.IssueType)
if issue.Assignee != "" {
fmt.Printf("Assignee: %s\n", issue.Assignee)
}
if issue.EstimatedMinutes != nil {
fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
}
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()
if issue.OriginalSize > 0 {
currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
saved := issue.OriginalSize - currentSize
if saved > 0 {
reduction := float64(saved) / float64(issue.OriginalSize) * 100
fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
issue.OriginalSize, currentSize, reduction)
// Show compaction status
if issue.CompactionLevel > 0 {
fmt.Println()
if issue.OriginalSize > 0 {
currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
saved := issue.OriginalSize - currentSize
if saved > 0 {
reduction := float64(saved) / float64(issue.OriginalSize) * 100
fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
issue.OriginalSize, currentSize, reduction)
}
}
tierEmoji2 := "🗜️"
if issue.CompactionLevel == 2 {
tierEmoji2 = "📦"
}
compactedDate := ""
if issue.CompactedAt != nil {
compactedDate = issue.CompactedAt.Format("2006-01-02")
}
fmt.Printf("%s Compacted: %s (Tier %d)\n", tierEmoji2, compactedDate, issue.CompactionLevel)
}
if issue.Description != "" {
fmt.Printf("\nDescription:\n%s\n", issue.Description)
}
if issue.Design != "" {
fmt.Printf("\nDesign:\n%s\n", issue.Design)
}
if issue.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", issue.Notes)
}
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("\nDepends on (%d):\n", len(details.Dependencies))
for _, dep := range details.Dependencies {
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
}
}
tierEmoji2 := "🗜️"
if issue.CompactionLevel == 2 {
tierEmoji2 = "📦"
}
compactedDate := ""
if issue.CompactedAt != nil {
compactedDate = issue.CompactedAt.Format("2006-01-02")
}
fmt.Printf("%s Compacted: %s (Tier %d)\n", tierEmoji2, compactedDate, issue.CompactionLevel)
}
if issue.Description != "" {
fmt.Printf("\nDescription:\n%s\n", issue.Description)
}
if issue.Design != "" {
fmt.Printf("\nDesign:\n%s\n", issue.Design)
}
if issue.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", issue.Notes)
}
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("\nDepends on (%d):\n", len(details.Dependencies))
for _, dep := range details.Dependencies {
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
if len(details.Dependents) > 0 {
fmt.Printf("\nBlocks (%d):\n", len(details.Dependents))
for _, dep := range details.Dependents {
fmt.Printf(" ← %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
}
}
}
if len(details.Dependents) > 0 {
fmt.Printf("\nBlocks (%d):\n", len(details.Dependents))
for _, dep := range details.Dependents {
fmt.Printf(" ← %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
}
}
fmt.Println()
}
@@ -2069,94 +1985,94 @@ var showCmd = &cobra.Command{
fmt.Println("\n" + strings.Repeat("─", 60))
}
cyan := color.New(color.FgCyan).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
// Add compaction emoji to title line
tierEmoji := ""
statusSuffix := ""
if issue.CompactionLevel == 1 {
tierEmoji = " 🗜️"
} else if issue.CompactionLevel == 2 {
tierEmoji = " 📦"
}
if issue.CompactionLevel > 0 {
statusSuffix = fmt.Sprintf(" (compacted L%d)", issue.CompactionLevel)
}
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)
fmt.Printf("Type: %s\n", issue.IssueType)
if issue.Assignee != "" {
fmt.Printf("Assignee: %s\n", issue.Assignee)
}
if issue.EstimatedMinutes != nil {
fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
}
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 := "🗜️"
if issue.CompactionLevel == 2 {
tierEmoji = "📦"
// Add compaction emoji to title line
tierEmoji := ""
statusSuffix := ""
switch issue.CompactionLevel {
case 1:
tierEmoji = " 🗜️"
statusSuffix = " (compacted L1)"
case 2:
tierEmoji = " 📦"
statusSuffix = " (compacted L2)"
}
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)
saved := issue.OriginalSize - currentSize
if saved > 0 {
reduction := float64(saved) / float64(issue.OriginalSize) * 100
fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
issue.OriginalSize, currentSize, reduction)
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)
fmt.Printf("Type: %s\n", issue.IssueType)
if issue.Assignee != "" {
fmt.Printf("Assignee: %s\n", issue.Assignee)
}
if issue.EstimatedMinutes != nil {
fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
}
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 := "🗜️"
if issue.CompactionLevel == 2 {
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)
saved := issue.OriginalSize - currentSize
if saved > 0 {
reduction := float64(saved) / float64(issue.OriginalSize) * 100
fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
issue.OriginalSize, currentSize, reduction)
}
}
compactedDate := ""
if issue.CompactedAt != nil {
compactedDate = issue.CompactedAt.Format("2006-01-02")
}
fmt.Printf("%s Compacted: %s (%s)\n", tierEmoji, compactedDate, tierName)
}
if issue.Description != "" {
fmt.Printf("\nDescription:\n%s\n", issue.Description)
}
if issue.Design != "" {
fmt.Printf("\nDesign:\n%s\n", issue.Design)
}
if issue.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", issue.Notes)
}
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
deps, _ := store.GetDependencies(ctx, issue.ID)
if len(deps) > 0 {
fmt.Printf("\nDepends on (%d):\n", len(deps))
for _, dep := range deps {
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
}
}
compactedDate := ""
if issue.CompactedAt != nil {
compactedDate = issue.CompactedAt.Format("2006-01-02")
// 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)
}
}
fmt.Printf("%s Compacted: %s (%s)\n", tierEmoji, compactedDate, tierName)
}
if issue.Description != "" {
fmt.Printf("\nDescription:\n%s\n", issue.Description)
}
if issue.Design != "" {
fmt.Printf("\nDesign:\n%s\n", issue.Design)
}
if issue.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", issue.Notes)
}
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
deps, _ := store.GetDependencies(ctx, issue.ID)
if len(deps) > 0 {
fmt.Printf("\nDepends on (%d):\n", len(deps))
for _, dep := range deps {
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, 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)
@@ -2200,30 +2116,30 @@ var updateCmd = &cobra.Command{
updates["title"] = title
}
if cmd.Flags().Changed("assignee") {
assignee, _ := cmd.Flags().GetString("assignee")
updates["assignee"] = assignee
assignee, _ := cmd.Flags().GetString("assignee")
updates["assignee"] = assignee
}
if cmd.Flags().Changed("description") {
description, _ := cmd.Flags().GetString("description")
updates["description"] = description
description, _ := cmd.Flags().GetString("description")
updates["description"] = description
}
if cmd.Flags().Changed("design") {
design, _ := cmd.Flags().GetString("design")
updates["design"] = design
}
if cmd.Flags().Changed("design") {
design, _ := cmd.Flags().GetString("design")
updates["design"] = design
}
if cmd.Flags().Changed("notes") {
notes, _ := cmd.Flags().GetString("notes")
updates["notes"] = notes
}
if cmd.Flags().Changed("acceptance") || cmd.Flags().Changed("acceptance-criteria") {
var acceptanceCriteria string
if cmd.Flags().Changed("acceptance") {
acceptanceCriteria, _ = cmd.Flags().GetString("acceptance")
} else {
acceptanceCriteria, _ = cmd.Flags().GetString("acceptance-criteria")
var acceptanceCriteria string
if cmd.Flags().Changed("acceptance") {
acceptanceCriteria, _ = cmd.Flags().GetString("acceptance")
} else {
acceptanceCriteria, _ = cmd.Flags().GetString("acceptance-criteria")
}
updates["acceptance_criteria"] = acceptanceCriteria
}
updates["acceptance_criteria"] = acceptanceCriteria
}
if cmd.Flags().Changed("external-ref") {
externalRef, _ := cmd.Flags().GetString("external-ref")
updates["external_ref"] = externalRef
@@ -2251,14 +2167,14 @@ var updateCmd = &cobra.Command{
updateArgs.Title = &title
}
if assignee, ok := updates["assignee"].(string); ok {
updateArgs.Assignee = &assignee
updateArgs.Assignee = &assignee
}
if description, ok := updates["description"].(string); ok {
updateArgs.Description = &description
updateArgs.Description = &description
}
if design, ok := updates["design"].(string); ok {
updateArgs.Design = &design
}
if design, ok := updates["design"].(string); ok {
updateArgs.Design = &design
}
if notes, ok := updates["notes"].(string); ok {
updateArgs.Notes = &notes
}

View File

@@ -7,7 +7,6 @@ import (
"os"
"regexp"
"sort"
"strconv"
"strings"
"github.com/fatih/color"
@@ -358,12 +357,6 @@ func renumberDependencies(ctx context.Context, idMapping map[string]string, allD
return nil
}
// Helper to extract numeric part from issue ID
func extractNumber(issueID, prefix string) (int, error) {
numStr := strings.TrimPrefix(issueID, prefix+"-")
return strconv.Atoi(numStr)
}
func init() {
renumberCmd.Flags().Bool("dry-run", false, "Preview changes without applying them")
renumberCmd.Flags().Bool("force", false, "Actually perform the renumbering")