refactor(ui): standardize on lipgloss semantic color system

Replace all fatih/color usages with internal/ui package that provides:
- Semantic color tokens (Pass, Warn, Fail, Accent, Muted)
- Adaptive light/dark mode support via Lipgloss AdaptiveColor
- Ayu theme colors for consistent, accessible output
- Tufte-inspired data-ink ratio principles

Files migrated: 35 command files in cmd/bd/

Add docs/ui-philosophy.md documenting:
- Semantic token usage guidelines
- Light/dark terminal optimization rationale
- Tufte and perceptual UI/UX theory application
- When to use (and not use) color in CLI output
This commit is contained in:
Ryan Snodgrass
2025-12-20 12:59:17 -08:00
parent fb1dff4f56
commit 6ca141712c
40 changed files with 887 additions and 646 deletions
+7 -9
View File
@@ -14,11 +14,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/steveyegge/beads/internal/beads" "github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/debug" "github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
@@ -566,10 +566,9 @@ func flushToJSONLWithState(state flushState) {
// Show prominent warning after 3+ consecutive failures // Show prominent warning after 3+ consecutive failures
if failCount >= 3 { if failCount >= 3 {
red := color.New(color.FgRed, color.Bold).SprintFunc() fmt.Fprintf(os.Stderr, "\n%s\n", ui.RenderFail("⚠️ CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!"))
fmt.Fprintf(os.Stderr, "\n%s\n", red("⚠️ CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!")) fmt.Fprintf(os.Stderr, "%s\n", ui.RenderFail("⚠️ Your JSONL file may be out of sync with the database."))
fmt.Fprintf(os.Stderr, "%s\n", red("⚠️ Your JSONL file may be out of sync with the database.")) fmt.Fprintf(os.Stderr, "%s\n\n", ui.RenderFail("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
fmt.Fprintf(os.Stderr, "%s\n\n", red("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
} }
return return
} }
@@ -601,10 +600,9 @@ func flushToJSONLWithState(state flushState) {
// Show prominent warning after 3+ consecutive failures // Show prominent warning after 3+ consecutive failures
if failCount >= 3 { if failCount >= 3 {
red := color.New(color.FgRed, color.Bold).SprintFunc() fmt.Fprintf(os.Stderr, "\n%s\n", ui.RenderFail("⚠️ CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!"))
fmt.Fprintf(os.Stderr, "\n%s\n", red("⚠️ CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!")) fmt.Fprintf(os.Stderr, "%s\n", ui.RenderFail("⚠️ Your JSONL file may be out of sync with the database."))
fmt.Fprintf(os.Stderr, "%s\n", red("⚠️ Your JSONL file may be out of sync with the database.")) fmt.Fprintf(os.Stderr, "%s\n\n", ui.RenderFail("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
fmt.Fprintf(os.Stderr, "%s\n\n", red("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
} }
} }
+6 -4
View File
@@ -7,13 +7,15 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/ui"
) )
// TODO: Consider consolidating into 'bd doctor --fix' for simpler maintenance UX
var cleanCmd = &cobra.Command{ var cleanCmd = &cobra.Command{
Use: "clean", Use: "clean",
Short: "Clean up temporary git merge artifacts from .beads directory", GroupID: "maint",
Short: "Clean up temporary git merge artifacts from .beads directory",
Long: `Delete temporary git merge artifacts from the .beads directory. Long: `Delete temporary git merge artifacts from the .beads directory.
This command removes temporary files created during git merges and conflicts. This command removes temporary files created during git merges and conflicts.
@@ -76,7 +78,7 @@ SEE ALSO:
// Just run by default, no --force needed // Just run by default, no --force needed
if dryRun { if dryRun {
fmt.Println(color.YellowString("DRY RUN - no changes will be made")) fmt.Println(ui.RenderWarn("DRY RUN - no changes will be made"))
} }
fmt.Printf("Found %d file(s) to clean:\n", len(filesToDelete)) fmt.Printf("Found %d file(s) to clean:\n", len(filesToDelete))
for _, file := range filesToDelete { for _, file := range filesToDelete {
+8 -7
View File
@@ -6,16 +6,18 @@ import (
"os" "os"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// Hard delete mode: bypass tombstone TTL safety, use --older-than days directly // Hard delete mode: bypass tombstone TTL safety, use --older-than days directly
// TODO: Consider consolidating into 'bd doctor --fix' for simpler maintenance UX
var cleanupCmd = &cobra.Command{ var cleanupCmd = &cobra.Command{
Use: "cleanup", Use: "cleanup",
Short: "Delete closed issues and prune expired tombstones", GroupID: "maint",
Short: "Delete closed issues and prune expired tombstones",
Long: `Delete closed issues and prune expired tombstones to reduce database size. Long: `Delete closed issues and prune expired tombstones to reduce database size.
This command: This command:
@@ -82,7 +84,7 @@ SEE ALSO:
customTTL = -1 customTTL = -1
} }
if !jsonOutput && !dryRun { if !jsonOutput && !dryRun {
fmt.Println(color.YellowString("⚠️ HARD DELETE MODE: Bypassing tombstone TTL safety")) fmt.Println(ui.RenderWarn("⚠️ HARD DELETE MODE: Bypassing tombstone TTL safety"))
} }
} }
@@ -197,7 +199,7 @@ SEE ALSO:
fmt.Printf("Found %d %s issue(s)\n", len(closedIssues), issueType) fmt.Printf("Found %d %s issue(s)\n", len(closedIssues), issueType)
} }
if dryRun { if dryRun {
fmt.Println(color.YellowString("DRY RUN - no changes will be made")) fmt.Println(ui.RenderWarn("DRY RUN - no changes will be made"))
} }
fmt.Println() fmt.Println()
} }
@@ -235,13 +237,12 @@ SEE ALSO:
} }
} else if tombstoneResult != nil && tombstoneResult.PrunedCount > 0 { } else if tombstoneResult != nil && tombstoneResult.PrunedCount > 0 {
if !jsonOutput { if !jsonOutput {
green := color.New(color.FgGreen).SprintFunc()
ttlMsg := fmt.Sprintf("older than %d days", tombstoneResult.TTLDays) ttlMsg := fmt.Sprintf("older than %d days", tombstoneResult.TTLDays)
if hardDelete && olderThanDays == 0 { if hardDelete && olderThanDays == 0 {
ttlMsg = "all tombstones (--hard mode)" ttlMsg = "all tombstones (--hard mode)"
} }
fmt.Printf("\n%s Pruned %d expired tombstone(s) (%s)\n", fmt.Printf("\n%s Pruned %d expired tombstone(s) (%s)\n",
green("✓"), tombstoneResult.PrunedCount, ttlMsg) ui.RenderPass("✓"), tombstoneResult.PrunedCount, ttlMsg)
} }
} }
} }
+6 -9
View File
@@ -6,7 +6,6 @@ import (
"os" "os"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/debug" "github.com/steveyegge/beads/internal/debug"
@@ -14,11 +13,13 @@ import (
"github.com/steveyegge/beads/internal/routing" "github.com/steveyegge/beads/internal/routing"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/validation" "github.com/steveyegge/beads/internal/validation"
) )
var createCmd = &cobra.Command{ var createCmd = &cobra.Command{
Use: "create [title]", Use: "create [title]",
GroupID: "issues",
Aliases: []string{"new"}, Aliases: []string{"new"},
Short: "Create a new issue (or multiple issues from markdown file)", Short: "Create a new issue (or multiple issues from markdown file)",
Args: cobra.MinimumNArgs(0), // Changed to allow no args when using -f Args: cobra.MinimumNArgs(0), // Changed to allow no args when using -f
@@ -59,8 +60,7 @@ var createCmd = &cobra.Command{
// Warn if creating a test issue in production database (unless silent mode) // Warn if creating a test issue in production database (unless silent mode)
if strings.HasPrefix(strings.ToLower(title), "test") && !silent && !debug.IsQuiet() { if strings.HasPrefix(strings.ToLower(title), "test") && !silent && !debug.IsQuiet() {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Fprintf(os.Stderr, "%s Creating issue with 'Test' prefix in production database.\n", ui.RenderWarn("⚠"))
fmt.Fprintf(os.Stderr, "%s Creating issue with 'Test' prefix in production database.\n", yellow("⚠"))
fmt.Fprintf(os.Stderr, " For testing, consider using: BEADS_DB=/tmp/test.db ./bd create \"Test issue\"\n") fmt.Fprintf(os.Stderr, " For testing, consider using: BEADS_DB=/tmp/test.db ./bd create \"Test issue\"\n")
} }
@@ -74,8 +74,7 @@ var createCmd = &cobra.Command{
} }
// Warn if creating an issue without a description (unless silent mode) // Warn if creating an issue without a description (unless silent mode)
if !silent && !debug.IsQuiet() { if !silent && !debug.IsQuiet() {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Fprintf(os.Stderr, "%s Creating issue without description.\n", ui.RenderWarn("⚠"))
fmt.Fprintf(os.Stderr, "%s Creating issue without description.\n", yellow("⚠"))
fmt.Fprintf(os.Stderr, " Issues without descriptions lack context for future work.\n") fmt.Fprintf(os.Stderr, " Issues without descriptions lack context for future work.\n")
fmt.Fprintf(os.Stderr, " Consider adding --description=\"Why this issue exists and what needs to be done\"\n") fmt.Fprintf(os.Stderr, " Consider adding --description=\"Why this issue exists and what needs to be done\"\n")
} }
@@ -241,8 +240,7 @@ var createCmd = &cobra.Command{
} else if silent { } else if silent {
fmt.Println(issue.ID) fmt.Println(issue.ID)
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Created issue: %s\n", ui.RenderPass("✓"), issue.ID)
fmt.Printf("%s Created issue: %s\n", green("✓"), issue.ID)
fmt.Printf(" Title: %s\n", issue.Title) fmt.Printf(" Title: %s\n", issue.Title)
fmt.Printf(" Priority: P%d\n", issue.Priority) fmt.Printf(" Priority: P%d\n", issue.Priority)
fmt.Printf(" Status: %s\n", issue.Status) fmt.Printf(" Status: %s\n", issue.Status)
@@ -381,8 +379,7 @@ var createCmd = &cobra.Command{
} else if silent { } else if silent {
fmt.Println(issue.ID) fmt.Println(issue.ID)
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Created issue: %s\n", ui.RenderPass("✓"), issue.ID)
fmt.Printf("%s Created issue: %s\n", green("✓"), issue.ID)
fmt.Printf(" Title: %s\n", issue.Title) fmt.Printf(" Title: %s\n", issue.Title)
fmt.Printf(" Priority: P%d\n", issue.Priority) fmt.Printf(" Priority: P%d\n", issue.Priority)
fmt.Printf(" Status: %s\n", issue.Status) fmt.Printf(" Status: %s\n", issue.Status)
+5 -5
View File
@@ -9,11 +9,11 @@ import (
"strings" "strings"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// createFormRawInput holds the raw string values from the form UI. // createFormRawInput holds the raw string values from the form UI.
@@ -198,8 +198,9 @@ func CreateIssueFromFormValues(ctx context.Context, s storage.Storage, fv *creat
} }
var createFormCmd = &cobra.Command{ var createFormCmd = &cobra.Command{
Use: "create-form", Use: "create-form",
Short: "Create a new issue using an interactive form", GroupID: "issues",
Short: "Create a new issue using an interactive form",
Long: `Create a new issue using an interactive terminal form. Long: `Create a new issue using an interactive terminal form.
This command provides a user-friendly form interface for creating issues, This command provides a user-friendly form interface for creating issues,
@@ -388,8 +389,7 @@ func runCreateForm(cmd *cobra.Command) {
} }
func printCreatedIssue(issue *types.Issue) { func printCreatedIssue(issue *types.Issue) {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s Created issue: %s\n", ui.RenderPass("✓"), issue.ID)
fmt.Printf("\n%s Created issue: %s\n", green("✓"), issue.ID)
fmt.Printf(" Title: %s\n", issue.Title) fmt.Printf(" Title: %s\n", issue.Title)
fmt.Printf(" Type: %s\n", issue.IssueType) fmt.Printf(" Type: %s\n", issue.IssueType)
fmt.Printf(" Priority: P%d\n", issue.Priority) fmt.Printf(" Priority: P%d\n", issue.Priority)
+22 -31
View File
@@ -10,11 +10,11 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// deleteViaDaemon uses the RPC daemon to delete issues // deleteViaDaemon uses the RPC daemon to delete issues
@@ -62,19 +62,17 @@ func deleteViaDaemon(issueIDs []string, force, dryRun, cascade bool, jsonOutput
deletedCount := int(result["deleted_count"].(float64)) deletedCount := int(result["deleted_count"].(float64))
totalCount := int(result["total_count"].(float64)) totalCount := int(result["total_count"].(float64))
green := color.New(color.FgGreen).SprintFunc()
if deletedCount > 0 { if deletedCount > 0 {
if deletedCount == 1 { if deletedCount == 1 {
fmt.Printf("%s Deleted %s\n", green("✓"), issueIDs[0]) fmt.Printf("%s Deleted %s\n", ui.RenderPass("✓"), issueIDs[0])
} else { } else {
fmt.Printf("%s Deleted %d issue(s)\n", green("✓"), deletedCount) fmt.Printf("%s Deleted %d issue(s)\n", ui.RenderPass("✓"), deletedCount)
} }
} }
if errors, ok := result["errors"].([]interface{}); ok && len(errors) > 0 { if errors, ok := result["errors"].([]interface{}); ok && len(errors) > 0 {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("\n%s Warnings:\n", ui.RenderWarn("⚠"))
fmt.Printf("\n%s Warnings:\n", yellow("⚠"))
for _, e := range errors { for _, e := range errors {
fmt.Printf(" %s\n", e) fmt.Printf(" %s\n", e)
} }
@@ -85,8 +83,9 @@ func deleteViaDaemon(issueIDs []string, force, dryRun, cascade bool, jsonOutput
} }
var deleteCmd = &cobra.Command{ var deleteCmd = &cobra.Command{
Use: "delete <issue-id> [issue-id...]", Use: "delete <issue-id> [issue-id...]",
Short: "Delete one or more issues and clean up references", GroupID: "issues",
Short: "Delete one or more issues and clean up references",
Long: `Delete one or more issues and clean up all references to them. Long: `Delete one or more issues and clean up all references to them.
This command will: This command will:
1. Remove all dependency links (any type, both directions) involving the issues 1. Remove all dependency links (any type, both directions) involving the issues
@@ -215,9 +214,7 @@ the issues will not resurrect from remote branches.`,
replacementText := `$1[deleted:` + issueID + `]$3` replacementText := `$1[deleted:` + issueID + `]$3`
// Preview mode // Preview mode
if !force { if !force {
red := color.New(color.FgRed).SprintFunc() fmt.Printf("\n%s\n", ui.RenderFail("⚠️ DELETE PREVIEW"))
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s\n", red("⚠️ DELETE PREVIEW"))
fmt.Printf("\nIssue to delete:\n") fmt.Printf("\nIssue to delete:\n")
fmt.Printf(" %s: %s\n", issueID, issue.Title) fmt.Printf(" %s: %s\n", issueID, issue.Title)
totalDeps := len(depRecords) + len(dependents) totalDeps := len(depRecords) + len(dependents)
@@ -248,8 +245,8 @@ the issues will not resurrect from remote branches.`,
fmt.Printf(" (none have text references)\n") fmt.Printf(" (none have text references)\n")
} }
} }
fmt.Printf("\n%s\n", yellow("This operation cannot be undone!")) fmt.Printf("\n%s\n", ui.RenderWarn("This operation cannot be undone!"))
fmt.Printf("To proceed, run: %s\n\n", yellow("bd delete "+issueID+" --force")) fmt.Printf("To proceed, run: %s\n\n", ui.RenderWarn("bd delete "+issueID+" --force"))
return return
} }
// Actually delete // Actually delete
@@ -323,8 +320,7 @@ the issues will not resurrect from remote branches.`,
"references_updated": updatedIssueCount, "references_updated": updatedIssueCount,
}) })
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Deleted %s\n", ui.RenderPass("✓"), issueID)
fmt.Printf("%s Deleted %s\n", green("✓"), issueID)
fmt.Printf(" Removed %d dependency link(s)\n", totalDepsRemoved) fmt.Printf(" Removed %d dependency link(s)\n", totalDepsRemoved)
fmt.Printf(" Updated text references in %d issue(s)\n", updatedIssueCount) fmt.Printf(" Updated text references in %d issue(s)\n", updatedIssueCount)
} }
@@ -476,14 +472,13 @@ func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, c
if dryRun { if dryRun {
fmt.Printf("\n(Dry-run mode - no changes made)\n") fmt.Printf("\n(Dry-run mode - no changes made)\n")
} else { } else {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("\n%s\n", ui.RenderWarn("This operation cannot be undone!"))
fmt.Printf("\n%s\n", yellow("This operation cannot be undone!"))
if cascade { if cascade {
fmt.Printf("To proceed with cascade deletion, run: %s\n", fmt.Printf("To proceed with cascade deletion, run: %s\n",
yellow("bd delete "+strings.Join(issueIDs, " ")+" --cascade --force")) ui.RenderWarn("bd delete "+strings.Join(issueIDs, " ")+" --cascade --force"))
} else { } else {
fmt.Printf("To proceed, run: %s\n", fmt.Printf("To proceed, run: %s\n",
yellow("bd delete "+strings.Join(issueIDs, " ")+" --force")) ui.RenderWarn("bd delete "+strings.Join(issueIDs, " ")+" --force"))
} }
} }
return return
@@ -527,7 +522,7 @@ func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, c
// Use 'bd cleanup --hard' after syncing to fully purge old tombstones. // Use 'bd cleanup --hard' after syncing to fully purge old tombstones.
if hardDelete { if hardDelete {
if !jsonOutput { if !jsonOutput {
fmt.Println(color.YellowString("⚠️ HARD DELETE MODE: Pruning tombstones from JSONL")) fmt.Println(ui.RenderWarn("⚠️ HARD DELETE MODE: Pruning tombstones from JSONL"))
fmt.Println(" Note: Tombstones kept in DB to prevent resurrection. Run 'bd sync' then 'bd cleanup --hard' to fully purge.") fmt.Println(" Note: Tombstones kept in DB to prevent resurrection. Run 'bd sync' then 'bd cleanup --hard' to fully purge.")
} }
// Prune tombstones from JSONL using negative TTL (immediate expiration) // Prune tombstones from JSONL using negative TTL (immediate expiration)
@@ -555,24 +550,20 @@ func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, c
"orphaned_issues": result.OrphanedIssues, "orphaned_issues": result.OrphanedIssues,
}) })
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Deleted %d issue(s)\n", ui.RenderPass("✓"), result.DeletedCount)
fmt.Printf("%s Deleted %d issue(s)\n", green("✓"), result.DeletedCount)
fmt.Printf(" Removed %d dependency link(s)\n", result.DependenciesCount) fmt.Printf(" Removed %d dependency link(s)\n", result.DependenciesCount)
fmt.Printf(" Removed %d label(s)\n", result.LabelsCount) fmt.Printf(" Removed %d label(s)\n", result.LabelsCount)
fmt.Printf(" Removed %d event(s)\n", result.EventsCount) fmt.Printf(" Removed %d event(s)\n", result.EventsCount)
fmt.Printf(" Updated text references in %d issue(s)\n", updatedCount) fmt.Printf(" Updated text references in %d issue(s)\n", updatedCount)
if len(result.OrphanedIssues) > 0 { if len(result.OrphanedIssues) > 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf(" %s Orphaned %d issue(s): %s\n", fmt.Printf(" %s Orphaned %d issue(s): %s\n",
yellow("⚠"), len(result.OrphanedIssues), strings.Join(result.OrphanedIssues, ", ")) ui.RenderWarn("⚠"), len(result.OrphanedIssues), strings.Join(result.OrphanedIssues, ", "))
} }
} }
} }
// showDeletionPreview shows what would be deleted // showDeletionPreview shows what would be deleted
func showDeletionPreview(issueIDs []string, issues map[string]*types.Issue, cascade bool, depError error) { func showDeletionPreview(issueIDs []string, issues map[string]*types.Issue, cascade bool, depError error) {
red := color.New(color.FgRed).SprintFunc() fmt.Printf("\n%s\n", ui.RenderFail("⚠️ DELETE PREVIEW"))
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s\n", red("⚠️ DELETE PREVIEW"))
fmt.Printf("\nIssues to delete (%d):\n", len(issueIDs)) fmt.Printf("\nIssues to delete (%d):\n", len(issueIDs))
for _, id := range issueIDs { for _, id := range issueIDs {
if issue := issues[id]; issue != nil { if issue := issues[id]; issue != nil {
@@ -580,10 +571,10 @@ func showDeletionPreview(issueIDs []string, issues map[string]*types.Issue, casc
} }
} }
if cascade { if cascade {
fmt.Printf("\n%s Cascade mode enabled - will also delete all dependent issues\n", yellow("⚠")) fmt.Printf("\n%s Cascade mode enabled - will also delete all dependent issues\n", ui.RenderWarn("⚠"))
} }
if depError != nil { if depError != nil {
fmt.Printf("\n%s\n", red(depError.Error())) fmt.Printf("\n%s\n", ui.RenderFail(depError.Error()))
} }
} }
// updateTextReferencesInIssues updates text references to deleted issues in pre-collected connected issues // updateTextReferencesInIssues updates text references to deleted issues in pre-collected connected issues
+21 -31
View File
@@ -7,17 +7,18 @@ import (
"os" "os"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var depCmd = &cobra.Command{ var depCmd = &cobra.Command{
Use: "dep", Use: "dep",
Short: "Manage dependencies", GroupID: "deps",
Short: "Manage dependencies",
} }
var depAddCmd = &cobra.Command{ var depAddCmd = &cobra.Command{
@@ -88,9 +89,8 @@ var depAddCmd = &cobra.Command{
return return
} }
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Added dependency: %s depends on %s (%s)\n", fmt.Printf("%s Added dependency: %s depends on %s (%s)\n",
green("✓"), args[0], args[1], depType) ui.RenderPass("✓"), args[0], args[1], depType)
return return
} }
@@ -114,8 +114,7 @@ var depAddCmd = &cobra.Command{
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to check for cycles: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: Failed to check for cycles: %v\n", err)
} else if len(cycles) > 0 { } else if len(cycles) > 0 {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Fprintf(os.Stderr, "\n%s Warning: Dependency cycle detected!\n", ui.RenderWarn("⚠"))
fmt.Fprintf(os.Stderr, "\n%s Warning: Dependency cycle detected!\n", yellow("⚠"))
fmt.Fprintf(os.Stderr, "This can hide issues from the ready work list and cause confusion.\n\n") fmt.Fprintf(os.Stderr, "This can hide issues from the ready work list and cause confusion.\n\n")
fmt.Fprintf(os.Stderr, "Cycle path:\n") fmt.Fprintf(os.Stderr, "Cycle path:\n")
for _, cycle := range cycles { for _, cycle := range cycles {
@@ -144,9 +143,8 @@ var depAddCmd = &cobra.Command{
return return
} }
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Added dependency: %s depends on %s (%s)\n", fmt.Printf("%s Added dependency: %s depends on %s (%s)\n",
green("✓"), fromID, toID, depType) ui.RenderPass("✓"), fromID, toID, depType)
}, },
} }
@@ -215,9 +213,8 @@ var depRemoveCmd = &cobra.Command{
return return
} }
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Removed dependency: %s no longer depends on %s\n", fmt.Printf("%s Removed dependency: %s no longer depends on %s\n",
green("✓"), fromID, toID) ui.RenderPass("✓"), fromID, toID)
return return
} }
@@ -242,9 +239,8 @@ var depRemoveCmd = &cobra.Command{
return return
} }
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Removed dependency: %s no longer depends on %s\n", fmt.Printf("%s Removed dependency: %s no longer depends on %s\n",
green("✓"), fullFromID, fullToID) ui.RenderPass("✓"), fullFromID, fullToID)
}, },
} }
@@ -388,14 +384,13 @@ Examples:
return return
} }
cyan := color.New(color.FgCyan).SprintFunc()
switch direction { switch direction {
case "up": case "up":
fmt.Printf("\n%s Dependent tree for %s:\n\n", cyan("🌲"), fullID) fmt.Printf("\n%s Dependent tree for %s:\n\n", ui.RenderAccent("🌲"), fullID)
case "both": case "both":
fmt.Printf("\n%s Full dependency graph for %s:\n\n", cyan("🌲"), fullID) fmt.Printf("\n%s Full dependency graph for %s:\n\n", ui.RenderAccent("🌲"), fullID)
default: default:
fmt.Printf("\n%s Dependency tree for %s:\n\n", cyan("🌲"), fullID) fmt.Printf("\n%s Dependency tree for %s:\n\n", ui.RenderAccent("🌲"), fullID)
} }
// Render tree with proper connectors // Render tree with proper connectors
@@ -436,13 +431,11 @@ var depCyclesCmd = &cobra.Command{
} }
if len(cycles) == 0 { if len(cycles) == 0 {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s No dependency cycles detected\n\n", ui.RenderPass("✓"))
fmt.Printf("\n%s No dependency cycles detected\n\n", green("✓"))
return return
} }
red := color.New(color.FgRed).SprintFunc() fmt.Printf("\n%s Found %d dependency cycles:\n\n", ui.RenderFail("⚠"), len(cycles))
fmt.Printf("\n%s Found %d dependency cycles:\n\n", red("⚠"), len(cycles))
for i, cycle := range cycles { for i, cycle := range cycles {
fmt.Printf("%d. Cycle involving:\n", i+1) fmt.Printf("%d. Cycle involving:\n", i+1)
for _, issue := range cycle { for _, issue := range cycle {
@@ -578,8 +571,7 @@ func (r *treeRenderer) renderNode(node *types.TreeNode, children map[string][]*t
// Check if we've seen this node before (diamond dependency) // Check if we've seen this node before (diamond dependency)
if r.seen[node.ID] { if r.seen[node.ID] {
gray := color.New(color.FgHiBlack).SprintFunc() fmt.Printf("%s%s (shown above)\n", prefix.String(), ui.RenderMuted(node.ID))
fmt.Printf("%s%s (shown above)\n", prefix.String(), gray(node.ID))
return return
} }
r.seen[node.ID] = true r.seen[node.ID] = true
@@ -589,8 +581,7 @@ func (r *treeRenderer) renderNode(node *types.TreeNode, children map[string][]*t
// Add truncation warning if at max depth and has children // Add truncation warning if at max depth and has children
if node.Truncated || (depth == r.maxDepth && len(children[node.ID]) > 0) { if node.Truncated || (depth == r.maxDepth && len(children[node.ID]) > 0) {
yellow := color.New(color.FgYellow).SprintFunc() line += ui.RenderWarn(" …")
line += yellow(" …")
} }
fmt.Printf("%s%s\n", prefix.String(), line) fmt.Printf("%s%s\n", prefix.String(), line)
@@ -613,13 +604,13 @@ func formatTreeNode(node *types.TreeNode) string {
var idStr string var idStr string
switch node.Status { switch node.Status {
case types.StatusOpen: case types.StatusOpen:
idStr = color.New(color.FgWhite).Sprint(node.ID) idStr = ui.StatusOpenStyle.Render(node.ID)
case types.StatusInProgress: case types.StatusInProgress:
idStr = color.New(color.FgYellow).Sprint(node.ID) idStr = ui.StatusInProgressStyle.Render(node.ID)
case types.StatusBlocked: case types.StatusBlocked:
idStr = color.New(color.FgRed).Sprint(node.ID) idStr = ui.StatusBlockedStyle.Render(node.ID)
case types.StatusClosed: case types.StatusClosed:
idStr = color.New(color.FgGreen).Sprint(node.ID) idStr = ui.StatusClosedStyle.Render(node.ID)
default: default:
idStr = node.ID idStr = node.ID
} }
@@ -632,8 +623,7 @@ func formatTreeNode(node *types.TreeNode) string {
// An issue is ready if it's open and has no blocking dependencies // An issue is ready if it's open and has no blocking dependencies
// (In the tree view, depth 0 with status open implies ready in the "down" direction) // (In the tree view, depth 0 with status open implies ready in the "down" direction)
if node.Status == types.StatusOpen && node.Depth == 0 { if node.Status == types.StatusOpen && node.Depth == 0 {
green := color.New(color.FgGreen, color.Bold).SprintFunc() line += " " + ui.PassStyle.Bold(true).Render("[READY]")
line += " " + green("[READY]")
} }
return line return line
+6 -5
View File
@@ -7,14 +7,16 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// TODO: Consider consolidating into 'bd doctor --fix' for simpler maintenance UX
var detectPollutionCmd = &cobra.Command{ var detectPollutionCmd = &cobra.Command{
Use: "detect-pollution", Use: "detect-pollution",
Short: "Detect and optionally clean test issues from database", GroupID: "maint",
Short: "Detect and optionally clean test issues from database",
Long: `Detect test issues that leaked into production database using pattern matching. Long: `Detect test issues that leaked into production database using pattern matching.
This command finds issues that appear to be test data based on: This command finds issues that appear to be test data based on:
@@ -168,8 +170,7 @@ NOTE: Review detected issues carefully before using --clean. False positives are
// Schedule auto-flush // Schedule auto-flush
markDirtyAndScheduleFlush() markDirtyAndScheduleFlush()
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Deleted %d test issues\n", ui.RenderPass("✓"), deleted)
fmt.Printf("%s Deleted %d test issues\n", green("✓"), deleted)
fmt.Printf("\nCleanup complete. To restore, run: bd import %s\n", backupPath) fmt.Printf("\nCleanup complete. To restore, run: bd import %s\n", backupPath)
}, },
} }
+10 -10
View File
@@ -10,7 +10,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
_ "github.com/ncruces/go-sqlite3/driver" _ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed" _ "github.com/ncruces/go-sqlite3/embed"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -64,8 +63,9 @@ const ConfigKeyHintsDoctor = "hints.doctor"
const minSyncBranchHookVersion = "0.29.0" const minSyncBranchHookVersion = "0.29.0"
var doctorCmd = &cobra.Command{ var doctorCmd = &cobra.Command{
Use: "doctor [path]", Use: "doctor [path]",
Short: "Check beads installation health", GroupID: "maint",
Short: "Check and fix beads installation health (start here)",
Long: `Sanity check the beads installation for the current directory or specified path. Long: `Sanity check the beads installation for the current directory or specified path.
This command checks: This command checks:
@@ -205,9 +205,9 @@ func previewFixes(result doctorResult) {
// Show the issue details // Show the issue details
fmt.Printf(" %d. %s\n", i+1, issue.Name) fmt.Printf(" %d. %s\n", i+1, issue.Name)
if issue.Status == statusError { if issue.Status == statusError {
color.Red(" Status: ERROR\n") fmt.Printf(" Status: %s\n", ui.RenderFail("ERROR"))
} else { } else {
color.Yellow(" Status: WARNING\n") fmt.Printf(" Status: %s\n", ui.RenderWarn("WARNING"))
} }
fmt.Printf(" Issue: %s\n", issue.Message) fmt.Printf(" Issue: %s\n", issue.Message)
if issue.Detail != "" { if issue.Detail != "" {
@@ -286,9 +286,9 @@ func applyFixesInteractive(path string, issues []doctorCheck) {
// Show issue details // Show issue details
fmt.Printf("(%d/%d) %s\n", i+1, len(issues), issue.Name) fmt.Printf("(%d/%d) %s\n", i+1, len(issues), issue.Name)
if issue.Status == statusError { if issue.Status == statusError {
color.Red(" Status: ERROR\n") fmt.Printf(" Status: %s\n", ui.RenderFail("ERROR"))
} else { } else {
color.Yellow(" Status: WARNING\n") fmt.Printf(" Status: %s\n", ui.RenderWarn("WARNING"))
} }
fmt.Printf(" Issue: %s\n", issue.Message) fmt.Printf(" Issue: %s\n", issue.Message)
if issue.Detail != "" { if issue.Detail != "" {
@@ -401,11 +401,11 @@ func applyFixList(path string, fixes []doctorCheck) {
if err != nil { if err != nil {
errorCount++ errorCount++
color.Red(" Error: %v\n", err) fmt.Printf(" %s Error: %v\n", ui.RenderFail("✗"), err)
fmt.Printf(" Manual fix: %s\n", check.Fix) fmt.Printf(" Manual fix: %s\n", check.Fix)
} else { } else {
fixedCount++ fixedCount++
color.Green(" Fixed\n") fmt.Printf(" %s Fixed\n", ui.RenderPass("✓"))
} }
} }
@@ -886,7 +886,7 @@ func printDiagnostics(result doctorResult) {
} }
} else { } else {
fmt.Println() fmt.Println()
color.Green("✓ All checks passed\n") fmt.Printf("%s\n", ui.RenderPass("✓ All checks passed"))
} }
} }
+9 -9
View File
@@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var duplicateCmd = &cobra.Command{ var duplicateCmd = &cobra.Command{
Use: "duplicate <id> --of <canonical>", Use: "duplicate <id> --of <canonical>",
Short: "Mark an issue as a duplicate of another", GroupID: "deps",
Short: "Mark an issue as a duplicate of another",
Long: `Mark an issue as a duplicate of a canonical issue. Long: `Mark an issue as a duplicate of a canonical issue.
The duplicate issue is automatically closed with a reference to the canonical. The duplicate issue is automatically closed with a reference to the canonical.
@@ -27,8 +28,9 @@ Examples:
} }
var supersedeCmd = &cobra.Command{ var supersedeCmd = &cobra.Command{
Use: "supersede <id> --with <new>", Use: "supersede <id> --with <new>",
Short: "Mark an issue as superseded by a newer one", GroupID: "deps",
Short: "Mark an issue as superseded by a newer one",
Long: `Mark an issue as superseded by a newer version. Long: `Mark an issue as superseded by a newer version.
The superseded issue is automatically closed with a reference to the replacement. The superseded issue is automatically closed with a reference to the replacement.
@@ -149,8 +151,7 @@ func runDuplicate(cmd *cobra.Command, args []string) error {
return encoder.Encode(result) return encoder.Encode(result)
} }
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Marked %s as duplicate of %s (closed)\n", ui.RenderPass("✓"), duplicateID, canonicalID)
fmt.Printf("%s Marked %s as duplicate of %s (closed)\n", green("✓"), duplicateID, canonicalID)
return nil return nil
} }
@@ -248,7 +249,6 @@ func runSupersede(cmd *cobra.Command, args []string) error {
return encoder.Encode(result) return encoder.Encode(result)
} }
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Marked %s as superseded by %s (closed)\n", ui.RenderPass("✓"), oldID, newID)
fmt.Printf("%s Marked %s as superseded by %s (closed)\n", green("✓"), oldID, newID)
return nil return nil
} }
+12 -14
View File
@@ -4,13 +4,14 @@ import (
"os" "os"
"regexp" "regexp"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
var duplicatesCmd = &cobra.Command{ var duplicatesCmd = &cobra.Command{
Use: "duplicates", Use: "duplicates",
Short: "Find and optionally merge duplicate issues", GroupID: "deps",
Short: "Find and optionally merge duplicate issues",
Long: `Find issues with identical content (title, description, design, acceptance criteria). Long: `Find issues with identical content (title, description, design, acceptance criteria).
Groups issues by content hash and reports duplicates with suggested merge targets. Groups issues by content hash and reports duplicates with suggested merge targets.
The merge target is chosen by: The merge target is chosen by:
@@ -119,18 +120,15 @@ Example:
} }
outputJSON(output) outputJSON(output)
} else { } else {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("%s Found %d duplicate group(s):\n\n", ui.RenderWarn("🔍"), len(duplicateGroups))
cyan := color.New(color.FgCyan).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Found %d duplicate group(s):\n\n", yellow("🔍"), len(duplicateGroups))
for i, group := range duplicateGroups { for i, group := range duplicateGroups {
target := chooseMergeTarget(group, refCounts) target := chooseMergeTarget(group, refCounts)
fmt.Printf("%s Group %d: %s\n", cyan("━━"), i+1, group[0].Title) fmt.Printf("%s Group %d: %s\n", ui.RenderAccent("━━"), i+1, group[0].Title)
for _, issue := range group { for _, issue := range group {
refs := refCounts[issue.ID] refs := refCounts[issue.ID]
marker := " " marker := " "
if issue.ID == target.ID { if issue.ID == target.ID {
marker = green("→ ") marker = ui.RenderPass("→ ")
} }
fmt.Printf("%s%s (%s, P%d, %d references)\n", fmt.Printf("%s%s (%s, P%d, %d references)\n",
marker, issue.ID, issue.Status, issue.Priority, refs) marker, issue.ID, issue.Status, issue.Priority, refs)
@@ -141,18 +139,18 @@ Example:
sources = append(sources, issue.ID) sources = append(sources, issue.ID)
} }
} }
fmt.Printf(" %s Duplicate: %s (same content as %s)\n", cyan("Note:"), strings.Join(sources, " "), target.ID) fmt.Printf(" %s Duplicate: %s (same content as %s)\n", ui.RenderAccent("Note:"), strings.Join(sources, " "), target.ID)
fmt.Printf(" %s bd close %s && bd dep add %s %s --type related\n\n", fmt.Printf(" %s bd close %s && bd dep add %s %s --type related\n\n",
cyan("Suggested:"), strings.Join(sources, " "), strings.Join(sources, " "), target.ID) ui.RenderAccent("Suggested:"), strings.Join(sources, " "), strings.Join(sources, " "), target.ID)
} }
if autoMerge { if autoMerge {
if dryRun { if dryRun {
fmt.Printf("%s Dry run - would execute %d merge(s)\n", yellow("⚠"), len(mergeCommands)) fmt.Printf("%s Dry run - would execute %d merge(s)\n", ui.RenderWarn("⚠"), len(mergeCommands))
} else { } else {
fmt.Printf("%s Merged %d group(s)\n", green("✓"), len(mergeCommands)) fmt.Printf("%s Merged %d group(s)\n", ui.RenderPass("✓"), len(mergeCommands))
} }
} else { } else {
fmt.Printf("%s Run with --auto-merge to execute all suggested merges\n", cyan("💡")) fmt.Printf("%s Run with --auto-merge to execute all suggested merges\n", ui.RenderAccent("💡"))
} }
} }
}, },
+8 -11
View File
@@ -3,14 +3,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
var epicCmd = &cobra.Command{ var epicCmd = &cobra.Command{
Use: "epic", Use: "epic",
Short: "Epic management commands", GroupID: "deps",
Short: "Epic management commands",
} }
var epicStatusCmd = &cobra.Command{ var epicStatusCmd = &cobra.Command{
Use: "status", Use: "status",
@@ -67,10 +68,6 @@ var epicStatusCmd = &cobra.Command{
fmt.Println("No open epics found") fmt.Println("No open epics found")
return return
} }
cyan := color.New(color.FgCyan).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
for _, epicStatus := range epics { for _, epicStatus := range epics {
epic := epicStatus.Epic epic := epicStatus.Epic
percentage := 0 percentage := 0
@@ -79,17 +76,17 @@ var epicStatusCmd = &cobra.Command{
} }
statusIcon := "" statusIcon := ""
if epicStatus.EligibleForClose { if epicStatus.EligibleForClose {
statusIcon = green("✓") statusIcon = ui.RenderPass("✓")
} else if percentage > 0 { } else if percentage > 0 {
statusIcon = yellow("○") statusIcon = ui.RenderWarn("○")
} else { } else {
statusIcon = "○" statusIcon = "○"
} }
fmt.Printf("%s %s %s\n", statusIcon, cyan(epic.ID), bold(epic.Title)) fmt.Printf("%s %s %s\n", statusIcon, ui.RenderAccent(epic.ID), ui.RenderBold(epic.Title))
fmt.Printf(" Progress: %d/%d children closed (%d%%)\n", fmt.Printf(" Progress: %d/%d children closed (%d%%)\n",
epicStatus.ClosedChildren, epicStatus.TotalChildren, percentage) epicStatus.ClosedChildren, epicStatus.TotalChildren, percentage)
if epicStatus.EligibleForClose { if epicStatus.EligibleForClose {
fmt.Printf(" %s\n", green("Eligible for closure")) fmt.Printf(" %s\n", ui.RenderPass("Eligible for closure"))
} }
fmt.Println() fmt.Println()
} }
+26 -24
View File
@@ -8,11 +8,11 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
@@ -33,8 +33,9 @@ type GraphLayout struct {
} }
var graphCmd = &cobra.Command{ var graphCmd = &cobra.Command{
Use: "graph <issue-id>", Use: "graph <issue-id>",
Short: "Display issue dependency graph", GroupID: "deps",
Short: "Display issue dependency graph",
Long: `Display an ASCII visualization of an issue's dependency graph. Long: `Display an ASCII visualization of an issue's dependency graph.
For epics, shows all children and their dependencies. For epics, shows all children and their dependencies.
@@ -283,8 +284,7 @@ func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Dependency graph for %s:\n\n", ui.RenderAccent("📊"), layout.RootID)
fmt.Printf("\n%s Dependency graph for %s:\n\n", cyan("📊"), layout.RootID)
// Calculate box width based on longest title // Calculate box width based on longest title
maxTitleLen := 0 maxTitleLen := 0
@@ -370,33 +370,34 @@ func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
func renderNodeBox(node *GraphNode, width int) string { func renderNodeBox(node *GraphNode, width int) string {
// Status indicator // Status indicator
var statusIcon string var statusIcon string
var colorFn func(a ...interface{}) string var titleStr string
title := truncateTitle(node.Issue.Title, width-4)
switch node.Issue.Status { switch node.Issue.Status {
case types.StatusOpen: case types.StatusOpen:
statusIcon = "○" statusIcon = "○"
colorFn = color.New(color.FgWhite).SprintFunc() titleStr = padRight(title, width-4)
case types.StatusInProgress: case types.StatusInProgress:
statusIcon = "◐" statusIcon = "◐"
colorFn = color.New(color.FgYellow).SprintFunc() titleStr = ui.RenderWarn(padRight(title, width-4))
case types.StatusBlocked: case types.StatusBlocked:
statusIcon = "●" statusIcon = "●"
colorFn = color.New(color.FgRed).SprintFunc() titleStr = ui.RenderFail(padRight(title, width-4))
case types.StatusClosed: case types.StatusClosed:
statusIcon = "✓" statusIcon = "✓"
colorFn = color.New(color.FgGreen).SprintFunc() titleStr = ui.RenderPass(padRight(title, width-4))
default: default:
statusIcon = "?" statusIcon = "?"
colorFn = color.New(color.FgWhite).SprintFunc() titleStr = padRight(title, width-4)
} }
title := truncateTitle(node.Issue.Title, width-4)
id := node.Issue.ID id := node.Issue.ID
// Build the box // Build the box
topBottom := " ┌" + strings.Repeat("─", width) + "┐" topBottom := " ┌" + strings.Repeat("─", width) + "┐"
middle := fmt.Sprintf(" │ %s %s │", statusIcon, colorFn(padRight(title, width-4))) middle := fmt.Sprintf(" │ %s %s │", statusIcon, titleStr)
idLine := fmt.Sprintf(" │ %s │", color.New(color.FgHiBlack).Sprint(padRight(id, width-2))) idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
bottom := " └" + strings.Repeat("─", width) + "┘" bottom := " └" + strings.Repeat("─", width) + "┘"
return topBottom + "\n" + middle + "\n" + idLine + "\n" + bottom return topBottom + "\n" + middle + "\n" + idLine + "\n" + bottom
@@ -446,27 +447,28 @@ func computeDependencyCounts(subgraph *TemplateSubgraph) (blocks map[string]int,
func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedByCount int) string { func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedByCount int) string {
// Status indicator // Status indicator
var statusIcon string var statusIcon string
var colorFn func(a ...interface{}) string var titleStr string
title := truncateTitle(node.Issue.Title, width-4)
switch node.Issue.Status { switch node.Issue.Status {
case types.StatusOpen: case types.StatusOpen:
statusIcon = "○" statusIcon = "○"
colorFn = color.New(color.FgWhite).SprintFunc() titleStr = padRight(title, width-4)
case types.StatusInProgress: case types.StatusInProgress:
statusIcon = "◐" statusIcon = "◐"
colorFn = color.New(color.FgYellow).SprintFunc() titleStr = ui.RenderWarn(padRight(title, width-4))
case types.StatusBlocked: case types.StatusBlocked:
statusIcon = "●" statusIcon = "●"
colorFn = color.New(color.FgRed).SprintFunc() titleStr = ui.RenderFail(padRight(title, width-4))
case types.StatusClosed: case types.StatusClosed:
statusIcon = "✓" statusIcon = "✓"
colorFn = color.New(color.FgGreen).SprintFunc() titleStr = ui.RenderPass(padRight(title, width-4))
default: default:
statusIcon = "?" statusIcon = "?"
colorFn = color.New(color.FgWhite).SprintFunc() titleStr = padRight(title, width-4)
} }
title := truncateTitle(node.Issue.Title, width-4)
id := node.Issue.ID id := node.Issue.ID
// Build dependency info string // Build dependency info string
@@ -484,12 +486,12 @@ func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedB
// Build the box // Build the box
topBottom := " ┌" + strings.Repeat("─", width) + "┐" topBottom := " ┌" + strings.Repeat("─", width) + "┐"
middle := fmt.Sprintf(" │ %s %s │", statusIcon, colorFn(padRight(title, width-4))) middle := fmt.Sprintf(" │ %s %s │", statusIcon, titleStr)
idLine := fmt.Sprintf(" │ %s │", color.New(color.FgHiBlack).Sprint(padRight(id, width-2))) idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
var result string var result string
if depInfo != "" { if depInfo != "" {
depLine := fmt.Sprintf(" │ %s │", color.New(color.FgCyan).Sprint(padRight(depInfo, width-2))) depLine := fmt.Sprintf(" │ %s │", ui.RenderAccent(padRight(depInfo, width-2)))
bottom := " └" + strings.Repeat("─", width) + "┘" bottom := " └" + strings.Repeat("─", width) + "┘"
result = topBottom + "\n" + middle + "\n" + idLine + "\n" + depLine + "\n" + bottom result = topBottom + "\n" + middle + "\n" + idLine + "\n" + depLine + "\n" + bottom
} else { } else {
+31 -50
View File
@@ -11,7 +11,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor" "github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/internal/beads" "github.com/steveyegge/beads/internal/beads"
@@ -21,12 +20,14 @@ import (
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/syncbranch" "github.com/steveyegge/beads/internal/syncbranch"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var initCmd = &cobra.Command{ var initCmd = &cobra.Command{
Use: "init", Use: "init",
Short: "Initialize bd in the current directory", GroupID: "setup",
Short: "Initialize bd in the current directory",
Long: `Initialize bd in the current directory by creating a .beads/ directory Long: `Initialize bd in the current directory by creating a .beads/ directory
and database file. Optionally specify a custom issue prefix. and database file. Optionally specify a custom issue prefix.
@@ -225,15 +226,12 @@ With --stealth: configures global git settings for invisible beads usage:
} }
if !quiet { if !quiet {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s bd initialized successfully in --no-db mode!\n\n", ui.RenderPass("✓"))
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf(" Mode: %s\n", ui.RenderAccent("no-db (JSONL-only)"))
fmt.Printf(" Issues file: %s\n", ui.RenderAccent(jsonlPath))
fmt.Printf("\n%s bd initialized successfully in --no-db mode!\n\n", green("✓")) fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
fmt.Printf(" Mode: %s\n", cyan("no-db (JSONL-only)")) fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
fmt.Printf(" Issues file: %s\n", cyan(jsonlPath)) fmt.Printf("Run %s to get started.\n\n", ui.RenderAccent("bd --no-db quickstart"))
fmt.Printf(" Issue prefix: %s\n", cyan(prefix))
fmt.Printf(" Issues will be named: %s\n\n", cyan(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
fmt.Printf("Run %s to get started.\n\n", cyan("bd --no-db quickstart"))
} }
return return
} }
@@ -427,9 +425,8 @@ With --stealth: configures global git settings for invisible beads usage:
// Install by default unless --skip-hooks is passed // Install by default unless --skip-hooks is passed
if !skipHooks && isGitRepo() && !hooksInstalled() { if !skipHooks && isGitRepo() && !hooksInstalled() {
if err := installGitHooks(); err != nil && !quiet { if err := installGitHooks(); err != nil && !quiet {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", ui.RenderWarn("⚠"), err)
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", yellow("⚠"), err) fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", color.New(color.FgCyan).Sprint("bd doctor --fix"))
} }
} }
@@ -437,9 +434,8 @@ With --stealth: configures global git settings for invisible beads usage:
// Install by default unless --skip-merge-driver is passed // Install by default unless --skip-merge-driver is passed
if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() { if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() {
if err := installMergeDriver(); err != nil && !quiet { if err := installMergeDriver(); err != nil && !quiet {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Fprintf(os.Stderr, "\n%s Failed to install merge driver: %v\n", ui.RenderWarn("⚠"), err)
fmt.Fprintf(os.Stderr, "\n%s Failed to install merge driver: %v\n", yellow("⚠"), err) fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", color.New(color.FgCyan).Sprint("bd doctor --fix"))
} }
} }
@@ -454,14 +450,11 @@ With --stealth: configures global git settings for invisible beads usage:
return return
} }
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s bd initialized successfully!\n\n", ui.RenderPass("✓"))
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf(" Database: %s\n", ui.RenderAccent(initDBPath))
fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
fmt.Printf("\n%s bd initialized successfully!\n\n", green("✓")) fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
fmt.Printf(" Database: %s\n", cyan(initDBPath)) fmt.Printf("Run %s to get started.\n\n", ui.RenderAccent("bd quickstart"))
fmt.Printf(" Issue prefix: %s\n", cyan(prefix))
fmt.Printf(" Issues will be named: %s\n\n", cyan(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
fmt.Printf("Run %s to get started.\n\n", cyan("bd quickstart"))
// Run bd doctor diagnostics to catch setup issues early (bd-zwtq) // Run bd doctor diagnostics to catch setup issues early (bd-zwtq)
doctorResult := runDiagnostics(cwd) doctorResult := runDiagnostics(cwd)
@@ -474,15 +467,14 @@ With --stealth: configures global git settings for invisible beads usage:
} }
} }
if hasIssues { if hasIssues {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("%s Setup incomplete. Some issues were detected:\n", ui.RenderWarn("⚠"))
fmt.Printf("%s Setup incomplete. Some issues were detected:\n", yellow("⚠"))
// Show just the warnings/errors, not all checks // Show just the warnings/errors, not all checks
for _, check := range doctorResult.Checks { for _, check := range doctorResult.Checks {
if check.Status != statusOK { if check.Status != statusOK {
fmt.Printf(" • %s: %s\n", check.Name, check.Message) fmt.Printf(" • %s: %s\n", check.Name, check.Message)
} }
} }
fmt.Printf("\nRun %s to see details and fix these issues.\n\n", cyan("bd doctor --fix")) fmt.Printf("\nRun %s to see details and fix these issues.\n\n", ui.RenderAccent("bd doctor --fix"))
} }
}, },
} }
@@ -592,9 +584,7 @@ func detectExistingHooks() []hookInfo {
// promptHookAction asks user what to do with existing hooks // promptHookAction asks user what to do with existing hooks
func promptHookAction(existingHooks []hookInfo) string { func promptHookAction(existingHooks []hookInfo) string {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("\n%s Found existing git hooks:\n", ui.RenderWarn("⚠"))
fmt.Printf("\n%s Found existing git hooks:\n", yellow("⚠"))
for _, hook := range existingHooks { for _, hook := range existingHooks {
if hook.exists && !hook.isBdHook { if hook.exists && !hook.isBdHook {
hookType := "custom script" hookType := "custom script"
@@ -646,7 +636,6 @@ func installGitHooks() error {
// Determine installation mode // Determine installation mode
chainHooks := false chainHooks := false
if hasExistingHooks { if hasExistingHooks {
cyan := color.New(color.FgCyan).SprintFunc()
choice := promptHookAction(existingHooks) choice := promptHookAction(existingHooks)
switch choice { switch choice {
case "1", "": case "1", "":
@@ -665,7 +654,7 @@ func installGitHooks() error {
} }
case "3": case "3":
fmt.Printf("Skipping git hooks installation.\n") fmt.Printf("Skipping git hooks installation.\n")
fmt.Printf("You can install manually later with: %s\n", cyan("./examples/git-hooks/install.sh")) fmt.Printf("You can install manually later with: %s\n", ui.RenderAccent("./examples/git-hooks/install.sh"))
return nil return nil
default: default:
return fmt.Errorf("invalid choice: %s", choice) return fmt.Errorf("invalid choice: %s", choice)
@@ -971,8 +960,7 @@ exit 0
} }
if chainHooks { if chainHooks {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Chained bd hooks with existing hooks\n", ui.RenderPass("✓"))
fmt.Printf("%s Chained bd hooks with existing hooks\n", green("✓"))
} }
return nil return nil
@@ -1400,12 +1388,10 @@ func setupStealthMode(verbose bool) error {
} }
if verbose { if verbose {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s Stealth mode configured successfully!\n\n", ui.RenderPass("✓"))
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf(" Global gitignore: %s\n", ui.RenderAccent(projectPath+"/.beads/ ignored"))
fmt.Printf("\n%s Stealth mode configured successfully!\n\n", green("")) fmt.Printf(" Claude settings: %s\n\n", ui.RenderAccent("configured with bd onboard instruction"))
fmt.Printf(" Global gitignore: %s\n", cyan(projectPath+"/.beads/ ignored")) fmt.Printf("Your beads setup is now %s - other repo collaborators won't see any beads-related files.\n\n", ui.RenderAccent("invisible"))
fmt.Printf(" Claude settings: %s\n\n", cyan("configured with bd onboard instruction"))
fmt.Printf("Your beads setup is now %s - other repo collaborators won't see any beads-related files.\n\n", cyan("invisible"))
} }
return nil return nil
@@ -1550,9 +1536,6 @@ func checkExistingBeadsData(prefix string) error {
// Check for existing database file // Check for existing database file
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
if _, err := os.Stat(dbPath); err == nil { if _, err := os.Stat(dbPath); err == nil {
yellow := color.New(color.FgYellow).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
return fmt.Errorf(` return fmt.Errorf(`
%s Found existing database: %s %s Found existing database: %s
@@ -1564,7 +1547,7 @@ To use the existing database:
To completely reinitialize (data loss warning): To completely reinitialize (data loss warning):
rm -rf .beads && bd init --prefix %s rm -rf .beads && bd init --prefix %s
Aborting.`, yellow("⚠"), dbPath, cyan("bd list"), prefix) Aborting.`, ui.RenderWarn("⚠"), dbPath, ui.RenderAccent("bd list"), prefix)
} }
// Fresh clones (JSONL exists but no database) are allowed - init will // Fresh clones (JSONL exists but no database) are allowed - init will
@@ -1646,8 +1629,7 @@ bd sync # Sync with git
return fmt.Errorf("failed to create %s: %w", filename, err) return fmt.Errorf("failed to create %s: %w", filename, err)
} }
if verbose { if verbose {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf(" %s Created %s with landing-the-plane instructions\n", ui.RenderPass("✓"), filename)
fmt.Printf(" %s Created %s with landing-the-plane instructions\n", green("✓"), filename)
} }
return nil return nil
} else if err != nil { } else if err != nil {
@@ -1674,8 +1656,7 @@ bd sync # Sync with git
return fmt.Errorf("failed to update %s: %w", filename, err) return fmt.Errorf("failed to update %s: %w", filename, err)
} }
if verbose { if verbose {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf(" %s Added landing-the-plane instructions to %s\n", ui.RenderPass("✓"), filename)
fmt.Printf(" %s Added landing-the-plane instructions to %s\n", green("✓"), filename)
} }
return nil return nil
} }
+21 -26
View File
@@ -9,30 +9,25 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/fatih/color"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/ui"
) )
// runContributorWizard guides the user through OSS contributor setup // runContributorWizard guides the user through OSS contributor setup
func runContributorWizard(ctx context.Context, store storage.Storage) error { func runContributorWizard(ctx context.Context, store storage.Storage) error {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s %s\n\n", ui.RenderBold("bd"), ui.RenderBold("Contributor Workflow Setup Wizard"))
cyan := color.New(color.FgCyan).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
fmt.Printf("\n%s %s\n\n", bold("bd"), bold("Contributor Workflow Setup Wizard"))
fmt.Println("This wizard will configure beads for OSS contribution.") fmt.Println("This wizard will configure beads for OSS contribution.")
fmt.Println() fmt.Println()
// Step 1: Detect fork relationship // Step 1: Detect fork relationship
fmt.Printf("%s Detecting git repository setup...\n", cyan("▶")) fmt.Printf("%s Detecting git repository setup...\n", ui.RenderAccent("▶"))
isFork, upstreamURL := detectForkSetup() isFork, upstreamURL := detectForkSetup()
if isFork { if isFork {
fmt.Printf("%s Detected fork workflow (upstream: %s)\n", green("✓"), upstreamURL) fmt.Printf("%s Detected fork workflow (upstream: %s)\n", ui.RenderPass("✓"), upstreamURL)
} else { } else {
fmt.Printf("%s No upstream remote detected\n", yellow("⚠")) fmt.Printf("%s No upstream remote detected\n", ui.RenderWarn("⚠"))
fmt.Println("\n For fork workflows, add an 'upstream' remote:") fmt.Println("\n For fork workflows, add an 'upstream' remote:")
fmt.Println(" git remote add upstream <original-repo-url>") fmt.Println(" git remote add upstream <original-repo-url>")
fmt.Println() fmt.Println()
@@ -50,13 +45,13 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
} }
// Step 2: Check push access to origin // Step 2: Check push access to origin
fmt.Printf("\n%s Checking repository access...\n", cyan("▶")) fmt.Printf("\n%s Checking repository access...\n", ui.RenderAccent("▶"))
hasPushAccess, originURL := checkPushAccess() hasPushAccess, originURL := checkPushAccess()
if hasPushAccess { if hasPushAccess {
fmt.Printf("%s You have push access to origin (%s)\n", green("✓"), originURL) fmt.Printf("%s You have push access to origin (%s)\n", ui.RenderPass("✓"), originURL)
fmt.Printf(" %s You can commit directly to this repository.\n", yellow("⚠")) fmt.Printf(" %s You can commit directly to this repository.\n", ui.RenderWarn("⚠"))
fmt.Println() fmt.Println()
fmt.Print("Do you want to use a separate planning repo anyway? [Y/n]: ") fmt.Print("Do you want to use a separate planning repo anyway? [Y/n]: ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@@ -68,12 +63,12 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
return nil return nil
} }
} else { } else {
fmt.Printf("%s Read-only access to origin (%s)\n", green("✓"), originURL) fmt.Printf("%s Read-only access to origin (%s)\n", ui.RenderPass("✓"), originURL)
fmt.Println(" Planning repo recommended to keep experimental work separate.") fmt.Println(" Planning repo recommended to keep experimental work separate.")
} }
// Step 3: Configure planning repository // Step 3: Configure planning repository
fmt.Printf("\n%s Setting up planning repository...\n", cyan("▶")) fmt.Printf("\n%s Setting up planning repository...\n", ui.RenderAccent("▶"))
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
@@ -83,7 +78,7 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
defaultPlanningRepo := filepath.Join(homeDir, ".beads-planning") defaultPlanningRepo := filepath.Join(homeDir, ".beads-planning")
fmt.Printf("\nWhere should contributor planning issues be stored?\n") fmt.Printf("\nWhere should contributor planning issues be stored?\n")
fmt.Printf("Default: %s\n", cyan(defaultPlanningRepo)) fmt.Printf("Default: %s\n", ui.RenderAccent(defaultPlanningRepo))
fmt.Print("Planning repo path [press Enter for default]: ") fmt.Print("Planning repo path [press Enter for default]: ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@@ -101,7 +96,7 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
// Create planning repository if it doesn't exist // Create planning repository if it doesn't exist
if _, err := os.Stat(planningPath); os.IsNotExist(err) { if _, err := os.Stat(planningPath); os.IsNotExist(err) {
fmt.Printf("\nCreating planning repository at %s\n", cyan(planningPath)) fmt.Printf("\nCreating planning repository at %s\n", ui.RenderAccent(planningPath))
if err := os.MkdirAll(planningPath, 0750); err != nil { if err := os.MkdirAll(planningPath, 0750); err != nil {
return fmt.Errorf("failed to create planning repo directory: %w", err) return fmt.Errorf("failed to create planning repo directory: %w", err)
@@ -159,13 +154,13 @@ Created by: bd init --contributor
cmd.Dir = planningPath cmd.Dir = planningPath
_ = cmd.Run() _ = cmd.Run()
fmt.Printf("%s Planning repository created\n", green("✓")) fmt.Printf("%s Planning repository created\n", ui.RenderPass("✓"))
} else { } else {
fmt.Printf("%s Using existing planning repository\n", green("✓")) fmt.Printf("%s Using existing planning repository\n", ui.RenderPass("✓"))
} }
// Step 4: Configure contributor routing // Step 4: Configure contributor routing
fmt.Printf("\n%s Configuring contributor auto-routing...\n", cyan("▶")) fmt.Printf("\n%s Configuring contributor auto-routing...\n", ui.RenderAccent("▶"))
// Set contributor.planning_repo config // Set contributor.planning_repo config
if err := store.SetConfig(ctx, "contributor.planning_repo", planningPath); err != nil { if err := store.SetConfig(ctx, "contributor.planning_repo", planningPath); err != nil {
@@ -177,7 +172,7 @@ Created by: bd init --contributor
return fmt.Errorf("failed to enable auto-routing: %w", err) return fmt.Errorf("failed to enable auto-routing: %w", err)
} }
fmt.Printf("%s Auto-routing enabled\n", green("✓")) fmt.Printf("%s Auto-routing enabled\n", ui.RenderPass("✓"))
// If this is a fork, configure sync to pull beads from upstream (bd-bx9) // If this is a fork, configure sync to pull beads from upstream (bd-bx9)
// This ensures `bd sync` gets the latest issues from the source repo, // This ensures `bd sync` gets the latest issues from the source repo,
@@ -186,22 +181,22 @@ Created by: bd init --contributor
if err := store.SetConfig(ctx, "sync.remote", "upstream"); err != nil { if err := store.SetConfig(ctx, "sync.remote", "upstream"); err != nil {
return fmt.Errorf("failed to set sync remote: %w", err) return fmt.Errorf("failed to set sync remote: %w", err)
} }
fmt.Printf("%s Sync configured to pull from upstream (source repo)\n", green("✓")) fmt.Printf("%s Sync configured to pull from upstream (source repo)\n", ui.RenderPass("✓"))
} }
// Step 5: Summary // Step 5: Summary
fmt.Printf("\n%s %s\n\n", green("✓"), bold("Contributor setup complete!")) fmt.Printf("\n%s %s\n\n", ui.RenderPass("✓"), ui.RenderBold("Contributor setup complete!"))
fmt.Println("Configuration:") fmt.Println("Configuration:")
fmt.Printf(" Current repo issues: %s\n", cyan(".beads/issues.jsonl")) fmt.Printf(" Current repo issues: %s\n", ui.RenderAccent(".beads/issues.jsonl"))
fmt.Printf(" Planning repo issues: %s\n", cyan(filepath.Join(planningPath, ".beads/issues.jsonl"))) fmt.Printf(" Planning repo issues: %s\n", ui.RenderAccent(filepath.Join(planningPath, ".beads/issues.jsonl")))
fmt.Println() fmt.Println()
fmt.Println("How it works:") fmt.Println("How it works:")
fmt.Println(" • Issues you create will route to the planning repo") fmt.Println(" • Issues you create will route to the planning repo")
fmt.Println(" • Planning stays out of your PRs to upstream") fmt.Println(" • Planning stays out of your PRs to upstream")
fmt.Println(" • Use 'bd list' to see issues from both repos") fmt.Println(" • Use 'bd list' to see issues from both repos")
fmt.Println() fmt.Println()
fmt.Printf("Try it: %s\n", cyan("bd create \"Plan feature X\" -p 2")) fmt.Printf("Try it: %s\n", ui.RenderAccent("bd create \"Plan feature X\" -p 2"))
fmt.Println() fmt.Println()
return nil return nil
+54 -59
View File
@@ -8,172 +8,167 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/fatih/color"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/ui"
) )
// runTeamWizard guides the user through team workflow setup // runTeamWizard guides the user through team workflow setup
func runTeamWizard(ctx context.Context, store storage.Storage) error { func runTeamWizard(ctx context.Context, store storage.Storage) error {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s %s\n\n", ui.RenderBold("bd"), ui.RenderBold("Team Workflow Setup Wizard"))
cyan := color.New(color.FgCyan).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
fmt.Printf("\n%s %s\n\n", bold("bd"), bold("Team Workflow Setup Wizard"))
fmt.Println("This wizard will configure beads for team collaboration.") fmt.Println("This wizard will configure beads for team collaboration.")
fmt.Println() fmt.Println()
// Step 1: Check if we're in a git repository // Step 1: Check if we're in a git repository
fmt.Printf("%s Detecting git repository setup...\n", cyan("▶")) fmt.Printf("%s Detecting git repository setup...\n", ui.RenderAccent("▶"))
if !isGitRepo() { if !isGitRepo() {
fmt.Printf("%s Not in a git repository\n", yellow("⚠")) fmt.Printf("%s Not in a git repository\n", ui.RenderWarn("⚠"))
fmt.Println("\n Initialize git first:") fmt.Println("\n Initialize git first:")
fmt.Println(" git init") fmt.Println(" git init")
fmt.Println() fmt.Println()
return fmt.Errorf("not in a git repository") return fmt.Errorf("not in a git repository")
} }
// Get current branch // Get current branch
currentBranch, err := getGitBranch() currentBranch, err := getGitBranch()
if err != nil { if err != nil {
return fmt.Errorf("failed to get current branch: %w", err) return fmt.Errorf("failed to get current branch: %w", err)
} }
fmt.Printf("%s Current branch: %s\n", green("✓"), currentBranch) fmt.Printf("%s Current branch: %s\n", ui.RenderPass("✓"), currentBranch)
// Step 2: Check for protected main branch // Step 2: Check for protected main branch
fmt.Printf("\n%s Checking branch configuration...\n", cyan("▶")) fmt.Printf("\n%s Checking branch configuration...\n", ui.RenderAccent("▶"))
fmt.Println("\nIs your main branch protected (prevents direct commits)?") fmt.Println("\nIs your main branch protected (prevents direct commits)?")
fmt.Println(" GitHub: Settings → Branches → Branch protection rules") fmt.Println(" GitHub: Settings → Branches → Branch protection rules")
fmt.Println(" GitLab: Settings → Repository → Protected branches") fmt.Println(" GitLab: Settings → Repository → Protected branches")
fmt.Print("\nProtected main branch? [y/N]: ") fmt.Print("\nProtected main branch? [y/N]: ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n') response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response)) response = strings.TrimSpace(strings.ToLower(response))
protectedMain := (response == "y" || response == "yes") protectedMain := (response == "y" || response == "yes")
var syncBranch string var syncBranch string
if protectedMain { if protectedMain {
fmt.Printf("\n%s Protected main detected\n", green("✓")) fmt.Printf("\n%s Protected main detected\n", ui.RenderPass("✓"))
fmt.Println("\n Beads will commit issue updates to a separate branch.") fmt.Println("\n Beads will commit issue updates to a separate branch.")
fmt.Printf(" Default sync branch: %s\n", cyan("beads-metadata")) fmt.Printf(" Default sync branch: %s\n", ui.RenderAccent("beads-metadata"))
fmt.Print("\n Sync branch name [press Enter for default]: ") fmt.Print("\n Sync branch name [press Enter for default]: ")
branchName, _ := reader.ReadString('\n') branchName, _ := reader.ReadString('\n')
branchName = strings.TrimSpace(branchName) branchName = strings.TrimSpace(branchName)
if branchName == "" { if branchName == "" {
syncBranch = "beads-metadata" syncBranch = "beads-metadata"
} else { } else {
syncBranch = branchName syncBranch = branchName
} }
fmt.Printf("\n%s Sync branch set to: %s\n", green("✓"), syncBranch) fmt.Printf("\n%s Sync branch set to: %s\n", ui.RenderPass("✓"), syncBranch)
// Set sync.branch config // Set sync.branch config
if err := store.SetConfig(ctx, "sync.branch", syncBranch); err != nil { if err := store.SetConfig(ctx, "sync.branch", syncBranch); err != nil {
return fmt.Errorf("failed to set sync branch: %w", err) return fmt.Errorf("failed to set sync branch: %w", err)
} }
// Create the sync branch if it doesn't exist // Create the sync branch if it doesn't exist
fmt.Printf("\n%s Creating sync branch...\n", cyan("▶")) fmt.Printf("\n%s Creating sync branch...\n", ui.RenderAccent("▶"))
if err := createSyncBranch(syncBranch); err != nil { if err := createSyncBranch(syncBranch); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create sync branch: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: failed to create sync branch: %v\n", err)
fmt.Println(" You can create it manually: git checkout -b", syncBranch) fmt.Println(" You can create it manually: git checkout -b", syncBranch)
} else { } else {
fmt.Printf("%s Sync branch created\n", green("✓")) fmt.Printf("%s Sync branch created\n", ui.RenderPass("✓"))
} }
} else { } else {
fmt.Printf("%s Direct commits to %s\n", green("✓"), currentBranch) fmt.Printf("%s Direct commits to %s\n", ui.RenderPass("✓"), currentBranch)
syncBranch = currentBranch syncBranch = currentBranch
} }
// Step 3: Configure team settings // Step 3: Configure team settings
fmt.Printf("\n%s Configuring team settings...\n", cyan("▶")) fmt.Printf("\n%s Configuring team settings...\n", ui.RenderAccent("▶"))
// Set team.enabled to true // Set team.enabled to true
if err := store.SetConfig(ctx, "team.enabled", "true"); err != nil { if err := store.SetConfig(ctx, "team.enabled", "true"); err != nil {
return fmt.Errorf("failed to enable team mode: %w", err) return fmt.Errorf("failed to enable team mode: %w", err)
} }
// Set team.sync_branch // Set team.sync_branch
if err := store.SetConfig(ctx, "team.sync_branch", syncBranch); err != nil { if err := store.SetConfig(ctx, "team.sync_branch", syncBranch); err != nil {
return fmt.Errorf("failed to set team sync branch: %w", err) return fmt.Errorf("failed to set team sync branch: %w", err)
} }
fmt.Printf("%s Team mode enabled\n", green("✓")) fmt.Printf("%s Team mode enabled\n", ui.RenderPass("✓"))
// Step 4: Configure auto-sync // Step 4: Configure auto-sync
fmt.Println("\n Enable automatic sync (daemon commits/pushes)?") fmt.Println("\n Enable automatic sync (daemon commits/pushes)?")
fmt.Println(" • Auto-commit: Commits issue changes every 5 seconds") fmt.Println(" • Auto-commit: Commits issue changes every 5 seconds")
fmt.Println(" • Auto-push: Pushes commits to remote") fmt.Println(" • Auto-push: Pushes commits to remote")
fmt.Print("\nEnable auto-sync? [Y/n]: ") fmt.Print("\nEnable auto-sync? [Y/n]: ")
response, _ = reader.ReadString('\n') response, _ = reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response)) response = strings.TrimSpace(strings.ToLower(response))
autoSync := !(response == "n" || response == "no") autoSync := !(response == "n" || response == "no")
if autoSync { if autoSync {
if err := store.SetConfig(ctx, "daemon.auto_commit", "true"); err != nil { if err := store.SetConfig(ctx, "daemon.auto_commit", "true"); err != nil {
return fmt.Errorf("failed to enable auto-commit: %w", err) return fmt.Errorf("failed to enable auto-commit: %w", err)
} }
if err := store.SetConfig(ctx, "daemon.auto_push", "true"); err != nil { if err := store.SetConfig(ctx, "daemon.auto_push", "true"); err != nil {
return fmt.Errorf("failed to enable auto-push: %w", err) return fmt.Errorf("failed to enable auto-push: %w", err)
} }
fmt.Printf("%s Auto-sync enabled\n", green("✓")) fmt.Printf("%s Auto-sync enabled\n", ui.RenderPass("✓"))
} else { } else {
fmt.Printf("%s Auto-sync disabled (manual sync with 'bd sync')\n", yellow("⚠")) fmt.Printf("%s Auto-sync disabled (manual sync with 'bd sync')\n", ui.RenderWarn("⚠"))
} }
// Step 5: Summary // Step 5: Summary
fmt.Printf("\n%s %s\n\n", green("✓"), bold("Team setup complete!")) fmt.Printf("\n%s %s\n\n", ui.RenderPass("✓"), ui.RenderBold("Team setup complete!"))
fmt.Println("Configuration:") fmt.Println("Configuration:")
if protectedMain { if protectedMain {
fmt.Printf(" Protected main: %s\n", cyan("yes")) fmt.Printf(" Protected main: %s\n", ui.RenderAccent("yes"))
fmt.Printf(" Sync branch: %s\n", cyan(syncBranch)) fmt.Printf(" Sync branch: %s\n", ui.RenderAccent(syncBranch))
fmt.Printf(" Commits will go to: %s\n", cyan(syncBranch)) fmt.Printf(" Commits will go to: %s\n", ui.RenderAccent(syncBranch))
fmt.Printf(" Merge to main via: %s\n", cyan("Pull Request")) fmt.Printf(" Merge to main via: %s\n", ui.RenderAccent("Pull Request"))
} else { } else {
fmt.Printf(" Protected main: %s\n", cyan("no")) fmt.Printf(" Protected main: %s\n", ui.RenderAccent("no"))
fmt.Printf(" Commits will go to: %s\n", cyan(currentBranch)) fmt.Printf(" Commits will go to: %s\n", ui.RenderAccent(currentBranch))
} }
if autoSync { if autoSync {
fmt.Printf(" Auto-sync: %s\n", cyan("enabled")) fmt.Printf(" Auto-sync: %s\n", ui.RenderAccent("enabled"))
} else { } else {
fmt.Printf(" Auto-sync: %s\n", cyan("disabled")) fmt.Printf(" Auto-sync: %s\n", ui.RenderAccent("disabled"))
} }
fmt.Println() fmt.Println()
fmt.Println("How it works:") fmt.Println("How it works:")
fmt.Println(" • All team members work on the same repository") fmt.Println(" • All team members work on the same repository")
fmt.Println(" • Issues are shared via git commits") fmt.Println(" • Issues are shared via git commits")
fmt.Println(" • Use 'bd list' to see all team's issues") fmt.Println(" • Use 'bd list' to see all team's issues")
if protectedMain { if protectedMain {
fmt.Println(" • Issue updates commit to", syncBranch) fmt.Println(" • Issue updates commit to", syncBranch)
fmt.Println(" • Periodically merge", syncBranch, "to main via PR") fmt.Println(" • Periodically merge", syncBranch, "to main via PR")
} }
if autoSync { if autoSync {
fmt.Println(" • Daemon automatically commits and pushes changes") fmt.Println(" • Daemon automatically commits and pushes changes")
} else { } else {
fmt.Println(" • Run 'bd sync' manually to sync changes") fmt.Println(" • Run 'bd sync' manually to sync changes")
} }
fmt.Println() fmt.Println()
fmt.Printf("Try it: %s\n", cyan("bd create \"Team planning issue\" -p 2")) fmt.Printf("Try it: %s\n", ui.RenderAccent("bd create \"Team planning issue\" -p 2"))
fmt.Println() fmt.Println()
if protectedMain { if protectedMain {
+7 -9
View File
@@ -7,15 +7,16 @@ import (
"os" "os"
"sort" "sort"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var labelCmd = &cobra.Command{ var labelCmd = &cobra.Command{
Use: "label", Use: "label",
Short: "Manage issue labels", GroupID: "issues",
Short: "Manage issue labels",
} }
// Helper function to process label operations for multiple issues // Helper function to process label operations for multiple issues
func processBatchLabelOperation(issueIDs []string, label string, operation string, jsonOut bool, func processBatchLabelOperation(issueIDs []string, label string, operation string, jsonOut bool,
@@ -40,14 +41,13 @@ func processBatchLabelOperation(issueIDs []string, label string, operation strin
"label": label, "label": label,
}) })
} else { } else {
green := color.New(color.FgGreen).SprintFunc()
verb := "Added" verb := "Added"
prep := "to" prep := "to"
if operation == "removed" { if operation == "removed" {
verb = "Removed" verb = "Removed"
prep = "from" prep = "from"
} }
fmt.Printf("%s %s label '%s' %s %s\n", green("✓"), verb, label, prep, issueID) fmt.Printf("%s %s label '%s' %s %s\n", ui.RenderPass("✓"), verb, label, prep, issueID)
} }
} }
if len(issueIDs) > 0 && daemonClient == nil { if len(issueIDs) > 0 && daemonClient == nil {
@@ -217,8 +217,7 @@ var labelListCmd = &cobra.Command{
fmt.Printf("\n%s has no labels\n", issueID) fmt.Printf("\n%s has no labels\n", issueID)
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Labels for %s:\n", ui.RenderAccent("🏷"), issueID)
fmt.Printf("\n%s Labels for %s:\n", cyan("🏷"), issueID)
for _, label := range labels { for _, label := range labels {
fmt.Printf(" - %s\n", label) fmt.Printf(" - %s\n", label)
} }
@@ -302,8 +301,7 @@ var labelListAllCmd = &cobra.Command{
outputJSON(result) outputJSON(result)
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s All labels (%d unique):\n", ui.RenderAccent("🏷"), len(labels))
fmt.Printf("\n%s All labels (%d unique):\n", cyan("🏷"), len(labels))
// Find longest label for alignment // Find longest label for alignment
maxLen := 0 maxLen := 0
for _, label := range labels { for _, label := range labels {
+3 -5
View File
@@ -10,9 +10,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/validation" "github.com/steveyegge/beads/internal/validation"
) )
@@ -397,8 +397,7 @@ func createIssuesFromMarkdown(_ *cobra.Command, filepath string) {
// Report failures if any // Report failures if any
if len(failedIssues) > 0 { if len(failedIssues) > 0 {
red := color.New(color.FgRed).SprintFunc() fmt.Fprintf(os.Stderr, "\n%s Failed to create %d issues:\n", ui.RenderFail("✗"), len(failedIssues))
fmt.Fprintf(os.Stderr, "\n%s Failed to create %d issues:\n", red("✗"), len(failedIssues))
for _, title := range failedIssues { for _, title := range failedIssues {
fmt.Fprintf(os.Stderr, " - %s\n", title) fmt.Fprintf(os.Stderr, " - %s\n", title)
} }
@@ -407,8 +406,7 @@ func createIssuesFromMarkdown(_ *cobra.Command, filepath string) {
if jsonOutput { if jsonOutput {
outputJSON(createdIssues) outputJSON(createdIssues)
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Created %d issues from %s:\n", ui.RenderPass("✓"), len(createdIssues), filepath)
fmt.Printf("%s Created %d issues from %s:\n", green("✓"), len(createdIssues), filepath)
for _, issue := range createdIssues { for _, issue := range createdIssues {
fmt.Printf(" %s: %s [P%d, %s]\n", issue.ID, issue.Title, issue.Priority, issue.IssueType) fmt.Printf(" %s: %s [P%d, %s]\n", issue.ID, issue.Title, issue.Priority, issue.IssueType)
} }
+21 -19
View File
@@ -8,20 +8,22 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/beads" "github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile" "github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
_ "github.com/ncruces/go-sqlite3/driver" _ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed" _ "github.com/ncruces/go-sqlite3/embed"
) )
// TODO: Consider integrating into 'bd doctor' migration detection
var migrateCmd = &cobra.Command{ var migrateCmd = &cobra.Command{
Use: "migrate", Use: "migrate",
Short: "Migrate database to current version", GroupID: "maint",
Short: "Migrate database to current version",
Long: `Detect and migrate database files to the current version. Long: `Detect and migrate database files to the current version.
This command: This command:
@@ -140,12 +142,12 @@ This command:
fmt.Printf(" Current database: %s\n", filepath.Base(currentDB.path)) fmt.Printf(" Current database: %s\n", filepath.Base(currentDB.path))
fmt.Printf(" Schema version: %s\n", currentDB.version) fmt.Printf(" Schema version: %s\n", currentDB.version)
if currentDB.version != Version { if currentDB.version != Version {
color.Yellow(" ⚠ Version mismatch (current: %s, expected: %s)\n", currentDB.version, Version) fmt.Printf(" ⚠ %s\n", ui.RenderWarn(fmt.Sprintf("Version mismatch (current: %s, expected: %s)", currentDB.version, Version)))
} else { } else {
color.Green(" ✓ Version matches\n") fmt.Printf(" %s\n", ui.RenderPass("✓ Version matches"))
} }
} else { } else {
color.Yellow(" No %s found\n", cfg.Database) fmt.Printf(" %s\n", ui.RenderWarn(fmt.Sprintf("No %s found", cfg.Database)))
} }
if len(oldDBs) > 0 { if len(oldDBs) > 0 {
@@ -231,7 +233,7 @@ This command:
os.Exit(1) os.Exit(1)
} }
if !jsonOutput { if !jsonOutput {
color.Green("✓ Created backup: %s\n", filepath.Base(backupPath)) fmt.Printf("%s\n", ui.RenderPass(fmt.Sprintf("✓ Created backup: %s", filepath.Base(backupPath))))
} }
} }
@@ -256,7 +258,7 @@ This command:
needsVersionUpdate = true needsVersionUpdate = true
if !jsonOutput { if !jsonOutput {
color.Green("✓ Migration complete\n\n") fmt.Printf("%s\n\n", ui.RenderPass("✓ Migration complete"))
} }
} }
@@ -305,7 +307,7 @@ This command:
os.Exit(1) os.Exit(1)
} }
if !jsonOutput { if !jsonOutput {
color.Green("✓ Detected and set issue prefix: %s\n", detectedPrefix) fmt.Printf("%s\n", ui.RenderPass(fmt.Sprintf("✓ Detected and set issue prefix: %s", detectedPrefix)))
} }
} }
} }
@@ -327,12 +329,12 @@ This command:
// Close and checkpoint to finalize the WAL // Close and checkpoint to finalize the WAL
if err := store.Close(); err != nil { if err := store.Close(); err != nil {
if !jsonOutput { if !jsonOutput {
color.Yellow("Warning: error closing database: %v\n", err) fmt.Printf("%s\n", ui.RenderWarn(fmt.Sprintf("Warning: error closing database: %v", err)))
} }
} }
if !jsonOutput { if !jsonOutput {
color.Green("✓ Version updated\n\n") fmt.Printf("%s\n\n", ui.RenderPass("✓ Version updated"))
} }
} }
@@ -361,7 +363,7 @@ This command:
for _, db := range oldDBs { for _, db := range oldDBs {
if err := os.Remove(db.path); err != nil { if err := os.Remove(db.path); err != nil {
if !jsonOutput { if !jsonOutput {
color.Yellow("Warning: failed to remove %s: %v\n", filepath.Base(db.path), err) fmt.Printf("%s\n", ui.RenderWarn(fmt.Sprintf("Warning: failed to remove %s: %v", filepath.Base(db.path), err)))
} }
} else if !jsonOutput { } else if !jsonOutput {
fmt.Printf("Removed %s\n", filepath.Base(db.path)) fmt.Printf("Removed %s\n", filepath.Base(db.path))
@@ -369,7 +371,7 @@ This command:
} }
if !jsonOutput { if !jsonOutput {
color.Green("\n✓ Cleanup complete\n") fmt.Printf("\n%s\n", ui.RenderPass("✓ Cleanup complete"))
} }
} }
} }
@@ -426,7 +428,7 @@ This command:
os.Exit(1) os.Exit(1)
} }
if !jsonOutput { if !jsonOutput {
color.Green("✓ Created backup: %s\n", filepath.Base(backupPath)) fmt.Printf("%s\n", ui.RenderPass(fmt.Sprintf("✓ Created backup: %s", filepath.Base(backupPath))))
} }
} }
@@ -449,7 +451,7 @@ This command:
if dryRun { if dryRun {
fmt.Printf("\nWould migrate %d issues to hash-based IDs\n", len(mapping)) fmt.Printf("\nWould migrate %d issues to hash-based IDs\n", len(mapping))
} else { } else {
color.Green("✓ Migrated %d issues to hash-based IDs\n", len(mapping)) fmt.Printf("%s\n", ui.RenderPass(fmt.Sprintf("✓ Migrated %d issues to hash-based IDs", len(mapping))))
} }
} }
} else { } else {
@@ -464,7 +466,7 @@ This command:
if !dryRun { if !dryRun {
if err := cfg.Save(beadsDir); err != nil { if err := cfg.Save(beadsDir); err != nil {
if !jsonOutput { if !jsonOutput {
color.Yellow("Warning: failed to save metadata.json: %v\n", err) fmt.Printf("%s\n", ui.RenderWarn(fmt.Sprintf("Warning: failed to save metadata.json: %v", err)))
} }
// Don't fail migration if config save fails // Don't fail migration if config save fails
} }
@@ -693,7 +695,7 @@ func handleUpdateRepoID(dryRun bool, autoYes bool) {
"new_repo_id": newRepoID[:8], "new_repo_id": newRepoID[:8],
}) })
} else { } else {
color.Green("✓ Repository ID updated\n\n") fmt.Printf("%s\n\n", ui.RenderPass("✓ Repository ID updated"))
fmt.Printf(" Old: %s\n", oldDisplay) fmt.Printf(" Old: %s\n", oldDisplay)
fmt.Printf(" New: %s\n", newRepoID[:8]) fmt.Printf(" New: %s\n", newRepoID[:8])
} }
@@ -1016,7 +1018,7 @@ func handleToSeparateBranch(branch string, dryRun bool) {
"message": "sync.branch already set to this value", "message": "sync.branch already set to this value",
}) })
} else { } else {
color.Green("✓ sync.branch already set to '%s'\n", b) fmt.Printf("%s\n", ui.RenderPass(fmt.Sprintf("✓ sync.branch already set to '%s'", b)))
fmt.Println("No changes needed") fmt.Println("No changes needed")
} }
return return
@@ -1044,7 +1046,7 @@ func handleToSeparateBranch(branch string, dryRun bool) {
"message": "Enabled separate branch workflow", "message": "Enabled separate branch workflow",
}) })
} else { } else {
color.Green("✓ Enabled separate branch workflow\n\n") fmt.Printf("%s\n\n", ui.RenderPass("✓ Enabled separate branch workflow"))
fmt.Printf("Set sync.branch to '%s'\n\n", b) fmt.Printf("Set sync.branch to '%s'\n\n", b)
fmt.Println("Next steps:") fmt.Println("Next steps:")
fmt.Println(" 1. Restart the daemon to create worktree and start committing to the branch:") fmt.Println(" 1. Restart the daemon to create worktree and start committing to the branch:")
+9 -7
View File
@@ -13,16 +13,18 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/beads" "github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// TODO: Consider integrating into 'bd doctor' migration detection
var migrateHashIDsCmd = &cobra.Command{ var migrateHashIDsCmd = &cobra.Command{
Use: "migrate-hash-ids", Use: "migrate-hash-ids",
Short: "Migrate sequential IDs to hash-based IDs (legacy)", GroupID: "maint",
Short: "Migrate sequential IDs to hash-based IDs (legacy)",
Long: `Migrate database from sequential IDs (bd-1, bd-2) to hash-based IDs (bd-a3f8e9a2). Long: `Migrate database from sequential IDs (bd-1, bd-2) to hash-based IDs (bd-a3f8e9a2).
*** LEGACY COMMAND *** *** LEGACY COMMAND ***
@@ -86,7 +88,7 @@ WARNING: Backup your database before running this command, even though it create
os.Exit(1) os.Exit(1)
} }
if !jsonOutput { if !jsonOutput {
color.Green("✓ Created backup: %s\n\n", filepath.Base(backupPath)) fmt.Printf("%s\n\n", ui.RenderPass(fmt.Sprintf("✓ Created backup: %s", filepath.Base(backupPath))))
} }
} }
@@ -163,10 +165,10 @@ WARNING: Backup your database before running this command, even though it create
mappingPath := filepath.Join(filepath.Dir(dbPath), "hash-id-mapping.json") mappingPath := filepath.Join(filepath.Dir(dbPath), "hash-id-mapping.json")
if err := saveMappingFile(mappingPath, mapping); err != nil { if err := saveMappingFile(mappingPath, mapping); err != nil {
if !jsonOutput { if !jsonOutput {
color.Yellow("Warning: failed to save mapping file: %v\n", err) fmt.Printf("%s\n", ui.RenderWarn(fmt.Sprintf("Warning: failed to save mapping file: %v", err)))
} }
} else if !jsonOutput { } else if !jsonOutput {
color.Green("✓ Saved mapping to: %s\n", filepath.Base(mappingPath)) fmt.Printf("%s\n", ui.RenderPass(fmt.Sprintf("✓ Saved mapping to: %s", filepath.Base(mappingPath))))
} }
} }
@@ -193,7 +195,7 @@ WARNING: Backup your database before running this command, even though it create
count++ count++
} }
} else { } else {
color.Green("\n✓ Migration complete!\n\n") fmt.Printf("\n%s\n\n", ui.RenderPass("✓ Migration complete!"))
fmt.Printf("Migrated %d issues to hash-based IDs\n", len(mapping)) fmt.Printf("Migrated %d issues to hash-based IDs\n", len(mapping))
fmt.Println("\nNext steps:") fmt.Println("\nNext steps:")
fmt.Println(" 1. Run 'bd export' to update JSONL file") fmt.Println(" 1. Run 'bd export' to update JSONL file")
+8 -6
View File
@@ -8,9 +8,9 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// legacyDeletionRecordCmd represents a single deletion entry from the legacy deletions.jsonl manifest. // legacyDeletionRecordCmd represents a single deletion entry from the legacy deletions.jsonl manifest.
@@ -68,9 +68,11 @@ func loadLegacyDeletionsCmd(path string) (map[string]legacyDeletionRecordCmd, []
return records, warnings, nil return records, warnings, nil
} }
// TODO: Consider integrating into 'bd doctor' migration detection
var migrateTombstonesCmd = &cobra.Command{ var migrateTombstonesCmd = &cobra.Command{
Use: "migrate-tombstones", Use: "migrate-tombstones",
Short: "Convert deletions.jsonl entries to inline tombstones", GroupID: "maint",
Short: "Convert deletions.jsonl entries to inline tombstones",
Long: `Migrate legacy deletions.jsonl entries to inline tombstones in issues.jsonl. Long: `Migrate legacy deletions.jsonl entries to inline tombstones in issues.jsonl.
This command converts existing deletion records from the legacy deletions.jsonl This command converts existing deletion records from the legacy deletions.jsonl
@@ -149,7 +151,7 @@ Examples:
// Print warnings from loading // Print warnings from loading
for _, warning := range warnings { for _, warning := range warnings {
if !jsonOutput { if !jsonOutput {
color.Yellow("Warning: %s\n", warning) fmt.Printf("%s\n", ui.RenderWarn(fmt.Sprintf("Warning: %s", warning)))
} }
} }
@@ -288,7 +290,7 @@ Examples:
if err := os.Rename(deletionsPath, archivePath); err != nil { if err := os.Rename(deletionsPath, archivePath); err != nil {
// Warn but don't fail - tombstones were already created // Warn but don't fail - tombstones were already created
if !jsonOutput { if !jsonOutput {
color.Yellow("Warning: could not archive deletions.jsonl: %v\n", err) fmt.Printf("%s\n", ui.RenderWarn(fmt.Sprintf("Warning: could not archive deletions.jsonl: %v", err)))
} }
} else if verbose && !jsonOutput { } else if verbose && !jsonOutput {
fmt.Printf(" ✓ Archived deletions.jsonl to %s\n", filepath.Base(archivePath)) fmt.Printf(" ✓ Archived deletions.jsonl to %s\n", filepath.Base(archivePath))
@@ -305,7 +307,7 @@ Examples:
"migrated_ids": migratedIDs, "migrated_ids": migratedIDs,
}) })
} else { } else {
color.Green("\n✓ Migration complete\n\n") fmt.Printf("\n%s\n\n", ui.RenderPass("✓ Migration complete"))
fmt.Printf(" Migrated: %d tombstone(s)\n", len(migratedIDs)) fmt.Printf(" Migrated: %d tombstone(s)\n", len(migratedIDs))
if len(skippedIDs) > 0 { if len(skippedIDs) > 0 {
fmt.Printf(" Skipped: %d (already had tombstones)\n", len(skippedIDs)) fmt.Printf(" Skipped: %d (already had tombstones)\n", len(skippedIDs))
+3 -5
View File
@@ -6,11 +6,11 @@ import (
"os" "os"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/hooks" "github.com/steveyegge/beads/internal/hooks"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
var moleculeCmd = &cobra.Command{ var moleculeCmd = &cobra.Command{
@@ -268,8 +268,7 @@ Examples:
if jsonOutput { if jsonOutput {
outputJSON(&issue) outputJSON(&issue)
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Created work item: %s (from template %s)\n", ui.RenderPass("✓"), issue.ID, moleculeID)
fmt.Printf("%s Created work item: %s (from template %s)\n", green("✓"), issue.ID, moleculeID)
fmt.Printf(" Title: %s\n", issue.Title) fmt.Printf(" Title: %s\n", issue.Title)
fmt.Printf(" Priority: P%d\n", issue.Priority) fmt.Printf(" Priority: P%d\n", issue.Priority)
fmt.Printf(" Status: %s\n", issue.Status) fmt.Printf(" Status: %s\n", issue.Status)
@@ -328,8 +327,7 @@ Examples:
if jsonOutput { if jsonOutput {
outputJSON(createdIssue) outputJSON(createdIssue)
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Created work item: %s (from template %s)\n", ui.RenderPass("✓"), createdIssue.ID, moleculeID)
fmt.Printf("%s Created work item: %s (from template %s)\n", green("✓"), createdIssue.ID, moleculeID)
fmt.Printf(" Title: %s\n", createdIssue.Title) fmt.Printf(" Title: %s\n", createdIssue.Title)
fmt.Printf(" Priority: P%d\n", createdIssue.Priority) fmt.Printf(" Priority: P%d\n", createdIssue.Priority)
fmt.Printf(" Status: %s\n", createdIssue.Status) fmt.Printf(" Status: %s\n", createdIssue.Status)
+12 -15
View File
@@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/ui"
) )
const copilotInstructionsContent = `# GitHub Copilot Instructions const copilotInstructionsContent = `# GitHub Copilot Instructions
@@ -37,10 +37,6 @@ Run ` + "`bd prime`" + ` for workflow context, or install hooks (` + "`bd hooks
For full workflow details: ` + "`bd prime`" + `` For full workflow details: ` + "`bd prime`" + ``
func renderOnboardInstructions(w io.Writer) error { func renderOnboardInstructions(w io.Writer) error {
bold := color.New(color.Bold).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
writef := func(format string, args ...interface{}) error { writef := func(format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, format, args...) _, err := fmt.Fprintf(w, format, args...)
return err return err
@@ -54,7 +50,7 @@ func renderOnboardInstructions(w io.Writer) error {
return err return err
} }
if err := writef("\n%s\n\n", bold("bd Onboarding")); err != nil { if err := writef("\n%s\n\n", ui.RenderBold("bd Onboarding")); err != nil {
return err return err
} }
if err := writeln("Add this minimal snippet to AGENTS.md (or create it):"); err != nil { if err := writeln("Add this minimal snippet to AGENTS.md (or create it):"); err != nil {
@@ -64,17 +60,17 @@ func renderOnboardInstructions(w io.Writer) error {
return err return err
} }
if err := writef("%s\n", cyan("--- BEGIN AGENTS.MD CONTENT ---")); err != nil { if err := writef("%s\n", ui.RenderAccent("--- BEGIN AGENTS.MD CONTENT ---")); err != nil {
return err return err
} }
if err := writeln(agentsContent); err != nil { if err := writeln(agentsContent); err != nil {
return err return err
} }
if err := writef("%s\n\n", cyan("--- END AGENTS.MD CONTENT ---")); err != nil { if err := writef("%s\n\n", ui.RenderAccent("--- END AGENTS.MD CONTENT ---")); err != nil {
return err return err
} }
if err := writef("%s\n", bold("For GitHub Copilot users:")); err != nil { if err := writef("%s\n", ui.RenderBold("For GitHub Copilot users:")); err != nil {
return err return err
} }
if err := writeln("Add the same content to .github/copilot-instructions.md"); err != nil { if err := writeln("Add the same content to .github/copilot-instructions.md"); err != nil {
@@ -84,13 +80,13 @@ func renderOnboardInstructions(w io.Writer) error {
return err return err
} }
if err := writef("%s\n", bold("How it works:")); err != nil { if err := writef("%s\n", ui.RenderBold("How it works:")); err != nil {
return err return err
} }
if err := writef(" • %s provides dynamic workflow context (~80 lines)\n", cyan("bd prime")); err != nil { if err := writef(" • %s provides dynamic workflow context (~80 lines)\n", ui.RenderAccent("bd prime")); err != nil {
return err return err
} }
if err := writef(" • %s auto-injects bd prime at session start\n", cyan("bd hooks install")); err != nil { if err := writef(" • %s auto-injects bd prime at session start\n", ui.RenderAccent("bd hooks install")); err != nil {
return err return err
} }
if err := writeln(" • AGENTS.md only needs this minimal pointer, not full instructions"); err != nil { if err := writeln(" • AGENTS.md only needs this minimal pointer, not full instructions"); err != nil {
@@ -100,7 +96,7 @@ func renderOnboardInstructions(w io.Writer) error {
return err return err
} }
if err := writef("%s\n\n", green("This keeps AGENTS.md lean while bd prime provides up-to-date workflow details.")); err != nil { if err := writef("%s\n\n", ui.RenderPass("This keeps AGENTS.md lean while bd prime provides up-to-date workflow details.")); err != nil {
return err return err
} }
@@ -108,8 +104,9 @@ func renderOnboardInstructions(w io.Writer) error {
} }
var onboardCmd = &cobra.Command{ var onboardCmd = &cobra.Command{
Use: "onboard", Use: "onboard",
Short: "Display minimal snippet for AGENTS.md", GroupID: "setup",
Short: "Display minimal snippet for AGENTS.md",
Long: `Display a minimal snippet to add to AGENTS.md for bd integration. Long: `Display a minimal snippet to add to AGENTS.md for bd integration.
This outputs a small (~10 line) snippet that points to 'bd prime' for full This outputs a small (~10 line) snippet that points to 'bd prime' for full
+8 -7
View File
@@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var pinCmd = &cobra.Command{ var pinCmd = &cobra.Command{
Use: "pin [id...]", Use: "pin [id...]",
Short: "Pin one or more issues as persistent context markers", GroupID: "issues",
Short: "Pin one or more issues as persistent context markers",
Long: `Pin issues to mark them as persistent context markers. Long: `Pin issues to mark them as persistent context markers.
Pinned issues are not work items - they are context beads that should Pinned issues are not work items - they are context beads that should
@@ -78,8 +79,8 @@ Examples:
pinnedIssues = append(pinnedIssues, &issue) pinnedIssues = append(pinnedIssues, &issue)
} }
} else { } else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Pinned %s\n", green("📌"), id) fmt.Printf("%s Pinned %s\n", ui.RenderPass("📌"), id)
} }
} }
@@ -117,8 +118,8 @@ Examples:
pinnedIssues = append(pinnedIssues, issue) pinnedIssues = append(pinnedIssues, issue)
} }
} else { } else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Pinned %s\n", green("📌"), fullID) fmt.Printf("%s Pinned %s\n", ui.RenderPass("📌"), fullID)
} }
} }
+54 -58
View File
@@ -3,98 +3,94 @@ package main
import ( import (
"fmt" "fmt"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/ui"
) )
var quickstartCmd = &cobra.Command{ var quickstartCmd = &cobra.Command{
Use: "quickstart", Use: "quickstart",
Short: "Quick start guide for bd", GroupID: "setup",
Short: "Quick start guide for bd",
Long: `Display a quick start guide showing common bd workflows and patterns.`, Long: `Display a quick start guide showing common bd workflows and patterns.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s\n\n", ui.RenderBold("bd - Dependency-Aware Issue Tracker"))
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
fmt.Printf("\n%s\n\n", bold("bd - Dependency-Aware Issue Tracker"))
fmt.Printf("Issues chained together like beads.\n\n") fmt.Printf("Issues chained together like beads.\n\n")
fmt.Printf("%s\n", bold("GETTING STARTED")) fmt.Printf("%s\n", ui.RenderBold("GETTING STARTED"))
fmt.Printf(" %s Initialize bd in your project\n", cyan("bd init")) fmt.Printf(" %s Initialize bd in your project\n", ui.RenderAccent("bd init"))
fmt.Printf(" Creates .beads/ directory with project-specific database\n") fmt.Printf(" Creates .beads/ directory with project-specific database\n")
fmt.Printf(" Auto-detects prefix from directory name (e.g., myapp-1, myapp-2)\n\n") fmt.Printf(" Auto-detects prefix from directory name (e.g., myapp-1, myapp-2)\n\n")
fmt.Printf(" %s Initialize with custom prefix\n", cyan("bd init --prefix api")) fmt.Printf(" %s Initialize with custom prefix\n", ui.RenderAccent("bd init --prefix api"))
fmt.Printf(" Issues will be named: api-<hash> (e.g., api-a3f2dd)\n\n") fmt.Printf(" Issues will be named: api-<hash> (e.g., api-a3f2dd)\n\n")
fmt.Printf("%s\n", bold("CREATING ISSUES")) fmt.Printf("%s\n", ui.RenderBold("CREATING ISSUES"))
fmt.Printf(" %s\n", cyan("bd create \"Fix login bug\"")) fmt.Printf(" %s\n", ui.RenderAccent("bd create \"Fix login bug\""))
fmt.Printf(" %s\n", cyan("bd create \"Add auth\" -p 0 -t feature")) fmt.Printf(" %s\n", ui.RenderAccent("bd create \"Add auth\" -p 0 -t feature"))
fmt.Printf(" %s\n\n", cyan("bd create \"Write tests\" -d \"Unit tests for auth\" --assignee alice")) fmt.Printf(" %s\n\n", ui.RenderAccent("bd create \"Write tests\" -d \"Unit tests for auth\" --assignee alice"))
fmt.Printf("%s\n", bold("VIEWING ISSUES")) fmt.Printf("%s\n", ui.RenderBold("VIEWING ISSUES"))
fmt.Printf(" %s List all issues\n", cyan("bd list")) fmt.Printf(" %s List all issues\n", ui.RenderAccent("bd list"))
fmt.Printf(" %s List by status\n", cyan("bd list --status open")) fmt.Printf(" %s List by status\n", ui.RenderAccent("bd list --status open"))
fmt.Printf(" %s List by priority (0-4, 0=highest)\n", cyan("bd list --priority 0")) fmt.Printf(" %s List by priority (0-4, 0=highest)\n", ui.RenderAccent("bd list --priority 0"))
fmt.Printf(" %s Show issue details\n\n", cyan("bd show bd-1")) fmt.Printf(" %s Show issue details\n\n", ui.RenderAccent("bd show bd-1"))
fmt.Printf("%s\n", bold("MANAGING DEPENDENCIES")) fmt.Printf("%s\n", ui.RenderBold("MANAGING DEPENDENCIES"))
fmt.Printf(" %s Add dependency (bd-2 blocks bd-1)\n", cyan("bd dep add bd-1 bd-2")) fmt.Printf(" %s Add dependency (bd-2 blocks bd-1)\n", ui.RenderAccent("bd dep add bd-1 bd-2"))
fmt.Printf(" %s Visualize dependency tree\n", cyan("bd dep tree bd-1")) fmt.Printf(" %s Visualize dependency tree\n", ui.RenderAccent("bd dep tree bd-1"))
fmt.Printf(" %s Detect circular dependencies\n\n", cyan("bd dep cycles")) fmt.Printf(" %s Detect circular dependencies\n\n", ui.RenderAccent("bd dep cycles"))
fmt.Printf("%s\n", bold("DEPENDENCY TYPES")) fmt.Printf("%s\n", ui.RenderBold("DEPENDENCY TYPES"))
fmt.Printf(" %s Task B must complete before task A\n", yellow("blocks")) fmt.Printf(" %s Task B must complete before task A\n", ui.RenderWarn("blocks"))
fmt.Printf(" %s Soft connection, doesn't block progress\n", yellow("related")) fmt.Printf(" %s Soft connection, doesn't block progress\n", ui.RenderWarn("related"))
fmt.Printf(" %s Epic/subtask hierarchical relationship\n", yellow("parent-child")) fmt.Printf(" %s Epic/subtask hierarchical relationship\n", ui.RenderWarn("parent-child"))
fmt.Printf(" %s Auto-created when AI discovers related work\n\n", yellow("discovered-from")) fmt.Printf(" %s Auto-created when AI discovers related work\n\n", ui.RenderWarn("discovered-from"))
fmt.Printf("%s\n", bold("READY WORK")) fmt.Printf("%s\n", ui.RenderBold("READY WORK"))
fmt.Printf(" %s Show issues ready to work on\n", cyan("bd ready")) fmt.Printf(" %s Show issues ready to work on\n", ui.RenderAccent("bd ready"))
fmt.Printf(" Ready = status is 'open' AND no blocking dependencies\n") fmt.Printf(" Ready = status is 'open' AND no blocking dependencies\n")
fmt.Printf(" Perfect for agents to claim next work!\n\n") fmt.Printf(" Perfect for agents to claim next work!\n\n")
fmt.Printf("%s\n", bold("UPDATING ISSUES")) fmt.Printf("%s\n", ui.RenderBold("UPDATING ISSUES"))
fmt.Printf(" %s\n", cyan("bd update bd-1 --status in_progress")) fmt.Printf(" %s\n", ui.RenderAccent("bd update bd-1 --status in_progress"))
fmt.Printf(" %s\n", cyan("bd update bd-1 --priority 0")) fmt.Printf(" %s\n", ui.RenderAccent("bd update bd-1 --priority 0"))
fmt.Printf(" %s\n\n", cyan("bd update bd-1 --assignee bob")) fmt.Printf(" %s\n\n", ui.RenderAccent("bd update bd-1 --assignee bob"))
fmt.Printf("%s\n", bold("CLOSING ISSUES")) fmt.Printf("%s\n", ui.RenderBold("CLOSING ISSUES"))
fmt.Printf(" %s\n", cyan("bd close bd-1")) fmt.Printf(" %s\n", ui.RenderAccent("bd close bd-1"))
fmt.Printf(" %s\n\n", cyan("bd close bd-2 bd-3 --reason \"Fixed in PR #42\"")) fmt.Printf(" %s\n\n", ui.RenderAccent("bd close bd-2 bd-3 --reason \"Fixed in PR #42\""))
fmt.Printf("%s\n", bold("DATABASE LOCATION")) fmt.Printf("%s\n", ui.RenderBold("DATABASE LOCATION"))
fmt.Printf(" bd automatically discovers your database:\n") fmt.Printf(" bd automatically discovers your database:\n")
fmt.Printf(" 1. %s flag\n", cyan("--db /path/to/db.db")) fmt.Printf(" 1. %s flag\n", ui.RenderAccent("--db /path/to/db.db"))
fmt.Printf(" 2. %s environment variable\n", cyan("$BEADS_DB")) fmt.Printf(" 2. %s environment variable\n", ui.RenderAccent("$BEADS_DB"))
fmt.Printf(" 3. %s in current directory or ancestors\n", cyan(".beads/*.db")) fmt.Printf(" 3. %s in current directory or ancestors\n", ui.RenderAccent(".beads/*.db"))
fmt.Printf(" 4. %s as fallback\n\n", cyan("~/.beads/default.db")) fmt.Printf(" 4. %s as fallback\n\n", ui.RenderAccent("~/.beads/default.db"))
fmt.Printf("%s\n", bold("AGENT INTEGRATION")) fmt.Printf("%s\n", ui.RenderBold("AGENT INTEGRATION"))
fmt.Printf(" bd is designed for AI-supervised workflows:\n") fmt.Printf(" bd is designed for AI-supervised workflows:\n")
fmt.Printf(" • Agents create issues when discovering new work\n") fmt.Printf(" • Agents create issues when discovering new work\n")
fmt.Printf(" • %s shows unblocked work ready to claim\n", cyan("bd ready")) fmt.Printf(" • %s shows unblocked work ready to claim\n", ui.RenderAccent("bd ready"))
fmt.Printf(" • Use %s flags for programmatic parsing\n", cyan("--json")) fmt.Printf(" • Use %s flags for programmatic parsing\n", ui.RenderAccent("--json"))
fmt.Printf(" • Dependencies prevent agents from duplicating effort\n\n") fmt.Printf(" • Dependencies prevent agents from duplicating effort\n\n")
fmt.Printf("%s\n", bold("DATABASE EXTENSION")) fmt.Printf("%s\n", ui.RenderBold("DATABASE EXTENSION"))
fmt.Printf(" Applications can extend bd's SQLite database:\n") fmt.Printf(" Applications can extend bd's SQLite database:\n")
fmt.Printf(" • Add your own tables (e.g., %s)\n", cyan("myapp_executions")) fmt.Printf(" • Add your own tables (e.g., %s)\n", ui.RenderAccent("myapp_executions"))
fmt.Printf(" • Join with %s table for powerful queries\n", cyan("issues")) fmt.Printf(" • Join with %s table for powerful queries\n", ui.RenderAccent("issues"))
fmt.Printf(" • See database extension docs for integration patterns:\n") fmt.Printf(" • See database extension docs for integration patterns:\n")
fmt.Printf(" %s\n\n", cyan("https://github.com/steveyegge/beads/blob/main/EXTENDING.md")) fmt.Printf(" %s\n\n", ui.RenderAccent("https://github.com/steveyegge/beads/blob/main/EXTENDING.md"))
fmt.Printf("%s\n", bold("GIT WORKFLOW (AUTO-SYNC)")) fmt.Printf("%s\n", ui.RenderBold("GIT WORKFLOW (AUTO-SYNC)"))
fmt.Printf(" bd automatically keeps git in sync:\n") fmt.Printf(" bd automatically keeps git in sync:\n")
fmt.Printf(" • %s Export to JSONL after CRUD operations (5s debounce)\n", green("✓")) fmt.Printf(" • %s Export to JSONL after CRUD operations (5s debounce)\n", ui.RenderPass("✓"))
fmt.Printf(" • %s Import from JSONL when newer than DB (after %s)\n", green("✓"), cyan("git pull")) fmt.Printf(" • %s Import from JSONL when newer than DB (after %s)\n", ui.RenderPass("✓"), ui.RenderAccent("git pull"))
fmt.Printf(" • %s Works seamlessly across machines and team members\n", green("✓")) fmt.Printf(" • %s Works seamlessly across machines and team members\n", ui.RenderPass("✓"))
fmt.Printf(" • No manual export/import needed!\n") fmt.Printf(" • No manual export/import needed!\n")
fmt.Printf(" Disable with: %s or %s\n\n", cyan("--no-auto-flush"), cyan("--no-auto-import")) fmt.Printf(" Disable with: %s or %s\n\n", ui.RenderAccent("--no-auto-flush"), ui.RenderAccent("--no-auto-import"))
fmt.Printf("%s\n", green("Ready to start!")) fmt.Printf("%s\n", ui.RenderPass("Ready to start!"))
fmt.Printf("Run %s to create your first issue.\n\n", cyan("bd create \"My first issue\"")) fmt.Printf("Run %s to create your first issue.\n\n", ui.RenderAccent("bd create \"My first issue\""))
}, },
} }
+31 -37
View File
@@ -5,12 +5,12 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/util" "github.com/steveyegge/beads/internal/util"
) )
var readyCmd = &cobra.Command{ var readyCmd = &cobra.Command{
@@ -105,20 +105,20 @@ var readyCmd = &cobra.Command{
hasOpenIssues = stats.OpenIssues > 0 || stats.InProgressIssues > 0 hasOpenIssues = stats.OpenIssues > 0 || stats.InProgressIssues > 0
} }
} }
yellow := color.New(color.FgYellow).SprintFunc()
if hasOpenIssues { if hasOpenIssues {
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n", fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
yellow("✨")) ui.RenderWarn("✨"))
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s No open issues\n\n", ui.RenderPass("✨"))
fmt.Printf("\n%s No open issues\n\n", green("✨"))
} }
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", ui.RenderAccent("📋"), len(issues))
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", cyan("📋"), len(issues))
for i, issue := range issues { for i, issue := range issues {
fmt.Printf("%d. [P%d] %s: %s\n", i+1, issue.Priority, issue.ID, issue.Title) fmt.Printf("%d. [%s] [%s] %s: %s\n", i+1,
ui.RenderPriority(issue.Priority),
ui.RenderType(string(issue.IssueType)),
ui.RenderID(issue.ID), issue.Title)
if issue.EstimatedMinutes != nil { if issue.EstimatedMinutes != nil {
fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes) fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes)
} }
@@ -175,21 +175,21 @@ var readyCmd = &cobra.Command{
hasOpenIssues = stats.OpenIssues > 0 || stats.InProgressIssues > 0 hasOpenIssues = stats.OpenIssues > 0 || stats.InProgressIssues > 0
} }
if hasOpenIssues { if hasOpenIssues {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n", fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
yellow("✨")) ui.RenderWarn("✨"))
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s No open issues\n\n", ui.RenderPass("✨"))
fmt.Printf("\n%s No open issues\n\n", green("✨"))
} }
// Show tip even when no ready work found // Show tip even when no ready work found
maybeShowTip(store) maybeShowTip(store)
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", ui.RenderAccent("📋"), len(issues))
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", cyan("📋"), len(issues))
for i, issue := range issues { for i, issue := range issues {
fmt.Printf("%d. [P%d] %s: %s\n", i+1, issue.Priority, issue.ID, issue.Title) fmt.Printf("%d. [%s] [%s] %s: %s\n", i+1,
ui.RenderPriority(issue.Priority),
ui.RenderType(string(issue.IssueType)),
ui.RenderID(issue.ID), issue.Title)
if issue.EstimatedMinutes != nil { if issue.EstimatedMinutes != nil {
fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes) fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes)
} }
@@ -233,14 +233,14 @@ var blockedCmd = &cobra.Command{
return return
} }
if len(blocked) == 0 { if len(blocked) == 0 {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s No blocked issues\n\n", ui.RenderPass("✨"))
fmt.Printf("\n%s No blocked issues\n\n", green("✨"))
return return
} }
red := color.New(color.FgRed).SprintFunc() fmt.Printf("\n%s Blocked issues (%d):\n\n", ui.RenderFail("🚫"), len(blocked))
fmt.Printf("\n%s Blocked issues (%d):\n\n", red("🚫"), len(blocked))
for _, issue := range blocked { for _, issue := range blocked {
fmt.Printf("[P%d] %s: %s\n", issue.Priority, issue.ID, issue.Title) fmt.Printf("[%s] %s: %s\n",
ui.RenderPriority(issue.Priority),
ui.RenderID(issue.ID), issue.Title)
blockedBy := issue.BlockedBy blockedBy := issue.BlockedBy
if blockedBy == nil { if blockedBy == nil {
blockedBy = []string{} blockedBy = []string{}
@@ -272,16 +272,13 @@ var statsCmd = &cobra.Command{
outputJSON(stats) outputJSON(stats)
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Beads Statistics:\n\n", ui.RenderAccent("📊"))
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s Beads Statistics:\n\n", cyan("📊"))
fmt.Printf("Total Issues: %d\n", stats.TotalIssues) fmt.Printf("Total Issues: %d\n", stats.TotalIssues)
fmt.Printf("Open: %s\n", green(fmt.Sprintf("%d", stats.OpenIssues))) fmt.Printf("Open: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.OpenIssues)))
fmt.Printf("In Progress: %s\n", yellow(fmt.Sprintf("%d", stats.InProgressIssues))) fmt.Printf("In Progress: %s\n", ui.RenderWarn(fmt.Sprintf("%d", stats.InProgressIssues)))
fmt.Printf("Closed: %d\n", stats.ClosedIssues) fmt.Printf("Closed: %d\n", stats.ClosedIssues)
fmt.Printf("Blocked: %d\n", stats.BlockedIssues) fmt.Printf("Blocked: %s\n", ui.RenderFail(fmt.Sprintf("%d", stats.BlockedIssues)))
fmt.Printf("Ready: %s\n", green(fmt.Sprintf("%d", stats.ReadyIssues))) fmt.Printf("Ready: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.ReadyIssues)))
if stats.TombstoneIssues > 0 { if stats.TombstoneIssues > 0 {
fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues) fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues)
} }
@@ -316,16 +313,13 @@ var statsCmd = &cobra.Command{
outputJSON(stats) outputJSON(stats)
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Beads Statistics:\n\n", ui.RenderAccent("📊"))
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s Beads Statistics:\n\n", cyan("📊"))
fmt.Printf("Total Issues: %d\n", stats.TotalIssues) fmt.Printf("Total Issues: %d\n", stats.TotalIssues)
fmt.Printf("Open: %s\n", green(fmt.Sprintf("%d", stats.OpenIssues))) fmt.Printf("Open: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.OpenIssues)))
fmt.Printf("In Progress: %s\n", yellow(fmt.Sprintf("%d", stats.InProgressIssues))) fmt.Printf("In Progress: %s\n", ui.RenderWarn(fmt.Sprintf("%d", stats.InProgressIssues)))
fmt.Printf("Closed: %d\n", stats.ClosedIssues) fmt.Printf("Closed: %d\n", stats.ClosedIssues)
fmt.Printf("Blocked: %d\n", stats.BlockedIssues) fmt.Printf("Blocked: %s\n", ui.RenderFail(fmt.Sprintf("%d", stats.BlockedIssues)))
fmt.Printf("Ready: %s\n", green(fmt.Sprintf("%d", stats.ReadyIssues))) fmt.Printf("Ready: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.ReadyIssues)))
if stats.TombstoneIssues > 0 { if stats.TombstoneIssues > 0 {
fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues) fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues)
} }
@@ -333,7 +327,7 @@ var statsCmd = &cobra.Command{
fmt.Printf("Pinned: %d\n", stats.PinnedIssues) fmt.Printf("Pinned: %d\n", stats.PinnedIssues)
} }
if stats.EpicsEligibleForClosure > 0 { if stats.EpicsEligibleForClosure > 0 {
fmt.Printf("Epics Ready to Close: %s\n", green(fmt.Sprintf("%d", stats.EpicsEligibleForClosure))) fmt.Printf("Epics Ready to Close: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.EpicsEligibleForClosure)))
} }
if stats.AverageLeadTime > 0 { if stats.AverageLeadTime > 0 {
fmt.Printf("Avg Lead Time: %.1f hours\n", stats.AverageLeadTime) fmt.Printf("Avg Lead Time: %.1f hours\n", stats.AverageLeadTime)
+9 -9
View File
@@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var relateCmd = &cobra.Command{ var relateCmd = &cobra.Command{
Use: "relate <id1> <id2>", Use: "relate <id1> <id2>",
Short: "Create a bidirectional relates_to link between issues", GroupID: "deps",
Short: "Create a bidirectional relates_to link between issues",
Long: `Create a loose 'see also' relationship between two issues. Long: `Create a loose 'see also' relationship between two issues.
The relates_to link is bidirectional - both issues will reference each other. The relates_to link is bidirectional - both issues will reference each other.
@@ -28,8 +29,9 @@ Examples:
} }
var unrelateCmd = &cobra.Command{ var unrelateCmd = &cobra.Command{
Use: "unrelate <id1> <id2>", Use: "unrelate <id1> <id2>",
Short: "Remove a relates_to link between issues", GroupID: "deps",
Short: "Remove a relates_to link between issues",
Long: `Remove a relates_to relationship between two issues. Long: `Remove a relates_to relationship between two issues.
Removes the link in both directions. Removes the link in both directions.
@@ -177,8 +179,7 @@ func runRelate(cmd *cobra.Command, args []string) error {
return encoder.Encode(result) return encoder.Encode(result)
} }
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Linked %s ↔ %s\n", ui.RenderPass("✓"), id1, id2)
fmt.Printf("%s Linked %s ↔ %s\n", green("✓"), id1, id2)
return nil return nil
} }
@@ -300,8 +301,7 @@ func runUnrelate(cmd *cobra.Command, args []string) error {
return encoder.Encode(result) return encoder.Encode(result)
} }
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Unlinked %s ↔ %s\n", ui.RenderPass("✓"), id1, id2)
fmt.Printf("%s Unlinked %s ↔ %s\n", green("✓"), id1, id2)
return nil return nil
} }
+14 -21
View File
@@ -11,17 +11,18 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var renamePrefixCmd = &cobra.Command{ var renamePrefixCmd = &cobra.Command{
Use: "rename-prefix <new-prefix>", Use: "rename-prefix <new-prefix>",
Short: "Rename the issue prefix for all issues in the database", GroupID: "advanced",
Short: "Rename the issue prefix for all issues in the database",
Long: `Rename the issue prefix for all issues in the database. Long: `Rename the issue prefix for all issues in the database.
This will update all issue IDs and all text references across all fields. This will update all issue IDs and all text references across all fields.
@@ -99,12 +100,10 @@ NOTE: This is a rare operation. Most users never need this command.`,
if len(prefixes) > 1 { if len(prefixes) > 1 {
// Multiple prefixes detected - requires repair mode // Multiple prefixes detected - requires repair mode
red := color.New(color.FgRed).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Fprintf(os.Stderr, "%s Multiple prefixes detected in database:\n", red("✗")) fmt.Fprintf(os.Stderr, "%s Multiple prefixes detected in database:\n", ui.RenderFail("✗"))
for prefix, count := range prefixes { for prefix, count := range prefixes {
fmt.Fprintf(os.Stderr, " - %s: %d issues\n", yellow(prefix), count) fmt.Fprintf(os.Stderr, " - %s: %d issues\n", ui.RenderWarn(prefix), count)
} }
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
@@ -141,8 +140,7 @@ NOTE: This is a rare operation. Most users never need this command.`,
} }
if dryRun { if dryRun {
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("DRY RUN: Would rename %d issues from prefix '%s' to '%s'\n\n", len(issues), oldPrefix, newPrefix)
fmt.Printf("DRY RUN: Would rename %d issues from prefix '%s' to '%s'\n\n", len(issues), oldPrefix, newPrefix)
fmt.Printf("Sample changes:\n") fmt.Printf("Sample changes:\n")
for i, issue := range issues { for i, issue := range issues {
if i >= 5 { if i >= 5 {
@@ -151,13 +149,11 @@ NOTE: This is a rare operation. Most users never need this command.`,
} }
oldID := fmt.Sprintf("%s-%s", oldPrefix, strings.TrimPrefix(issue.ID, oldPrefix+"-")) oldID := fmt.Sprintf("%s-%s", oldPrefix, strings.TrimPrefix(issue.ID, oldPrefix+"-"))
newID := fmt.Sprintf("%s-%s", newPrefix, strings.TrimPrefix(issue.ID, oldPrefix+"-")) newID := fmt.Sprintf("%s-%s", newPrefix, strings.TrimPrefix(issue.ID, oldPrefix+"-"))
fmt.Printf(" %s -> %s\n", cyan(oldID), cyan(newID)) fmt.Printf(" %s -> %s\n", ui.RenderAccent(oldID), ui.RenderAccent(newID))
} }
return return
} }
green := color.New(color.FgGreen).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("Renaming %d issues from prefix '%s' to '%s'...\n", len(issues), oldPrefix, newPrefix) fmt.Printf("Renaming %d issues from prefix '%s' to '%s'...\n", len(issues), oldPrefix, newPrefix)
@@ -169,7 +165,7 @@ NOTE: This is a rare operation. Most users never need this command.`,
// Schedule full export (IDs changed, incremental won't work) // Schedule full export (IDs changed, incremental won't work)
markDirtyAndScheduleFullExport() markDirtyAndScheduleFullExport()
fmt.Printf("%s Successfully renamed prefix from %s to %s\n", green("✓"), cyan(oldPrefix), cyan(newPrefix)) fmt.Printf("%s Successfully renamed prefix from %s to %s\n", ui.RenderPass("✓"), ui.RenderAccent(oldPrefix), ui.RenderAccent(newPrefix))
if jsonOutput { if jsonOutput {
result := map[string]interface{}{ result := map[string]interface{}{
@@ -230,9 +226,6 @@ type issueSort struct {
// Issues with the correct prefix are left unchanged. // Issues with the correct prefix are left unchanged.
// Issues with incorrect prefixes get new hash-based IDs. // Issues with incorrect prefixes get new hash-based IDs.
func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, targetPrefix string, issues []*types.Issue, prefixes map[string]int, dryRun bool) error { func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, targetPrefix string, issues []*types.Issue, prefixes map[string]int, dryRun bool) error {
green := color.New(color.FgGreen).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
// Separate issues into correct and incorrect prefix groups // Separate issues into correct and incorrect prefix groups
var correctIssues []*types.Issue var correctIssues []*types.Issue
@@ -290,7 +283,7 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t
if dryRun { if dryRun {
fmt.Printf("DRY RUN: Would repair %d issues with incorrect prefixes\n\n", len(incorrectIssues)) fmt.Printf("DRY RUN: Would repair %d issues with incorrect prefixes\n\n", len(incorrectIssues))
fmt.Printf("Issues with correct prefix (%s): %d\n", cyan(targetPrefix), len(correctIssues)) fmt.Printf("Issues with correct prefix (%s): %d\n", ui.RenderAccent(targetPrefix), len(correctIssues))
fmt.Printf("Issues to repair: %d\n\n", len(incorrectIssues)) fmt.Printf("Issues to repair: %d\n\n", len(incorrectIssues))
fmt.Printf("Planned renames (showing first 10):\n") fmt.Printf("Planned renames (showing first 10):\n")
@@ -301,14 +294,14 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t
} }
oldID := is.issue.ID oldID := is.issue.ID
newID := renameMap[oldID] newID := renameMap[oldID]
fmt.Printf(" %s -> %s\n", yellow(oldID), cyan(newID)) fmt.Printf(" %s -> %s\n", ui.RenderWarn(oldID), ui.RenderAccent(newID))
} }
return nil return nil
} }
// Perform the repairs // Perform the repairs
fmt.Printf("Repairing database with multiple prefixes...\n") fmt.Printf("Repairing database with multiple prefixes...\n")
fmt.Printf(" Issues with correct prefix (%s): %d\n", cyan(targetPrefix), len(correctIssues)) fmt.Printf(" Issues with correct prefix (%s): %d\n", ui.RenderAccent(targetPrefix), len(correctIssues))
fmt.Printf(" Issues to repair: %d\n\n", len(incorrectIssues)) fmt.Printf(" Issues to repair: %d\n\n", len(incorrectIssues))
// Pattern to match any issue ID reference in text (both hash and sequential IDs) // Pattern to match any issue ID reference in text (both hash and sequential IDs)
@@ -348,7 +341,7 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t
return fmt.Errorf("failed to update issue %s -> %s: %w", oldID, newID, err) return fmt.Errorf("failed to update issue %s -> %s: %w", oldID, newID, err)
} }
fmt.Printf(" Renamed %s -> %s\n", yellow(oldID), cyan(newID)) fmt.Printf(" Renamed %s -> %s\n", ui.RenderWarn(oldID), ui.RenderAccent(newID))
} }
// Update all dependencies to use new prefix // Update all dependencies to use new prefix
@@ -378,7 +371,7 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t
markDirtyAndScheduleFullExport() markDirtyAndScheduleFullExport()
fmt.Printf("\n%s Successfully consolidated %d prefixes into %s\n", fmt.Printf("\n%s Successfully consolidated %d prefixes into %s\n",
green("✓"), len(prefixes), cyan(targetPrefix)) ui.RenderPass("✓"), len(prefixes), ui.RenderAccent(targetPrefix))
fmt.Printf(" %d issues repaired, %d issues unchanged\n", len(incorrectIssues), len(correctIssues)) fmt.Printf(" %d issues repaired, %d issues unchanged\n", len(incorrectIssues), len(correctIssues))
if jsonOutput { if jsonOutput {
+6 -7
View File
@@ -3,15 +3,16 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var reopenCmd = &cobra.Command{ var reopenCmd = &cobra.Command{
Use: "reopen [id...]", Use: "reopen [id...]",
Short: "Reopen one or more closed issues", GroupID: "issues",
Short: "Reopen one or more closed issues",
Long: `Reopen closed issues by setting status to 'open' and clearing the closed_at timestamp. Long: `Reopen closed issues by setting status to 'open' and clearing the closed_at timestamp.
This is more explicit than 'bd update --status open' and emits a Reopened event.`, This is more explicit than 'bd update --status open' and emits a Reopened event.`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
@@ -76,12 +77,11 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
reopenedIssues = append(reopenedIssues, &issue) reopenedIssues = append(reopenedIssues, &issue)
} }
} else { } else {
blue := color.New(color.FgBlue).SprintFunc()
reasonMsg := "" reasonMsg := ""
if reason != "" { if reason != "" {
reasonMsg = ": " + reason reasonMsg = ": " + reason
} }
fmt.Printf("%s Reopened %s%s\n", blue("↻"), id, reasonMsg) fmt.Printf("%s Reopened %s%s\n", ui.RenderAccent("↻"), id, reasonMsg)
} }
} }
if jsonOutput && len(reopenedIssues) > 0 { if jsonOutput && len(reopenedIssues) > 0 {
@@ -120,12 +120,11 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
reopenedIssues = append(reopenedIssues, issue) reopenedIssues = append(reopenedIssues, issue)
} }
} else { } else {
blue := color.New(color.FgBlue).SprintFunc()
reasonMsg := "" reasonMsg := ""
if reason != "" { if reason != "" {
reasonMsg = ": " + reason reasonMsg = ": " + reason
} }
fmt.Printf("%s Reopened %s%s\n", blue("↻"), fullID, reasonMsg) fmt.Printf("%s Reopened %s%s\n", ui.RenderAccent("↻"), fullID, reasonMsg)
} }
} }
// Schedule auto-flush if any issues were reopened // Schedule auto-flush if any issues were reopened
+9 -11
View File
@@ -5,15 +5,17 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// TODO: Consider consolidating into 'bd doctor --fix' for simpler maintenance UX
var repairDepsCmd = &cobra.Command{ var repairDepsCmd = &cobra.Command{
Use: "repair-deps", Use: "repair-deps",
Short: "Find and fix orphaned dependency references", GroupID: "maint",
Short: "Find and fix orphaned dependency references",
Long: `Scans all issues for dependencies pointing to non-existent issues. Long: `Scans all issues for dependencies pointing to non-existent issues.
Reports orphaned dependencies and optionally removes them with --fix. Reports orphaned dependencies and optionally removes them with --fix.
@@ -105,13 +107,11 @@ Interactive mode with --interactive prompts for each orphan.`,
// Report results // Report results
if len(orphans) == 0 { if len(orphans) == 0 {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s No orphaned dependencies found\n\n", ui.RenderPass("✓"))
fmt.Printf("\n%s No orphaned dependencies found\n\n", green("✓"))
return return
} }
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("\n%s Found %d orphaned dependencies:\n\n", ui.RenderWarn("⚠"), len(orphans))
fmt.Printf("\n%s Found %d orphaned dependencies:\n\n", yellow("⚠"), len(orphans))
for i, o := range orphans { for i, o := range orphans {
fmt.Printf("%d. %s → %s (%s) [%s does not exist]\n", fmt.Printf("%d. %s → %s (%s) [%s does not exist]\n",
@@ -142,8 +142,7 @@ Interactive mode with --interactive prompts for each orphan.`,
} }
} }
markDirtyAndScheduleFlush() markDirtyAndScheduleFlush()
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("\n%s Fixed %d orphaned dependencies\n\n", ui.RenderPass("✓"), fixed)
fmt.Printf("\n%s Fixed %d orphaned dependencies\n\n", green("✓"), fixed)
} else if fix { } else if fix {
db := store.UnderlyingDB() db := store.UnderlyingDB()
for _, o := range orphans { for _, o := range orphans {
@@ -159,8 +158,7 @@ Interactive mode with --interactive prompts for each orphan.`,
} }
} }
markDirtyAndScheduleFlush() markDirtyAndScheduleFlush()
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Fixed %d orphaned dependencies\n\n", ui.RenderPass("✓"), len(orphans))
fmt.Printf("%s Fixed %d orphaned dependencies\n\n", green("✓"), len(orphans))
} else { } else {
fmt.Printf("Run with --fix to automatically remove orphaned dependencies\n") fmt.Printf("Run with --fix to automatically remove orphaned dependencies\n")
fmt.Printf("Run with --interactive to review each dependency\n\n") fmt.Printf("Run with --interactive to review each dependency\n\n")
+15 -17
View File
@@ -8,14 +8,15 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/git" "github.com/steveyegge/beads/internal/git"
"github.com/steveyegge/beads/internal/ui"
) )
var resetCmd = &cobra.Command{ var resetCmd = &cobra.Command{
Use: "reset", Use: "reset",
Short: "Remove all beads data and configuration", GroupID: "advanced",
Short: "Remove all beads data and configuration",
Long: `Reset beads to an uninitialized state, removing all local data. Long: `Reset beads to an uninitialized state, removing all local data.
This command removes: This command removes:
@@ -206,29 +207,26 @@ func showResetPreview(items []resetItem) {
return return
} }
yellow := color.New(color.FgYellow).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
fmt.Println(yellow("Reset preview (dry-run mode)")) fmt.Println(ui.RenderWarn("Reset preview (dry-run mode)"))
fmt.Println() fmt.Println()
fmt.Println("The following will be removed:") fmt.Println("The following will be removed:")
fmt.Println() fmt.Println()
for _, item := range items { for _, item := range items {
fmt.Printf(" %s %s\n", red("•"), item.Description) fmt.Printf(" %s %s\n", ui.RenderFail("•"), item.Description)
if item.Type != "config" { if item.Type != "config" {
fmt.Printf(" %s\n", item.Path) fmt.Printf(" %s\n", item.Path)
} }
} }
fmt.Println() fmt.Println()
fmt.Println(red("⚠ This operation cannot be undone!")) fmt.Println(ui.RenderFail("⚠ This operation cannot be undone!"))
fmt.Println() fmt.Println()
fmt.Printf("To proceed, run: %s\n", yellow("bd reset --force")) fmt.Printf("To proceed, run: %s\n", ui.RenderWarn("bd reset --force"))
} }
func performReset(items []resetItem, _, beadsDir string) { func performReset(items []resetItem, _, beadsDir string) {
green := color.New(color.FgGreen).SprintFunc()
var errors []string var errors []string
@@ -238,14 +236,14 @@ func performReset(items []resetItem, _, beadsDir string) {
pidFile := filepath.Join(beadsDir, "daemon.pid") pidFile := filepath.Join(beadsDir, "daemon.pid")
stopDaemonQuiet(pidFile) stopDaemonQuiet(pidFile)
if !jsonOutput { if !jsonOutput {
fmt.Printf("%s Stopped daemon\n", green("✓")) fmt.Printf("%s Stopped daemon\n", ui.RenderPass("✓"))
} }
case "hook": case "hook":
if err := os.Remove(item.Path); err != nil { if err := os.Remove(item.Path); err != nil {
errors = append(errors, fmt.Sprintf("failed to remove hook %s: %v", item.Path, err)) errors = append(errors, fmt.Sprintf("failed to remove hook %s: %v", item.Path, err))
} else if !jsonOutput { } else if !jsonOutput {
fmt.Printf("%s Removed %s\n", green("✓"), filepath.Base(item.Path)) fmt.Printf("%s Removed %s\n", ui.RenderPass("✓"), filepath.Base(item.Path))
} }
// Restore backup if exists // Restore backup if exists
backupPath := item.Path + ".backup" backupPath := item.Path + ".backup"
@@ -260,28 +258,28 @@ func performReset(items []resetItem, _, beadsDir string) {
_ = exec.Command("git", "config", "--unset", "merge.beads.driver").Run() _ = exec.Command("git", "config", "--unset", "merge.beads.driver").Run()
_ = exec.Command("git", "config", "--unset", "merge.beads.name").Run() _ = exec.Command("git", "config", "--unset", "merge.beads.name").Run()
if !jsonOutput { if !jsonOutput {
fmt.Printf("%s Removed merge driver config\n", green("✓")) fmt.Printf("%s Removed merge driver config\n", ui.RenderPass("✓"))
} }
case "gitattributes": case "gitattributes":
if err := removeGitattributesEntry(); err != nil { if err := removeGitattributesEntry(); err != nil {
errors = append(errors, fmt.Sprintf("failed to update .gitattributes: %v", err)) errors = append(errors, fmt.Sprintf("failed to update .gitattributes: %v", err))
} else if !jsonOutput { } else if !jsonOutput {
fmt.Printf("%s Updated .gitattributes\n", green("✓")) fmt.Printf("%s Updated .gitattributes\n", ui.RenderPass("✓"))
} }
case "worktrees": case "worktrees":
if err := os.RemoveAll(item.Path); err != nil { if err := os.RemoveAll(item.Path); err != nil {
errors = append(errors, fmt.Sprintf("failed to remove worktrees: %v", err)) errors = append(errors, fmt.Sprintf("failed to remove worktrees: %v", err))
} else if !jsonOutput { } else if !jsonOutput {
fmt.Printf("%s Removed sync worktrees\n", green("✓")) fmt.Printf("%s Removed sync worktrees\n", ui.RenderPass("✓"))
} }
case "directory": case "directory":
if err := os.RemoveAll(item.Path); err != nil { if err := os.RemoveAll(item.Path); err != nil {
errors = append(errors, fmt.Sprintf("failed to remove .beads: %v", err)) errors = append(errors, fmt.Sprintf("failed to remove .beads: %v", err))
} else if !jsonOutput { } else if !jsonOutput {
fmt.Printf("%s Removed .beads directory\n", green("✓")) fmt.Printf("%s Removed .beads directory\n", ui.RenderPass("✓"))
} }
} }
} }
@@ -305,7 +303,7 @@ func performReset(items []resetItem, _, beadsDir string) {
fmt.Printf(" • %s\n", e) fmt.Printf(" • %s\n", e)
} }
} else { } else {
fmt.Printf("%s Reset complete\n", green("✓")) fmt.Printf("%s Reset complete\n", ui.RenderPass("✓"))
fmt.Println() fmt.Println()
fmt.Println("To reinitialize beads, run: bd init") fmt.Println("To reinitialize beads, run: bd init")
} }
+21 -25
View File
@@ -8,14 +8,15 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
var restoreCmd = &cobra.Command{ var restoreCmd = &cobra.Command{
Use: "restore <issue-id>", Use: "restore <issue-id>",
Short: "Restore full history of a compacted issue from git", GroupID: "sync",
Short: "Restore full history of a compacted issue from git",
Long: `Restore full history of a compacted issue from git version control. Long: `Restore full history of a compacted issue from git version control.
When an issue is compacted, the git commit hash is saved. This command: When an issue is compacted, the git commit hash is saved. This command:
@@ -185,59 +186,54 @@ func readIssueFromJSONL(jsonlPath, issueID string) (*types.Issue, error) {
// displayRestoredIssue displays the restored issue in a readable format // displayRestoredIssue displays the restored issue in a readable format
func displayRestoredIssue(issue *types.Issue, commitHash string) { func displayRestoredIssue(issue *types.Issue, commitHash string) {
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s %s (restored from git commit %s)\n", ui.RenderAccent("📜"), ui.RenderBold(issue.ID), ui.RenderWarn(commitHash[:8]))
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s\n\n", ui.RenderBold(issue.Title))
yellow := color.New(color.FgYellow).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
fmt.Printf("\n%s %s (restored from git commit %s)\n", cyan("📜"), bold(issue.ID), yellow(commitHash[:8]))
fmt.Printf("%s\n\n", bold(issue.Title))
if issue.Description != "" { if issue.Description != "" {
fmt.Printf("%s\n%s\n\n", bold("Description:"), issue.Description) fmt.Printf("%s\n%s\n\n", ui.RenderBold("Description:"), issue.Description)
} }
if issue.Design != "" { if issue.Design != "" {
fmt.Printf("%s\n%s\n\n", bold("Design:"), issue.Design) fmt.Printf("%s\n%s\n\n", ui.RenderBold("Design:"), issue.Design)
} }
if issue.AcceptanceCriteria != "" { if issue.AcceptanceCriteria != "" {
fmt.Printf("%s\n%s\n\n", bold("Acceptance Criteria:"), issue.AcceptanceCriteria) fmt.Printf("%s\n%s\n\n", ui.RenderBold("Acceptance Criteria:"), issue.AcceptanceCriteria)
} }
if issue.Notes != "" { if issue.Notes != "" {
fmt.Printf("%s\n%s\n\n", bold("Notes:"), issue.Notes) fmt.Printf("%s\n%s\n\n", ui.RenderBold("Notes:"), issue.Notes)
} }
fmt.Printf("%s %s | %s %d | %s %s\n", fmt.Printf("%s %s | %s %d | %s %s\n",
bold("Status:"), issue.Status, ui.RenderBold("Status:"), issue.Status,
bold("Priority:"), issue.Priority, ui.RenderBold("Priority:"), issue.Priority,
bold("Type:"), issue.IssueType, ui.RenderBold("Type:"), issue.IssueType,
) )
if issue.Assignee != "" { if issue.Assignee != "" {
fmt.Printf("%s %s\n", bold("Assignee:"), issue.Assignee) fmt.Printf("%s %s\n", ui.RenderBold("Assignee:"), issue.Assignee)
} }
if len(issue.Labels) > 0 { if len(issue.Labels) > 0 {
fmt.Printf("%s %s\n", bold("Labels:"), strings.Join(issue.Labels, ", ")) fmt.Printf("%s %s\n", ui.RenderBold("Labels:"), strings.Join(issue.Labels, ", "))
} }
fmt.Printf("\n%s %s\n", bold("Created:"), issue.CreatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("\n%s %s\n", ui.RenderBold("Created:"), issue.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Printf("%s %s\n", bold("Updated:"), issue.UpdatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("%s %s\n", ui.RenderBold("Updated:"), issue.UpdatedAt.Format("2006-01-02 15:04:05"))
if issue.ClosedAt != nil { if issue.ClosedAt != nil {
fmt.Printf("%s %s\n", bold("Closed:"), issue.ClosedAt.Format("2006-01-02 15:04:05")) fmt.Printf("%s %s\n", ui.RenderBold("Closed:"), issue.ClosedAt.Format("2006-01-02 15:04:05"))
} }
if len(issue.Dependencies) > 0 { if len(issue.Dependencies) > 0 {
fmt.Printf("\n%s\n", bold("Dependencies:")) fmt.Printf("\n%s\n", ui.RenderBold("Dependencies:"))
for _, dep := range issue.Dependencies { for _, dep := range issue.Dependencies {
fmt.Printf(" %s %s (%s)\n", green("→"), dep.DependsOnID, dep.Type) fmt.Printf(" %s %s (%s)\n", ui.RenderPass("→"), dep.DependsOnID, dep.Type)
} }
} }
if issue.CompactionLevel > 0 { if issue.CompactionLevel > 0 {
fmt.Printf("\n%s Level %d", yellow("⚠️ This issue was compacted:"), issue.CompactionLevel) fmt.Printf("\n%s Level %d", ui.RenderWarn("⚠️ This issue was compacted:"), issue.CompactionLevel)
if issue.CompactedAt != nil { if issue.CompactedAt != nil {
fmt.Printf(" at %s", issue.CompactedAt.Format("2006-01-02 15:04:05")) fmt.Printf(" at %s", issue.CompactedAt.Format("2006-01-02 15:04:05"))
} }
+26 -34
View File
@@ -9,21 +9,22 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/hooks" "github.com/steveyegge/beads/internal/hooks"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
"github.com/steveyegge/beads/internal/validation" "github.com/steveyegge/beads/internal/validation"
) )
var showCmd = &cobra.Command{ var showCmd = &cobra.Command{
Use: "show [id...]", Use: "show [id...]",
Short: "Show issue details", GroupID: "issues",
Args: cobra.MinimumNArgs(1), Short: "Show issue details",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
jsonOutput, _ := cmd.Flags().GetBool("json") jsonOutput, _ := cmd.Flags().GetBool("json")
showThread, _ := cmd.Flags().GetBool("thread") showThread, _ := cmd.Flags().GetBool("thread")
@@ -118,8 +119,6 @@ var showCmd = &cobra.Command{
} }
issue := &details.Issue issue := &details.Issue
cyan := color.New(color.FgCyan).SprintFunc()
// Format output (same as direct mode below) // Format output (same as direct mode below)
tierEmoji := "" tierEmoji := ""
statusSuffix := "" statusSuffix := ""
@@ -132,7 +131,7 @@ var showCmd = &cobra.Command{
statusSuffix = " (compacted L2)" statusSuffix = " (compacted L2)"
} }
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji) fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix) fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
if issue.CloseReason != "" { if issue.CloseReason != "" {
fmt.Printf("Close reason: %s\n", issue.CloseReason) fmt.Printf("Close reason: %s\n", issue.CloseReason)
@@ -299,8 +298,6 @@ var showCmd = &cobra.Command{
fmt.Println("\n" + strings.Repeat("─", 60)) fmt.Println("\n" + strings.Repeat("─", 60))
} }
cyan := color.New(color.FgCyan).SprintFunc()
// Add compaction emoji to title line // Add compaction emoji to title line
tierEmoji := "" tierEmoji := ""
statusSuffix := "" statusSuffix := ""
@@ -313,7 +310,7 @@ var showCmd = &cobra.Command{
statusSuffix = " (compacted L2)" statusSuffix = " (compacted L2)"
} }
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji) fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix) fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
if issue.CloseReason != "" { if issue.CloseReason != "" {
fmt.Printf("Close reason: %s\n", issue.CloseReason) fmt.Printf("Close reason: %s\n", issue.CloseReason)
@@ -463,9 +460,10 @@ var showCmd = &cobra.Command{
} }
var updateCmd = &cobra.Command{ var updateCmd = &cobra.Command{
Use: "update [id...]", Use: "update [id...]",
Short: "Update one or more issues", GroupID: "issues",
Args: cobra.MinimumNArgs(1), Short: "Update one or more issues",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("update") CheckReadonly("update")
jsonOutput, _ := cmd.Flags().GetBool("json") jsonOutput, _ := cmd.Flags().GetBool("json")
@@ -659,8 +657,7 @@ var updateCmd = &cobra.Command{
} }
} }
if !jsonOutput { if !jsonOutput {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), id)
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
} }
} }
@@ -754,8 +751,7 @@ var updateCmd = &cobra.Command{
updatedIssues = append(updatedIssues, updatedIssue) updatedIssues = append(updatedIssues, updatedIssue)
} }
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), id)
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
} }
} }
@@ -771,8 +767,9 @@ var updateCmd = &cobra.Command{
} }
var editCmd = &cobra.Command{ var editCmd = &cobra.Command{
Use: "edit [id]", Use: "edit [id]",
Short: "Edit an issue field in $EDITOR", GroupID: "issues",
Short: "Edit an issue field in $EDITOR",
Long: `Edit an issue field using your configured $EDITOR. Long: `Edit an issue field using your configured $EDITOR.
By default, edits the description. Use flags to edit other fields. By default, edits the description. Use flags to edit other fields.
@@ -962,16 +959,16 @@ Examples:
markDirtyAndScheduleFlush() markDirtyAndScheduleFlush()
} }
green := color.New(color.FgGreen).SprintFunc()
fieldName := strings.ReplaceAll(fieldToEdit, "_", " ") fieldName := strings.ReplaceAll(fieldToEdit, "_", " ")
fmt.Printf("%s Updated %s for issue: %s\n", green("✓"), fieldName, id) fmt.Printf("%s Updated %s for issue: %s\n", ui.RenderPass("✓"), fieldName, id)
}, },
} }
var closeCmd = &cobra.Command{ var closeCmd = &cobra.Command{
Use: "close [id...]", Use: "close [id...]",
Short: "Close one or more issues", GroupID: "issues",
Args: cobra.MinimumNArgs(1), Short: "Close one or more issues",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("close") CheckReadonly("close")
reason, _ := cmd.Flags().GetString("reason") reason, _ := cmd.Flags().GetString("reason")
@@ -1053,8 +1050,7 @@ var closeCmd = &cobra.Command{
} }
} }
if !jsonOutput { if !jsonOutput {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Closed %s: %s\n", ui.RenderPass("✓"), id, reason)
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
} }
} }
@@ -1100,8 +1096,7 @@ var closeCmd = &cobra.Command{
closedIssues = append(closedIssues, closedIssue) closedIssues = append(closedIssues, closedIssue)
} }
} else { } else {
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Closed %s: %s\n", ui.RenderPass("✓"), id, reason)
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
} }
} }
@@ -1221,10 +1216,7 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
} }
// Display the thread // Display the thread
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Thread: %s\n", ui.RenderAccent("📬"), rootMsg.Title)
dim := color.New(color.Faint).SprintFunc()
fmt.Printf("\n%s Thread: %s\n", cyan("📬"), rootMsg.Title)
fmt.Println(strings.Repeat("─", 66)) fmt.Println(strings.Repeat("─", 66))
for _, msg := range threadMessages { for _, msg := range threadMessages {
@@ -1246,12 +1238,12 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
statusIcon = "✓" statusIcon = "✓"
} }
fmt.Printf("%s%s %s %s\n", indent, statusIcon, cyan(msg.ID), dim(timeStr)) fmt.Printf("%s%s %s %s\n", indent, statusIcon, ui.RenderAccent(msg.ID), ui.RenderMuted(timeStr))
fmt.Printf("%s From: %s To: %s\n", indent, msg.Sender, msg.Assignee) fmt.Printf("%s From: %s To: %s\n", indent, msg.Sender, msg.Assignee)
if parentID := repliesTo[msg.ID]; parentID != "" { if parentID := repliesTo[msg.ID]; parentID != "" {
fmt.Printf("%s Re: %s\n", indent, parentID) fmt.Printf("%s Re: %s\n", indent, parentID)
} }
fmt.Printf("%s %s: %s\n", indent, dim("Subject"), msg.Title) fmt.Printf("%s %s: %s\n", indent, ui.RenderMuted("Subject"), msg.Title)
if msg.Description != "" { if msg.Description != "" {
// Indent the body // Indent the body
bodyLines := strings.Split(msg.Description, "\n") bodyLines := strings.Split(msg.Description, "\n")
+10 -17
View File
@@ -9,11 +9,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
@@ -39,8 +39,9 @@ type InstantiateResult struct {
} }
var templateCmd = &cobra.Command{ var templateCmd = &cobra.Command{
Use: "template", Use: "template",
Short: "Manage issue templates", GroupID: "setup",
Short: "Manage issue templates",
Long: `Manage Beads templates for creating issue hierarchies. Long: `Manage Beads templates for creating issue hierarchies.
Templates are epics with the "template" label. They can have child issues Templates are epics with the "template" label. They can have child issues
@@ -106,17 +107,14 @@ var templateListCmd = &cobra.Command{
return return
} }
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s\n", ui.RenderPass("Templates (for bd template instantiate):"))
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("%s\n", green("Templates (for bd template instantiate):"))
for _, tmpl := range beadsTemplates { for _, tmpl := range beadsTemplates {
vars := extractVariables(tmpl.Title + " " + tmpl.Description) vars := extractVariables(tmpl.Title + " " + tmpl.Description)
varStr := "" varStr := ""
if len(vars) > 0 { if len(vars) > 0 {
varStr = fmt.Sprintf(" (vars: %s)", strings.Join(vars, ", ")) varStr = fmt.Sprintf(" (vars: %s)", strings.Join(vars, ", "))
} }
fmt.Printf(" %s: %s%s\n", cyan(tmpl.ID), tmpl.Title, varStr) fmt.Printf(" %s: %s%s\n", ui.RenderAccent(tmpl.ID), tmpl.Title, varStr)
} }
fmt.Println() fmt.Println()
}, },
@@ -175,25 +173,21 @@ func showBeadsTemplate(subgraph *TemplateSubgraph) {
return return
} }
cyan := color.New(color.FgCyan).SprintFunc() fmt.Printf("\n%s Template: %s\n", ui.RenderAccent("📋"), subgraph.Root.Title)
yellow := color.New(color.FgYellow).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("\n%s Template: %s\n", cyan("📋"), subgraph.Root.Title)
fmt.Printf(" ID: %s\n", subgraph.Root.ID) fmt.Printf(" ID: %s\n", subgraph.Root.ID)
fmt.Printf(" Issues: %d\n", len(subgraph.Issues)) fmt.Printf(" Issues: %d\n", len(subgraph.Issues))
// Show variables // Show variables
vars := extractAllVariables(subgraph) vars := extractAllVariables(subgraph)
if len(vars) > 0 { if len(vars) > 0 {
fmt.Printf("\n%s Variables:\n", yellow("📝")) fmt.Printf("\n%s Variables:\n", ui.RenderWarn("📝"))
for _, v := range vars { for _, v := range vars {
fmt.Printf(" {{%s}}\n", v) fmt.Printf(" {{%s}}\n", v)
} }
} }
// Show structure // Show structure
fmt.Printf("\n%s Structure:\n", green("🌲")) fmt.Printf("\n%s Structure:\n", ui.RenderPass("🌲"))
printTemplateTree(subgraph, subgraph.Root.ID, 0, true) printTemplateTree(subgraph, subgraph.Root.ID, 0, true)
fmt.Println() fmt.Println()
} }
@@ -309,8 +303,7 @@ Example:
return return
} }
green := color.New(color.FgGreen).SprintFunc() fmt.Printf("%s Created %d issues from template\n", ui.RenderPass("✓"), result.Created)
fmt.Printf("%s Created %d issues from template\n", green("✓"), result.Created)
fmt.Printf(" New epic: %s\n", result.NewEpicID) fmt.Printf(" New epic: %s\n", result.NewEpicID)
}, },
} }
+6 -7
View File
@@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils" "github.com/steveyegge/beads/internal/utils"
) )
var unpinCmd = &cobra.Command{ var unpinCmd = &cobra.Command{
Use: "unpin [id...]", Use: "unpin [id...]",
Short: "Unpin one or more issues", GroupID: "issues",
Short: "Unpin one or more issues",
Long: `Unpin issues to remove their persistent context marker status. Long: `Unpin issues to remove their persistent context marker status.
This restores the issue to a normal work item that can be cleaned up This restores the issue to a normal work item that can be cleaned up
@@ -78,8 +79,7 @@ Examples:
unpinnedIssues = append(unpinnedIssues, &issue) unpinnedIssues = append(unpinnedIssues, &issue)
} }
} else { } else {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("%s Unpinned %s\n", ui.RenderWarn("📍"), id)
fmt.Printf("%s Unpinned %s\n", yellow("📍"), id)
} }
} }
@@ -117,8 +117,7 @@ Examples:
unpinnedIssues = append(unpinnedIssues, issue) unpinnedIssues = append(unpinnedIssues, issue)
} }
} else { } else {
yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("%s Unpinned %s\n", ui.RenderWarn("📍"), fullID)
fmt.Printf("%s Unpinned %s\n", yellow("📍"), fullID)
} }
} }
+16 -16
View File
@@ -5,13 +5,15 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
) )
// TODO: Consider consolidating into 'bd doctor --fix' for simpler maintenance UX
var validateCmd = &cobra.Command{ var validateCmd = &cobra.Command{
Use: "validate", Use: "validate",
Short: "Run comprehensive database health checks", GroupID: "maint",
Short: "Run comprehensive database health checks",
Long: `Run all validation checks to ensure database integrity: Long: `Run all validation checks to ensure database integrity:
- Orphaned dependencies (references to deleted issues) - Orphaned dependencies (references to deleted issues)
- Duplicate issues (identical content) - Duplicate issues (identical content)
@@ -193,9 +195,6 @@ func (r *validationResults) toJSON() map[string]interface{} {
return output return output
} }
func (r *validationResults) print(_ bool) { func (r *validationResults) print(_ bool) {
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
fmt.Println("\nValidation Results:") fmt.Println("\nValidation Results:")
fmt.Println("===================") fmt.Println("===================")
totalIssues := 0 totalIssues := 0
@@ -204,33 +203,34 @@ func (r *validationResults) print(_ bool) {
for _, name := range r.checkOrder { for _, name := range r.checkOrder {
result := r.checks[name] result := r.checks[name]
prefix := "✓" prefix := "✓"
colorFunc := green var coloredPrefix string
if result.err != nil { if result.err != nil {
prefix = "✗" prefix = "✗"
colorFunc = red coloredPrefix = ui.RenderFail(prefix)
fmt.Printf("%s %s: ERROR - %v\n", colorFunc(prefix), result.name, result.err) fmt.Printf("%s %s: ERROR - %v\n", coloredPrefix, result.name, result.err)
} else if result.issueCount > 0 { } else if result.issueCount > 0 {
prefix = "⚠" prefix = "⚠"
colorFunc = yellow coloredPrefix = ui.RenderWarn(prefix)
if result.fixedCount > 0 { if result.fixedCount > 0 {
fmt.Printf("%s %s: %d found, %d fixed\n", colorFunc(prefix), result.name, result.issueCount, result.fixedCount) fmt.Printf("%s %s: %d found, %d fixed\n", coloredPrefix, result.name, result.issueCount, result.fixedCount)
} else { } else {
fmt.Printf("%s %s: %d found\n", colorFunc(prefix), result.name, result.issueCount) fmt.Printf("%s %s: %d found\n", coloredPrefix, result.name, result.issueCount)
} }
} else { } else {
fmt.Printf("%s %s: OK\n", colorFunc(prefix), result.name) coloredPrefix = ui.RenderPass(prefix)
fmt.Printf("%s %s: OK\n", coloredPrefix, result.name)
} }
totalIssues += result.issueCount totalIssues += result.issueCount
totalFixed += result.fixedCount totalFixed += result.fixedCount
} }
fmt.Println() fmt.Println()
if totalIssues == 0 { if totalIssues == 0 {
fmt.Printf("%s Database is healthy!\n", green("✓")) fmt.Printf("%s Database is healthy!\n", ui.RenderPass("✓"))
} else if totalFixed == totalIssues { } else if totalFixed == totalIssues {
fmt.Printf("%s Fixed all %d issues\n", green("✓"), totalFixed) fmt.Printf("%s Fixed all %d issues\n", ui.RenderPass("✓"), totalFixed)
} else { } else {
remaining := totalIssues - totalFixed remaining := totalIssues - totalFixed
fmt.Printf("%s Found %d issues", yellow("⚠"), totalIssues) fmt.Printf("%s Found %d issues", ui.RenderWarn("⚠"), totalIssues)
if totalFixed > 0 { if totalFixed > 0 {
fmt.Printf(" (fixed %d, %d remaining)", totalFixed, remaining) fmt.Printf(" (fixed %d, %d remaining)", totalFixed, remaining)
} }
+114
View File
@@ -0,0 +1,114 @@
# UI/UX Philosophy
Beads CLI follows Tufte-inspired design principles for terminal output, using semantic color tokens with adaptive light/dark mode support via Lipgloss.
## Core Principles
### 1. Maximize Data-Ink Ratio (Tufte)
Only color what demands attention. Every colored element should serve a purpose:
- Navigation landmarks (section headers, group titles)
- Scan targets (command names, flag names)
- Semantic states (success, warning, error, blocked)
**Anti-pattern**: Coloring everything defeats the purpose and creates cognitive overload.
### 2. Semantic Color Tokens
Use meaning-based tokens, not raw colors:
| Token | Semantic Meaning | Use Cases |
|-------|-----------------|-----------|
| `Pass` | Success, completion, ready | Checkmarks, completed items, healthy status |
| `Warn` | Attention needed, caution | Warnings, in-progress items, action required |
| `Fail` | Error, blocked, critical | Errors, blocked items, failures |
| `Accent` | Navigation, emphasis | Headers, links, key information |
| `Muted` | De-emphasized, secondary | Defaults, closed items, metadata |
| `Command` | Interactive elements | Command names, flags |
### 3. Perceptual Optimization (Light/Dark Modes)
Lipgloss `AdaptiveColor` ensures optimal contrast in both terminal modes:
```go
ColorPass = lipgloss.AdaptiveColor{
Light: "#86b300", // Darker green for light backgrounds
Dark: "#c2d94c", // Brighter green for dark backgrounds
}
```
**Why this matters**:
- Light terminals need darker colors for contrast
- Dark terminals need brighter colors for visibility
- Same semantic meaning, optimized perception
### 4. Respect Cognitive Load
Let whitespace and position do most of the work:
- Group related information visually
- Use indentation for hierarchy
- Reserve color for exceptional states
## Color Usage Guide
### When to Color
| Situation | Style | Rationale |
|-----------|-------|-----------|
| Navigation landmarks | Accent | Helps users orient in output |
| Command/flag names | Bold | Creates vertical scan targets |
| Success indicators | Pass (green) | Immediate positive feedback |
| Warnings | Warn (yellow) | Draws attention without alarm |
| Errors | Fail (red) | Demands immediate attention |
| Closed/done items | Muted | Visually recedes, "done" |
| High priority (P0/P1) | Semantic color | Only urgent items deserve color |
| Normal priority (P2+) | Plain | Most items don't need highlighting |
### When NOT to Color
- **Descriptions and prose**: Let content speak for itself
- **Examples in help text**: Keep copy-paste friendly
- **Every list item**: Only color exceptional states
- **Decorative purposes**: Color is functional, not aesthetic
## Ayu Theme
All colors use the [Ayu theme](https://github.com/ayu-theme/ayu-colors) for consistency:
```go
// Semantic colors with light/dark adaptation
ColorPass = AdaptiveColor{Light: "#86b300", Dark: "#c2d94c"} // Green
ColorWarn = AdaptiveColor{Light: "#f2ae49", Dark: "#ffb454"} // Yellow
ColorFail = AdaptiveColor{Light: "#f07171", Dark: "#f07178"} // Red
ColorAccent = AdaptiveColor{Light: "#399ee6", Dark: "#59c2ff"} // Blue
ColorMuted = AdaptiveColor{Light: "#828c99", Dark: "#6c7680"} // Gray
```
## Implementation
All styling is centralized in `internal/ui/styles.go`:
```go
// Render functions for semantic styling
ui.RenderPass("✓") // Success indicator
ui.RenderWarn("⚠") // Warning indicator
ui.RenderFail("✗") // Error indicator
ui.RenderAccent("→") // Accent/link
ui.RenderMuted("...") // Secondary info
ui.RenderBold("name") // Emphasis
ui.RenderCommand("bd") // Command reference
```
## Help Text Styling
Following Tufte's principle of layered information:
1. **Section headers** (`Flags:`, `Examples:`) - Accent color for navigation
2. **Flag names** (`--file`) - Bold for scannability
3. **Type annotations** (`string`) - Muted, reference info
4. **Default values** (`(default: ...)`) - Muted, secondary
5. **Descriptions** - Plain, primary content
6. **Examples** - Plain, copy-paste friendly
## References
- Tufte, E. (2001). *The Visual Display of Quantitative Information*
- [Ayu Theme Colors](https://github.com/ayu-theme/ayu-colors)
- [Lipgloss - Terminal Styling](https://github.com/charmbracelet/lipgloss)
- [WCAG Color Contrast Guidelines](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)
-2
View File
@@ -8,7 +8,6 @@ require (
github.com/anthropics/anthropic-sdk-go v1.19.0 github.com/anthropics/anthropic-sdk-go v1.19.0
github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/ncruces/go-sqlite3 v0.30.3 github.com/ncruces/go-sqlite3 v0.30.3
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
@@ -39,7 +38,6 @@ require (
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
-6
View File
@@ -47,8 +47,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -65,9 +63,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -138,7 +133,6 @@ golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+233 -2
View File
@@ -3,6 +3,7 @@
package ui package ui
import ( import (
"fmt"
"strings" "strings"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@@ -11,8 +12,9 @@ import (
// Ayu theme color palette // Ayu theme color palette
// Dark: https://terminalcolors.com/themes/ayu/dark/ // Dark: https://terminalcolors.com/themes/ayu/dark/
// Light: https://terminalcolors.com/themes/ayu/light/ // Light: https://terminalcolors.com/themes/ayu/light/
// Source: https://github.com/ayu-theme/ayu-colors
var ( var (
// Semantic status colors (Ayu theme - adaptive light/dark) // Core semantic colors (Ayu theme - adaptive light/dark)
ColorPass = lipgloss.AdaptiveColor{ ColorPass = lipgloss.AdaptiveColor{
Light: "#86b300", // ayu light bright green Light: "#86b300", // ayu light bright green
Dark: "#c2d94c", // ayu dark bright green Dark: "#c2d94c", // ayu dark bright green
@@ -33,9 +35,86 @@ var (
Light: "#399ee6", // ayu light bright blue Light: "#399ee6", // ayu light bright blue
Dark: "#59c2ff", // ayu dark bright blue Dark: "#59c2ff", // ayu dark bright blue
} }
// === Workflow Status Colors ===
// Only actionable states get color - open/closed match standard text
ColorStatusOpen = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorStatusInProgress = lipgloss.AdaptiveColor{
Light: "#f2ae49", // yellow - active work, very visible
Dark: "#ffb454",
}
ColorStatusClosed = lipgloss.AdaptiveColor{
Light: "#9099a1", // slightly dimmed - visually shows "done"
Dark: "#8090a0",
}
ColorStatusBlocked = lipgloss.AdaptiveColor{
Light: "#f07171", // red - needs attention
Dark: "#f26d78",
}
ColorStatusPinned = lipgloss.AdaptiveColor{
Light: "#d2a6ff", // purple - special/elevated
Dark: "#d2a6ff",
}
// === Priority Colors ===
// Only P0/P1 get color - P2/P3/P4 match standard text
ColorPriorityP0 = lipgloss.AdaptiveColor{
Light: "#f07171", // bright red - critical
Dark: "#f07178",
}
ColorPriorityP1 = lipgloss.AdaptiveColor{
Light: "#ff8f40", // orange - high urgency
Dark: "#ff8f40",
}
ColorPriorityP2 = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorPriorityP3 = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorPriorityP4 = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
// === Issue Type Colors ===
// Bugs and epics get color - they need attention
// All other types use standard text
ColorTypeBug = lipgloss.AdaptiveColor{
Light: "#f07171", // bright red - bugs are problems
Dark: "#f26d78",
}
ColorTypeFeature = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorTypeTask = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorTypeEpic = lipgloss.AdaptiveColor{
Light: "#d2a6ff", // purple - larger scope work
Dark: "#d2a6ff",
}
ColorTypeChore = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
// === Issue ID Color ===
// IDs use standard text color - subtle, not attention-grabbing
ColorID = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
) )
// Status styles - consistent across all commands // Core styles - consistent across all commands
var ( var (
PassStyle = lipgloss.NewStyle().Foreground(ColorPass) PassStyle = lipgloss.NewStyle().Foreground(ColorPass)
WarnStyle = lipgloss.NewStyle().Foreground(ColorWarn) WarnStyle = lipgloss.NewStyle().Foreground(ColorWarn)
@@ -44,6 +123,36 @@ var (
AccentStyle = lipgloss.NewStyle().Foreground(ColorAccent) AccentStyle = lipgloss.NewStyle().Foreground(ColorAccent)
) )
// Issue ID style
var IDStyle = lipgloss.NewStyle().Foreground(ColorID)
// Status styles for workflow states
var (
StatusOpenStyle = lipgloss.NewStyle().Foreground(ColorStatusOpen)
StatusInProgressStyle = lipgloss.NewStyle().Foreground(ColorStatusInProgress)
StatusClosedStyle = lipgloss.NewStyle().Foreground(ColorStatusClosed)
StatusBlockedStyle = lipgloss.NewStyle().Foreground(ColorStatusBlocked)
StatusPinnedStyle = lipgloss.NewStyle().Foreground(ColorStatusPinned)
)
// Priority styles
var (
PriorityP0Style = lipgloss.NewStyle().Foreground(ColorPriorityP0).Bold(true)
PriorityP1Style = lipgloss.NewStyle().Foreground(ColorPriorityP1)
PriorityP2Style = lipgloss.NewStyle().Foreground(ColorPriorityP2)
PriorityP3Style = lipgloss.NewStyle().Foreground(ColorPriorityP3)
PriorityP4Style = lipgloss.NewStyle().Foreground(ColorPriorityP4)
)
// Type styles for issue categories
var (
TypeBugStyle = lipgloss.NewStyle().Foreground(ColorTypeBug)
TypeFeatureStyle = lipgloss.NewStyle().Foreground(ColorTypeFeature)
TypeTaskStyle = lipgloss.NewStyle().Foreground(ColorTypeTask)
TypeEpicStyle = lipgloss.NewStyle().Foreground(ColorTypeEpic)
TypeChoreStyle = lipgloss.NewStyle().Foreground(ColorTypeChore)
)
// CategoryStyle for section headers - bold with accent color // CategoryStyle for section headers - bold with accent color
var CategoryStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent) var CategoryStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent)
@@ -128,3 +237,125 @@ func RenderSkipIcon() string {
func RenderInfoIcon() string { func RenderInfoIcon() string {
return AccentStyle.Render(IconInfo) return AccentStyle.Render(IconInfo)
} }
// === Issue Component Renderers ===
// RenderID renders an issue ID with semantic styling
func RenderID(id string) string {
return IDStyle.Render(id)
}
// RenderStatus renders a status with semantic styling
// in_progress/blocked/pinned get color; open/closed use standard text
func RenderStatus(status string) string {
switch status {
case "in_progress":
return StatusInProgressStyle.Render(status)
case "blocked":
return StatusBlockedStyle.Render(status)
case "pinned":
return StatusPinnedStyle.Render(status)
case "closed":
return StatusClosedStyle.Render(status)
default: // open and others
return StatusOpenStyle.Render(status)
}
}
// RenderPriority renders a priority level with semantic styling
// P0/P1 get color; P2/P3/P4 use standard text
func RenderPriority(priority int) string {
label := fmt.Sprintf("P%d", priority)
switch priority {
case 0:
return PriorityP0Style.Render(label)
case 1:
return PriorityP1Style.Render(label)
case 2:
return PriorityP2Style.Render(label)
case 3:
return PriorityP3Style.Render(label)
case 4:
return PriorityP4Style.Render(label)
default:
return label
}
}
// RenderType renders an issue type with semantic styling
// bugs get color; all other types use standard text
func RenderType(issueType string) string {
switch issueType {
case "bug":
return TypeBugStyle.Render(issueType)
case "feature":
return TypeFeatureStyle.Render(issueType)
case "task":
return TypeTaskStyle.Render(issueType)
case "epic":
return TypeEpicStyle.Render(issueType)
case "chore":
return TypeChoreStyle.Render(issueType)
default:
return issueType
}
}
// RenderIssueCompact renders a compact one-line issue summary
// Format: ID [Priority] [Type] Status - Title
// When status is "closed", the entire line is dimmed to show it's done
func RenderIssueCompact(id string, priority int, issueType, status, title string) string {
line := fmt.Sprintf("%s [P%d] [%s] %s - %s",
id, priority, issueType, status, title)
if status == "closed" {
// Entire line is dimmed - visually shows "done"
return StatusClosedStyle.Render(line)
}
return fmt.Sprintf("%s [%s] [%s] %s - %s",
RenderID(id),
RenderPriority(priority),
RenderType(issueType),
RenderStatus(status),
title,
)
}
// RenderPriorityForStatus renders priority with color only if not closed
func RenderPriorityForStatus(priority int, status string) string {
if status == "closed" {
return fmt.Sprintf("P%d", priority)
}
return RenderPriority(priority)
}
// RenderTypeForStatus renders type with color only if not closed
func RenderTypeForStatus(issueType, status string) string {
if status == "closed" {
return issueType
}
return RenderType(issueType)
}
// RenderClosedLine renders an entire line in the closed/dimmed style
func RenderClosedLine(line string) string {
return StatusClosedStyle.Render(line)
}
// BoldStyle for emphasis
var BoldStyle = lipgloss.NewStyle().Bold(true)
// CommandStyle for command names - subtle contrast, not attention-grabbing
var CommandStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#5c6166", // slightly darker than standard
Dark: "#bfbdb6", // slightly brighter than standard
})
// RenderBold renders text in bold
func RenderBold(s string) string {
return BoldStyle.Render(s)
}
// RenderCommand renders a command name with subtle styling
func RenderCommand(s string) string {
return CommandStyle.Render(s)
}