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:
@@ -14,11 +14,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/debug"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
@@ -566,10 +566,9 @@ func flushToJSONLWithState(state flushState) {
|
||||
|
||||
// Show prominent warning after 3+ consecutive failures
|
||||
if failCount >= 3 {
|
||||
red := color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
fmt.Fprintf(os.Stderr, "\n%s\n", red("⚠️ CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!"))
|
||||
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", red("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
|
||||
fmt.Fprintf(os.Stderr, "\n%s\n", ui.RenderFail("⚠️ 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\n", ui.RenderFail("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -601,10 +600,9 @@ func flushToJSONLWithState(state flushState) {
|
||||
|
||||
// Show prominent warning after 3+ consecutive failures
|
||||
if failCount >= 3 {
|
||||
red := color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
fmt.Fprintf(os.Stderr, "\n%s\n", red("⚠️ CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!"))
|
||||
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", red("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
|
||||
fmt.Fprintf(os.Stderr, "\n%s\n", ui.RenderFail("⚠️ 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\n", ui.RenderFail("⚠️ Run 'bd export -o .beads/issues.jsonl' manually to fix."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"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{
|
||||
Use: "clean",
|
||||
Short: "Clean up temporary git merge artifacts from .beads directory",
|
||||
Use: "clean",
|
||||
GroupID: "maint",
|
||||
Short: "Clean up temporary git merge artifacts from .beads directory",
|
||||
Long: `Delete temporary git merge artifacts from the .beads directory.
|
||||
|
||||
This command removes temporary files created during git merges and conflicts.
|
||||
@@ -76,7 +78,7 @@ SEE ALSO:
|
||||
// Just run by default, no --force needed
|
||||
|
||||
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))
|
||||
for _, file := range filesToDelete {
|
||||
|
||||
@@ -6,16 +6,18 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// 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{
|
||||
Use: "cleanup",
|
||||
Short: "Delete closed issues and prune expired tombstones",
|
||||
Use: "cleanup",
|
||||
GroupID: "maint",
|
||||
Short: "Delete closed issues and prune expired tombstones",
|
||||
Long: `Delete closed issues and prune expired tombstones to reduce database size.
|
||||
|
||||
This command:
|
||||
@@ -82,7 +84,7 @@ SEE ALSO:
|
||||
customTTL = -1
|
||||
}
|
||||
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)
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -235,13 +237,12 @@ SEE ALSO:
|
||||
}
|
||||
} else if tombstoneResult != nil && tombstoneResult.PrunedCount > 0 {
|
||||
if !jsonOutput {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
ttlMsg := fmt.Sprintf("older than %d days", tombstoneResult.TTLDays)
|
||||
if hardDelete && olderThanDays == 0 {
|
||||
ttlMsg = "all tombstones (--hard mode)"
|
||||
}
|
||||
fmt.Printf("\n%s Pruned %d expired tombstone(s) (%s)\n",
|
||||
green("✓"), tombstoneResult.PrunedCount, ttlMsg)
|
||||
ui.RenderPass("✓"), tombstoneResult.PrunedCount, ttlMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/debug"
|
||||
@@ -14,11 +13,13 @@ import (
|
||||
"github.com/steveyegge/beads/internal/routing"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/validation"
|
||||
)
|
||||
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create [title]",
|
||||
GroupID: "issues",
|
||||
Aliases: []string{"new"},
|
||||
Short: "Create a new issue (or multiple issues from markdown file)",
|
||||
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)
|
||||
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", yellow("⚠"))
|
||||
fmt.Fprintf(os.Stderr, "%s Creating issue with 'Test' prefix in production database.\n", ui.RenderWarn("⚠"))
|
||||
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)
|
||||
if !silent && !debug.IsQuiet() {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Fprintf(os.Stderr, "%s Creating issue without description.\n", yellow("⚠"))
|
||||
fmt.Fprintf(os.Stderr, "%s Creating issue without description.\n", ui.RenderWarn("⚠"))
|
||||
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")
|
||||
}
|
||||
@@ -241,8 +240,7 @@ var createCmd = &cobra.Command{
|
||||
} else if silent {
|
||||
fmt.Println(issue.ID)
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Created issue: %s\n", green("✓"), issue.ID)
|
||||
fmt.Printf("%s Created issue: %s\n", ui.RenderPass("✓"), issue.ID)
|
||||
fmt.Printf(" Title: %s\n", issue.Title)
|
||||
fmt.Printf(" Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf(" Status: %s\n", issue.Status)
|
||||
@@ -381,8 +379,7 @@ var createCmd = &cobra.Command{
|
||||
} else if silent {
|
||||
fmt.Println(issue.ID)
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Created issue: %s\n", green("✓"), issue.ID)
|
||||
fmt.Printf("%s Created issue: %s\n", ui.RenderPass("✓"), issue.ID)
|
||||
fmt.Printf(" Title: %s\n", issue.Title)
|
||||
fmt.Printf(" Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf(" Status: %s\n", issue.Status)
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/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{
|
||||
Use: "create-form",
|
||||
Short: "Create a new issue using an interactive form",
|
||||
Use: "create-form",
|
||||
GroupID: "issues",
|
||||
Short: "Create a new issue using an interactive form",
|
||||
Long: `Create a new issue using an interactive terminal form.
|
||||
|
||||
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) {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s Created issue: %s\n", green("✓"), issue.ID)
|
||||
fmt.Printf("\n%s Created issue: %s\n", ui.RenderPass("✓"), issue.ID)
|
||||
fmt.Printf(" Title: %s\n", issue.Title)
|
||||
fmt.Printf(" Type: %s\n", issue.IssueType)
|
||||
fmt.Printf(" Priority: P%d\n", issue.Priority)
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// 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))
|
||||
totalCount := int(result["total_count"].(float64))
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
|
||||
if deletedCount > 0 {
|
||||
if deletedCount == 1 {
|
||||
fmt.Printf("%s Deleted %s\n", green("✓"), issueIDs[0])
|
||||
fmt.Printf("%s Deleted %s\n", ui.RenderPass("✓"), issueIDs[0])
|
||||
} 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 {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s Warnings:\n", yellow("⚠"))
|
||||
fmt.Printf("\n%s Warnings:\n", ui.RenderWarn("⚠"))
|
||||
for _, e := range errors {
|
||||
fmt.Printf(" %s\n", e)
|
||||
}
|
||||
@@ -85,8 +83,9 @@ func deleteViaDaemon(issueIDs []string, force, dryRun, cascade bool, jsonOutput
|
||||
}
|
||||
|
||||
var deleteCmd = &cobra.Command{
|
||||
Use: "delete <issue-id> [issue-id...]",
|
||||
Short: "Delete one or more issues and clean up references",
|
||||
Use: "delete <issue-id> [issue-id...]",
|
||||
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.
|
||||
This command will:
|
||||
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`
|
||||
// Preview mode
|
||||
if !force {
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s\n", red("⚠️ DELETE PREVIEW"))
|
||||
fmt.Printf("\n%s\n", ui.RenderFail("⚠️ DELETE PREVIEW"))
|
||||
fmt.Printf("\nIssue to delete:\n")
|
||||
fmt.Printf(" %s: %s\n", issueID, issue.Title)
|
||||
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("\n%s\n", yellow("This operation cannot be undone!"))
|
||||
fmt.Printf("To proceed, run: %s\n\n", yellow("bd delete "+issueID+" --force"))
|
||||
fmt.Printf("\n%s\n", ui.RenderWarn("This operation cannot be undone!"))
|
||||
fmt.Printf("To proceed, run: %s\n\n", ui.RenderWarn("bd delete "+issueID+" --force"))
|
||||
return
|
||||
}
|
||||
// Actually delete
|
||||
@@ -323,8 +320,7 @@ the issues will not resurrect from remote branches.`,
|
||||
"references_updated": updatedIssueCount,
|
||||
})
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Deleted %s\n", green("✓"), issueID)
|
||||
fmt.Printf("%s Deleted %s\n", ui.RenderPass("✓"), issueID)
|
||||
fmt.Printf(" Removed %d dependency link(s)\n", totalDepsRemoved)
|
||||
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 {
|
||||
fmt.Printf("\n(Dry-run mode - no changes made)\n")
|
||||
} else {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s\n", yellow("This operation cannot be undone!"))
|
||||
fmt.Printf("\n%s\n", ui.RenderWarn("This operation cannot be undone!"))
|
||||
if cascade {
|
||||
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 {
|
||||
fmt.Printf("To proceed, run: %s\n",
|
||||
yellow("bd delete "+strings.Join(issueIDs, " ")+" --force"))
|
||||
ui.RenderWarn("bd delete "+strings.Join(issueIDs, " ")+" --force"))
|
||||
}
|
||||
}
|
||||
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.
|
||||
if hardDelete {
|
||||
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.")
|
||||
}
|
||||
// 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,
|
||||
})
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Deleted %d issue(s)\n", green("✓"), result.DeletedCount)
|
||||
fmt.Printf("%s Deleted %d issue(s)\n", ui.RenderPass("✓"), result.DeletedCount)
|
||||
fmt.Printf(" Removed %d dependency link(s)\n", result.DependenciesCount)
|
||||
fmt.Printf(" Removed %d label(s)\n", result.LabelsCount)
|
||||
fmt.Printf(" Removed %d event(s)\n", result.EventsCount)
|
||||
fmt.Printf(" Updated text references in %d issue(s)\n", updatedCount)
|
||||
if len(result.OrphanedIssues) > 0 {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
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
|
||||
func showDeletionPreview(issueIDs []string, issues map[string]*types.Issue, cascade bool, depError error) {
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s\n", red("⚠️ DELETE PREVIEW"))
|
||||
fmt.Printf("\n%s\n", ui.RenderFail("⚠️ DELETE PREVIEW"))
|
||||
fmt.Printf("\nIssues to delete (%d):\n", len(issueIDs))
|
||||
for _, id := range issueIDs {
|
||||
if issue := issues[id]; issue != nil {
|
||||
@@ -580,10 +571,10 @@ func showDeletionPreview(issueIDs []string, issues map[string]*types.Issue, casc
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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
|
||||
|
||||
@@ -7,17 +7,18 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var depCmd = &cobra.Command{
|
||||
Use: "dep",
|
||||
Short: "Manage dependencies",
|
||||
Use: "dep",
|
||||
GroupID: "deps",
|
||||
Short: "Manage dependencies",
|
||||
}
|
||||
|
||||
var depAddCmd = &cobra.Command{
|
||||
@@ -88,9 +89,8 @@ var depAddCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -114,8 +114,7 @@ var depAddCmd = &cobra.Command{
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to check for cycles: %v\n", err)
|
||||
} else if len(cycles) > 0 {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Fprintf(os.Stderr, "\n%s Warning: Dependency cycle detected!\n", yellow("⚠"))
|
||||
fmt.Fprintf(os.Stderr, "\n%s Warning: Dependency cycle detected!\n", ui.RenderWarn("⚠"))
|
||||
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")
|
||||
for _, cycle := range cycles {
|
||||
@@ -144,9 +143,8 @@ var depAddCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
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
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Removed dependency: %s no longer depends on %s\n",
|
||||
green("✓"), fromID, toID)
|
||||
ui.RenderPass("✓"), fromID, toID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -242,9 +239,8 @@ var depRemoveCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
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
|
||||
}
|
||||
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
switch direction {
|
||||
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":
|
||||
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:
|
||||
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
|
||||
@@ -436,13 +431,11 @@ var depCyclesCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if len(cycles) == 0 {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s No dependency cycles detected\n\n", green("✓"))
|
||||
fmt.Printf("\n%s No dependency cycles detected\n\n", ui.RenderPass("✓"))
|
||||
return
|
||||
}
|
||||
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
fmt.Printf("\n%s Found %d dependency cycles:\n\n", red("⚠"), len(cycles))
|
||||
fmt.Printf("\n%s Found %d dependency cycles:\n\n", ui.RenderFail("⚠"), len(cycles))
|
||||
for i, cycle := range cycles {
|
||||
fmt.Printf("%d. Cycle involving:\n", i+1)
|
||||
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)
|
||||
if r.seen[node.ID] {
|
||||
gray := color.New(color.FgHiBlack).SprintFunc()
|
||||
fmt.Printf("%s%s (shown above)\n", prefix.String(), gray(node.ID))
|
||||
fmt.Printf("%s%s (shown above)\n", prefix.String(), ui.RenderMuted(node.ID))
|
||||
return
|
||||
}
|
||||
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
|
||||
if node.Truncated || (depth == r.maxDepth && len(children[node.ID]) > 0) {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
line += yellow(" …")
|
||||
line += ui.RenderWarn(" …")
|
||||
}
|
||||
|
||||
fmt.Printf("%s%s\n", prefix.String(), line)
|
||||
@@ -613,13 +604,13 @@ func formatTreeNode(node *types.TreeNode) string {
|
||||
var idStr string
|
||||
switch node.Status {
|
||||
case types.StatusOpen:
|
||||
idStr = color.New(color.FgWhite).Sprint(node.ID)
|
||||
idStr = ui.StatusOpenStyle.Render(node.ID)
|
||||
case types.StatusInProgress:
|
||||
idStr = color.New(color.FgYellow).Sprint(node.ID)
|
||||
idStr = ui.StatusInProgressStyle.Render(node.ID)
|
||||
case types.StatusBlocked:
|
||||
idStr = color.New(color.FgRed).Sprint(node.ID)
|
||||
idStr = ui.StatusBlockedStyle.Render(node.ID)
|
||||
case types.StatusClosed:
|
||||
idStr = color.New(color.FgGreen).Sprint(node.ID)
|
||||
idStr = ui.StatusClosedStyle.Render(node.ID)
|
||||
default:
|
||||
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
|
||||
// (In the tree view, depth 0 with status open implies ready in the "down" direction)
|
||||
if node.Status == types.StatusOpen && node.Depth == 0 {
|
||||
green := color.New(color.FgGreen, color.Bold).SprintFunc()
|
||||
line += " " + green("[READY]")
|
||||
line += " " + ui.PassStyle.Bold(true).Render("[READY]")
|
||||
}
|
||||
|
||||
return line
|
||||
|
||||
@@ -7,14 +7,16 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"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{
|
||||
Use: "detect-pollution",
|
||||
Short: "Detect and optionally clean test issues from database",
|
||||
Use: "detect-pollution",
|
||||
GroupID: "maint",
|
||||
Short: "Detect and optionally clean test issues from database",
|
||||
Long: `Detect test issues that leaked into production database using pattern matching.
|
||||
|
||||
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
|
||||
markDirtyAndScheduleFlush()
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Deleted %d test issues\n", green("✓"), deleted)
|
||||
fmt.Printf("%s Deleted %d test issues\n", ui.RenderPass("✓"), deleted)
|
||||
fmt.Printf("\nCleanup complete. To restore, run: bd import %s\n", backupPath)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -64,8 +63,9 @@ const ConfigKeyHintsDoctor = "hints.doctor"
|
||||
const minSyncBranchHookVersion = "0.29.0"
|
||||
|
||||
var doctorCmd = &cobra.Command{
|
||||
Use: "doctor [path]",
|
||||
Short: "Check beads installation health",
|
||||
Use: "doctor [path]",
|
||||
GroupID: "maint",
|
||||
Short: "Check and fix beads installation health (start here)",
|
||||
Long: `Sanity check the beads installation for the current directory or specified path.
|
||||
|
||||
This command checks:
|
||||
@@ -205,9 +205,9 @@ func previewFixes(result doctorResult) {
|
||||
// Show the issue details
|
||||
fmt.Printf(" %d. %s\n", i+1, issue.Name)
|
||||
if issue.Status == statusError {
|
||||
color.Red(" Status: ERROR\n")
|
||||
fmt.Printf(" Status: %s\n", ui.RenderFail("ERROR"))
|
||||
} else {
|
||||
color.Yellow(" Status: WARNING\n")
|
||||
fmt.Printf(" Status: %s\n", ui.RenderWarn("WARNING"))
|
||||
}
|
||||
fmt.Printf(" Issue: %s\n", issue.Message)
|
||||
if issue.Detail != "" {
|
||||
@@ -286,9 +286,9 @@ func applyFixesInteractive(path string, issues []doctorCheck) {
|
||||
// Show issue details
|
||||
fmt.Printf("(%d/%d) %s\n", i+1, len(issues), issue.Name)
|
||||
if issue.Status == statusError {
|
||||
color.Red(" Status: ERROR\n")
|
||||
fmt.Printf(" Status: %s\n", ui.RenderFail("ERROR"))
|
||||
} else {
|
||||
color.Yellow(" Status: WARNING\n")
|
||||
fmt.Printf(" Status: %s\n", ui.RenderWarn("WARNING"))
|
||||
}
|
||||
fmt.Printf(" Issue: %s\n", issue.Message)
|
||||
if issue.Detail != "" {
|
||||
@@ -401,11 +401,11 @@ func applyFixList(path string, fixes []doctorCheck) {
|
||||
|
||||
if err != nil {
|
||||
errorCount++
|
||||
color.Red(" ✗ Error: %v\n", err)
|
||||
fmt.Printf(" %s Error: %v\n", ui.RenderFail("✗"), err)
|
||||
fmt.Printf(" Manual fix: %s\n", check.Fix)
|
||||
} else {
|
||||
fixedCount++
|
||||
color.Green(" ✓ Fixed\n")
|
||||
fmt.Printf(" %s Fixed\n", ui.RenderPass("✓"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,7 +886,7 @@ func printDiagnostics(result doctorResult) {
|
||||
}
|
||||
} else {
|
||||
fmt.Println()
|
||||
color.Green("✓ All checks passed\n")
|
||||
fmt.Printf("%s\n", ui.RenderPass("✓ All checks passed"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var duplicateCmd = &cobra.Command{
|
||||
Use: "duplicate <id> --of <canonical>",
|
||||
Short: "Mark an issue as a duplicate of another",
|
||||
Use: "duplicate <id> --of <canonical>",
|
||||
GroupID: "deps",
|
||||
Short: "Mark an issue as a duplicate of another",
|
||||
Long: `Mark an issue as a duplicate of a canonical issue.
|
||||
|
||||
The duplicate issue is automatically closed with a reference to the canonical.
|
||||
@@ -27,8 +28,9 @@ Examples:
|
||||
}
|
||||
|
||||
var supersedeCmd = &cobra.Command{
|
||||
Use: "supersede <id> --with <new>",
|
||||
Short: "Mark an issue as superseded by a newer one",
|
||||
Use: "supersede <id> --with <new>",
|
||||
GroupID: "deps",
|
||||
Short: "Mark an issue as superseded by a newer one",
|
||||
Long: `Mark an issue as superseded by a newer version.
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Marked %s as duplicate of %s (closed)\n", green("✓"), duplicateID, canonicalID)
|
||||
fmt.Printf("%s Marked %s as duplicate of %s (closed)\n", ui.RenderPass("✓"), duplicateID, canonicalID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -248,7 +249,6 @@ func runSupersede(cmd *cobra.Command, args []string) error {
|
||||
return encoder.Encode(result)
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Marked %s as superseded by %s (closed)\n", green("✓"), oldID, newID)
|
||||
fmt.Printf("%s Marked %s as superseded by %s (closed)\n", ui.RenderPass("✓"), oldID, newID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
var duplicatesCmd = &cobra.Command{
|
||||
Use: "duplicates",
|
||||
Short: "Find and optionally merge duplicate issues",
|
||||
Use: "duplicates",
|
||||
GroupID: "deps",
|
||||
Short: "Find and optionally merge duplicate issues",
|
||||
Long: `Find issues with identical content (title, description, design, acceptance criteria).
|
||||
Groups issues by content hash and reports duplicates with suggested merge targets.
|
||||
The merge target is chosen by:
|
||||
@@ -119,18 +120,15 @@ Example:
|
||||
}
|
||||
outputJSON(output)
|
||||
} else {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
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))
|
||||
fmt.Printf("%s Found %d duplicate group(s):\n\n", ui.RenderWarn("🔍"), len(duplicateGroups))
|
||||
for i, group := range duplicateGroups {
|
||||
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 {
|
||||
refs := refCounts[issue.ID]
|
||||
marker := " "
|
||||
if issue.ID == target.ID {
|
||||
marker = green("→ ")
|
||||
marker = ui.RenderPass("→ ")
|
||||
}
|
||||
fmt.Printf("%s%s (%s, P%d, %d references)\n",
|
||||
marker, issue.ID, issue.Status, issue.Priority, refs)
|
||||
@@ -141,18 +139,18 @@ Example:
|
||||
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",
|
||||
cyan("Suggested:"), strings.Join(sources, " "), strings.Join(sources, " "), target.ID)
|
||||
ui.RenderAccent("Suggested:"), strings.Join(sources, " "), strings.Join(sources, " "), target.ID)
|
||||
}
|
||||
if autoMerge {
|
||||
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 {
|
||||
fmt.Printf("%s Merged %d group(s)\n", green("✓"), len(mergeCommands))
|
||||
fmt.Printf("%s Merged %d group(s)\n", ui.RenderPass("✓"), len(mergeCommands))
|
||||
}
|
||||
} 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("💡"))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,14 +3,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
var epicCmd = &cobra.Command{
|
||||
Use: "epic",
|
||||
Short: "Epic management commands",
|
||||
Use: "epic",
|
||||
GroupID: "deps",
|
||||
Short: "Epic management commands",
|
||||
}
|
||||
var epicStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
@@ -67,10 +68,6 @@ var epicStatusCmd = &cobra.Command{
|
||||
fmt.Println("No open epics found")
|
||||
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 {
|
||||
epic := epicStatus.Epic
|
||||
percentage := 0
|
||||
@@ -79,17 +76,17 @@ var epicStatusCmd = &cobra.Command{
|
||||
}
|
||||
statusIcon := ""
|
||||
if epicStatus.EligibleForClose {
|
||||
statusIcon = green("✓")
|
||||
statusIcon = ui.RenderPass("✓")
|
||||
} else if percentage > 0 {
|
||||
statusIcon = yellow("○")
|
||||
statusIcon = ui.RenderWarn("○")
|
||||
} else {
|
||||
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",
|
||||
epicStatus.ClosedChildren, epicStatus.TotalChildren, percentage)
|
||||
if epicStatus.EligibleForClose {
|
||||
fmt.Printf(" %s\n", green("Eligible for closure"))
|
||||
fmt.Printf(" %s\n", ui.RenderPass("Eligible for closure"))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
@@ -33,8 +33,9 @@ type GraphLayout struct {
|
||||
}
|
||||
|
||||
var graphCmd = &cobra.Command{
|
||||
Use: "graph <issue-id>",
|
||||
Short: "Display issue dependency graph",
|
||||
Use: "graph <issue-id>",
|
||||
GroupID: "deps",
|
||||
Short: "Display issue dependency graph",
|
||||
Long: `Display an ASCII visualization of an issue's dependency graph.
|
||||
|
||||
For epics, shows all children and their dependencies.
|
||||
@@ -283,8 +284,7 @@ func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
|
||||
return
|
||||
}
|
||||
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s Dependency graph for %s:\n\n", cyan("📊"), layout.RootID)
|
||||
fmt.Printf("\n%s Dependency graph for %s:\n\n", ui.RenderAccent("📊"), layout.RootID)
|
||||
|
||||
// Calculate box width based on longest title
|
||||
maxTitleLen := 0
|
||||
@@ -370,33 +370,34 @@ func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
|
||||
func renderNodeBox(node *GraphNode, width int) string {
|
||||
// Status indicator
|
||||
var statusIcon string
|
||||
var colorFn func(a ...interface{}) string
|
||||
var titleStr string
|
||||
|
||||
title := truncateTitle(node.Issue.Title, width-4)
|
||||
|
||||
switch node.Issue.Status {
|
||||
case types.StatusOpen:
|
||||
statusIcon = "○"
|
||||
colorFn = color.New(color.FgWhite).SprintFunc()
|
||||
titleStr = padRight(title, width-4)
|
||||
case types.StatusInProgress:
|
||||
statusIcon = "◐"
|
||||
colorFn = color.New(color.FgYellow).SprintFunc()
|
||||
titleStr = ui.RenderWarn(padRight(title, width-4))
|
||||
case types.StatusBlocked:
|
||||
statusIcon = "●"
|
||||
colorFn = color.New(color.FgRed).SprintFunc()
|
||||
titleStr = ui.RenderFail(padRight(title, width-4))
|
||||
case types.StatusClosed:
|
||||
statusIcon = "✓"
|
||||
colorFn = color.New(color.FgGreen).SprintFunc()
|
||||
titleStr = ui.RenderPass(padRight(title, width-4))
|
||||
default:
|
||||
statusIcon = "?"
|
||||
colorFn = color.New(color.FgWhite).SprintFunc()
|
||||
titleStr = padRight(title, width-4)
|
||||
}
|
||||
|
||||
title := truncateTitle(node.Issue.Title, width-4)
|
||||
id := node.Issue.ID
|
||||
|
||||
// Build the box
|
||||
topBottom := " ┌" + strings.Repeat("─", width) + "┐"
|
||||
middle := fmt.Sprintf(" │ %s %s │", statusIcon, colorFn(padRight(title, width-4)))
|
||||
idLine := fmt.Sprintf(" │ %s │", color.New(color.FgHiBlack).Sprint(padRight(id, width-2)))
|
||||
middle := fmt.Sprintf(" │ %s %s │", statusIcon, titleStr)
|
||||
idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
|
||||
bottom := " └" + strings.Repeat("─", width) + "┘"
|
||||
|
||||
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 {
|
||||
// Status indicator
|
||||
var statusIcon string
|
||||
var colorFn func(a ...interface{}) string
|
||||
var titleStr string
|
||||
|
||||
title := truncateTitle(node.Issue.Title, width-4)
|
||||
|
||||
switch node.Issue.Status {
|
||||
case types.StatusOpen:
|
||||
statusIcon = "○"
|
||||
colorFn = color.New(color.FgWhite).SprintFunc()
|
||||
titleStr = padRight(title, width-4)
|
||||
case types.StatusInProgress:
|
||||
statusIcon = "◐"
|
||||
colorFn = color.New(color.FgYellow).SprintFunc()
|
||||
titleStr = ui.RenderWarn(padRight(title, width-4))
|
||||
case types.StatusBlocked:
|
||||
statusIcon = "●"
|
||||
colorFn = color.New(color.FgRed).SprintFunc()
|
||||
titleStr = ui.RenderFail(padRight(title, width-4))
|
||||
case types.StatusClosed:
|
||||
statusIcon = "✓"
|
||||
colorFn = color.New(color.FgGreen).SprintFunc()
|
||||
titleStr = ui.RenderPass(padRight(title, width-4))
|
||||
default:
|
||||
statusIcon = "?"
|
||||
colorFn = color.New(color.FgWhite).SprintFunc()
|
||||
titleStr = padRight(title, width-4)
|
||||
}
|
||||
|
||||
title := truncateTitle(node.Issue.Title, width-4)
|
||||
id := node.Issue.ID
|
||||
|
||||
// Build dependency info string
|
||||
@@ -484,12 +486,12 @@ func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedB
|
||||
|
||||
// Build the box
|
||||
topBottom := " ┌" + strings.Repeat("─", width) + "┐"
|
||||
middle := fmt.Sprintf(" │ %s %s │", statusIcon, colorFn(padRight(title, width-4)))
|
||||
idLine := fmt.Sprintf(" │ %s │", color.New(color.FgHiBlack).Sprint(padRight(id, width-2)))
|
||||
middle := fmt.Sprintf(" │ %s %s │", statusIcon, titleStr)
|
||||
idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
|
||||
|
||||
var result string
|
||||
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) + "┘"
|
||||
result = topBottom + "\n" + middle + "\n" + idLine + "\n" + depLine + "\n" + bottom
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/cmd/bd/doctor"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
@@ -21,12 +20,14 @@ import (
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/syncbranch"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize bd in the current directory",
|
||||
Use: "init",
|
||||
GroupID: "setup",
|
||||
Short: "Initialize bd in the current directory",
|
||||
Long: `Initialize bd in the current directory by creating a .beads/ directory
|
||||
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 {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
fmt.Printf("\n%s bd initialized successfully in --no-db mode!\n\n", green("✓"))
|
||||
fmt.Printf(" Mode: %s\n", cyan("no-db (JSONL-only)"))
|
||||
fmt.Printf(" Issues file: %s\n", cyan(jsonlPath))
|
||||
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"))
|
||||
fmt.Printf("\n%s bd initialized successfully in --no-db mode!\n\n", ui.RenderPass("✓"))
|
||||
fmt.Printf(" Mode: %s\n", ui.RenderAccent("no-db (JSONL-only)"))
|
||||
fmt.Printf(" Issues file: %s\n", ui.RenderAccent(jsonlPath))
|
||||
fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
|
||||
fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
|
||||
fmt.Printf("Run %s to get started.\n\n", ui.RenderAccent("bd --no-db quickstart"))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -427,9 +425,8 @@ With --stealth: configures global git settings for invisible beads usage:
|
||||
// Install by default unless --skip-hooks is passed
|
||||
if !skipHooks && isGitRepo() && !hooksInstalled() {
|
||||
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", yellow("⚠"), err)
|
||||
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", color.New(color.FgCyan).Sprint("bd doctor --fix"))
|
||||
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", ui.RenderWarn("⚠"), err)
|
||||
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("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
|
||||
if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() {
|
||||
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", yellow("⚠"), err)
|
||||
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", color.New(color.FgCyan).Sprint("bd doctor --fix"))
|
||||
fmt.Fprintf(os.Stderr, "\n%s Failed to install merge driver: %v\n", ui.RenderWarn("⚠"), err)
|
||||
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,14 +450,11 @@ With --stealth: configures global git settings for invisible beads usage:
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
fmt.Printf("\n%s bd initialized successfully!\n\n", green("✓"))
|
||||
fmt.Printf(" Database: %s\n", cyan(initDBPath))
|
||||
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"))
|
||||
fmt.Printf("\n%s bd initialized successfully!\n\n", ui.RenderPass("✓"))
|
||||
fmt.Printf(" Database: %s\n", ui.RenderAccent(initDBPath))
|
||||
fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
|
||||
fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
|
||||
fmt.Printf("Run %s to get started.\n\n", ui.RenderAccent("bd quickstart"))
|
||||
|
||||
// Run bd doctor diagnostics to catch setup issues early (bd-zwtq)
|
||||
doctorResult := runDiagnostics(cwd)
|
||||
@@ -474,15 +467,14 @@ With --stealth: configures global git settings for invisible beads usage:
|
||||
}
|
||||
}
|
||||
if hasIssues {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("%s Setup incomplete. Some issues were detected:\n", yellow("⚠"))
|
||||
fmt.Printf("%s Setup incomplete. Some issues were detected:\n", ui.RenderWarn("⚠"))
|
||||
// Show just the warnings/errors, not all checks
|
||||
for _, check := range doctorResult.Checks {
|
||||
if check.Status != statusOK {
|
||||
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
|
||||
func promptHookAction(existingHooks []hookInfo) string {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
|
||||
fmt.Printf("\n%s Found existing git hooks:\n", yellow("⚠"))
|
||||
fmt.Printf("\n%s Found existing git hooks:\n", ui.RenderWarn("⚠"))
|
||||
for _, hook := range existingHooks {
|
||||
if hook.exists && !hook.isBdHook {
|
||||
hookType := "custom script"
|
||||
@@ -646,7 +636,6 @@ func installGitHooks() error {
|
||||
// Determine installation mode
|
||||
chainHooks := false
|
||||
if hasExistingHooks {
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
choice := promptHookAction(existingHooks)
|
||||
switch choice {
|
||||
case "1", "":
|
||||
@@ -665,7 +654,7 @@ func installGitHooks() error {
|
||||
}
|
||||
case "3":
|
||||
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
|
||||
default:
|
||||
return fmt.Errorf("invalid choice: %s", choice)
|
||||
@@ -971,8 +960,7 @@ exit 0
|
||||
}
|
||||
|
||||
if chainHooks {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Chained bd hooks with existing hooks\n", green("✓"))
|
||||
fmt.Printf("%s Chained bd hooks with existing hooks\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1400,12 +1388,10 @@ func setupStealthMode(verbose bool) error {
|
||||
}
|
||||
|
||||
if verbose {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s Stealth mode configured successfully!\n\n", green("✓"))
|
||||
fmt.Printf(" Global gitignore: %s\n", cyan(projectPath+"/.beads/ ignored"))
|
||||
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"))
|
||||
fmt.Printf("\n%s Stealth mode configured successfully!\n\n", ui.RenderPass("✓"))
|
||||
fmt.Printf(" Global gitignore: %s\n", ui.RenderAccent(projectPath+"/.beads/ ignored"))
|
||||
fmt.Printf(" Claude settings: %s\n\n", ui.RenderAccent("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", ui.RenderAccent("invisible"))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1550,9 +1536,6 @@ func checkExistingBeadsData(prefix string) error {
|
||||
// Check for existing database file
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
if _, err := os.Stat(dbPath); err == nil {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
return fmt.Errorf(`
|
||||
%s Found existing database: %s
|
||||
|
||||
@@ -1564,7 +1547,7 @@ To use the existing database:
|
||||
To completely reinitialize (data loss warning):
|
||||
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
|
||||
@@ -1646,8 +1629,7 @@ bd sync # Sync with git
|
||||
return fmt.Errorf("failed to create %s: %w", filename, err)
|
||||
}
|
||||
if verbose {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf(" %s Created %s with landing-the-plane instructions\n", green("✓"), filename)
|
||||
fmt.Printf(" %s Created %s with landing-the-plane instructions\n", ui.RenderPass("✓"), filename)
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1674,8 +1656,7 @@ bd sync # Sync with git
|
||||
return fmt.Errorf("failed to update %s: %w", filename, err)
|
||||
}
|
||||
if verbose {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf(" %s Added landing-the-plane instructions to %s\n", green("✓"), filename)
|
||||
fmt.Printf(" %s Added landing-the-plane instructions to %s\n", ui.RenderPass("✓"), filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,30 +9,25 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// runContributorWizard guides the user through OSS contributor setup
|
||||
func runContributorWizard(ctx context.Context, store storage.Storage) error {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
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.Printf("\n%s %s\n\n", ui.RenderBold("bd"), ui.RenderBold("Contributor Workflow Setup Wizard"))
|
||||
fmt.Println("This wizard will configure beads for OSS contribution.")
|
||||
fmt.Println()
|
||||
|
||||
// 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()
|
||||
|
||||
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 {
|
||||
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(" git remote add upstream <original-repo-url>")
|
||||
fmt.Println()
|
||||
@@ -50,13 +45,13 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
if hasPushAccess {
|
||||
fmt.Printf("%s You have push access to origin (%s)\n", green("✓"), originURL)
|
||||
fmt.Printf(" %s You can commit directly to this repository.\n", yellow("⚠"))
|
||||
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", ui.RenderWarn("⚠"))
|
||||
fmt.Println()
|
||||
fmt.Print("Do you want to use a separate planning repo anyway? [Y/n]: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
@@ -68,12 +63,12 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
|
||||
return nil
|
||||
}
|
||||
} 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.")
|
||||
}
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
@@ -83,7 +78,7 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
|
||||
defaultPlanningRepo := filepath.Join(homeDir, ".beads-planning")
|
||||
|
||||
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]: ")
|
||||
|
||||
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
|
||||
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 {
|
||||
return fmt.Errorf("failed to create planning repo directory: %w", err)
|
||||
@@ -159,13 +154,13 @@ Created by: bd init --contributor
|
||||
cmd.Dir = planningPath
|
||||
_ = cmd.Run()
|
||||
|
||||
fmt.Printf("%s Planning repository created\n", green("✓"))
|
||||
fmt.Printf("%s Planning repository created\n", ui.RenderPass("✓"))
|
||||
} 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
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
// 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 {
|
||||
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
|
||||
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.Printf(" Current repo issues: %s\n", cyan(".beads/issues.jsonl"))
|
||||
fmt.Printf(" Planning repo issues: %s\n", cyan(filepath.Join(planningPath, ".beads/issues.jsonl")))
|
||||
fmt.Printf(" Current repo issues: %s\n", ui.RenderAccent(".beads/issues.jsonl"))
|
||||
fmt.Printf(" Planning repo issues: %s\n", ui.RenderAccent(filepath.Join(planningPath, ".beads/issues.jsonl")))
|
||||
fmt.Println()
|
||||
fmt.Println("How it works:")
|
||||
fmt.Println(" • Issues you create will route to the planning repo")
|
||||
fmt.Println(" • Planning stays out of your PRs to upstream")
|
||||
fmt.Println(" • Use 'bd list' to see issues from both repos")
|
||||
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()
|
||||
|
||||
return nil
|
||||
|
||||
@@ -8,172 +8,167 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// runTeamWizard guides the user through team workflow setup
|
||||
func runTeamWizard(ctx context.Context, store storage.Storage) error {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
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.Printf("\n%s %s\n\n", ui.RenderBold("bd"), ui.RenderBold("Team Workflow Setup Wizard"))
|
||||
fmt.Println("This wizard will configure beads for team collaboration.")
|
||||
fmt.Println()
|
||||
|
||||
// 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() {
|
||||
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(" git init")
|
||||
fmt.Println()
|
||||
return fmt.Errorf("not in a git repository")
|
||||
}
|
||||
|
||||
|
||||
// Get current branch
|
||||
currentBranch, err := getGitBranch()
|
||||
if err != nil {
|
||||
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
|
||||
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(" GitHub: Settings → Branches → Branch protection rules")
|
||||
fmt.Println(" GitLab: Settings → Repository → Protected branches")
|
||||
fmt.Print("\nProtected main branch? [y/N]: ")
|
||||
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
|
||||
|
||||
protectedMain := (response == "y" || response == "yes")
|
||||
|
||||
var syncBranch string
|
||||
|
||||
|
||||
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.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]: ")
|
||||
|
||||
|
||||
branchName, _ := reader.ReadString('\n')
|
||||
branchName = strings.TrimSpace(branchName)
|
||||
|
||||
|
||||
if branchName == "" {
|
||||
syncBranch = "beads-metadata"
|
||||
} else {
|
||||
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
|
||||
if err := store.SetConfig(ctx, "sync.branch", syncBranch); err != nil {
|
||||
return fmt.Errorf("failed to set sync branch: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 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 {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to create sync branch: %v\n", err)
|
||||
fmt.Println(" You can create it manually: git checkout -b", syncBranch)
|
||||
} else {
|
||||
fmt.Printf("%s Sync branch created\n", green("✓"))
|
||||
fmt.Printf("%s Sync branch created\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
fmt.Printf("%s Direct commits to %s\n", green("✓"), currentBranch)
|
||||
fmt.Printf("%s Direct commits to %s\n", ui.RenderPass("✓"), currentBranch)
|
||||
syncBranch = currentBranch
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := store.SetConfig(ctx, "team.enabled", "true"); err != nil {
|
||||
return fmt.Errorf("failed to enable team mode: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Set team.sync_branch
|
||||
if err := store.SetConfig(ctx, "team.sync_branch", syncBranch); err != nil {
|
||||
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
|
||||
fmt.Println("\n Enable automatic sync (daemon commits/pushes)?")
|
||||
fmt.Println(" • Auto-commit: Commits issue changes every 5 seconds")
|
||||
fmt.Println(" • Auto-push: Pushes commits to remote")
|
||||
fmt.Print("\nEnable auto-sync? [Y/n]: ")
|
||||
|
||||
|
||||
response, _ = reader.ReadString('\n')
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
|
||||
|
||||
autoSync := !(response == "n" || response == "no")
|
||||
|
||||
|
||||
if autoSync {
|
||||
if err := store.SetConfig(ctx, "daemon.auto_commit", "true"); err != nil {
|
||||
return fmt.Errorf("failed to enable auto-commit: %w", err)
|
||||
}
|
||||
|
||||
|
||||
if err := store.SetConfig(ctx, "daemon.auto_push", "true"); err != nil {
|
||||
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 {
|
||||
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
|
||||
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:")
|
||||
if protectedMain {
|
||||
fmt.Printf(" Protected main: %s\n", cyan("yes"))
|
||||
fmt.Printf(" Sync branch: %s\n", cyan(syncBranch))
|
||||
fmt.Printf(" Commits will go to: %s\n", cyan(syncBranch))
|
||||
fmt.Printf(" Merge to main via: %s\n", cyan("Pull Request"))
|
||||
fmt.Printf(" Protected main: %s\n", ui.RenderAccent("yes"))
|
||||
fmt.Printf(" Sync branch: %s\n", ui.RenderAccent(syncBranch))
|
||||
fmt.Printf(" Commits will go to: %s\n", ui.RenderAccent(syncBranch))
|
||||
fmt.Printf(" Merge to main via: %s\n", ui.RenderAccent("Pull Request"))
|
||||
} else {
|
||||
fmt.Printf(" Protected main: %s\n", cyan("no"))
|
||||
fmt.Printf(" Commits will go to: %s\n", cyan(currentBranch))
|
||||
fmt.Printf(" Protected main: %s\n", ui.RenderAccent("no"))
|
||||
fmt.Printf(" Commits will go to: %s\n", ui.RenderAccent(currentBranch))
|
||||
}
|
||||
|
||||
|
||||
if autoSync {
|
||||
fmt.Printf(" Auto-sync: %s\n", cyan("enabled"))
|
||||
fmt.Printf(" Auto-sync: %s\n", ui.RenderAccent("enabled"))
|
||||
} else {
|
||||
fmt.Printf(" Auto-sync: %s\n", cyan("disabled"))
|
||||
fmt.Printf(" Auto-sync: %s\n", ui.RenderAccent("disabled"))
|
||||
}
|
||||
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("How it works:")
|
||||
fmt.Println(" • All team members work on the same repository")
|
||||
fmt.Println(" • Issues are shared via git commits")
|
||||
fmt.Println(" • Use 'bd list' to see all team's issues")
|
||||
|
||||
|
||||
if protectedMain {
|
||||
fmt.Println(" • Issue updates commit to", syncBranch)
|
||||
fmt.Println(" • Periodically merge", syncBranch, "to main via PR")
|
||||
}
|
||||
|
||||
|
||||
if autoSync {
|
||||
fmt.Println(" • Daemon automatically commits and pushes changes")
|
||||
} else {
|
||||
fmt.Println(" • Run 'bd sync' manually to sync changes")
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
if protectedMain {
|
||||
|
||||
@@ -7,15 +7,16 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
var labelCmd = &cobra.Command{
|
||||
Use: "label",
|
||||
Short: "Manage issue labels",
|
||||
Use: "label",
|
||||
GroupID: "issues",
|
||||
Short: "Manage issue labels",
|
||||
}
|
||||
// Helper function to process label operations for multiple issues
|
||||
func processBatchLabelOperation(issueIDs []string, label string, operation string, jsonOut bool,
|
||||
@@ -40,14 +41,13 @@ func processBatchLabelOperation(issueIDs []string, label string, operation strin
|
||||
"label": label,
|
||||
})
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
verb := "Added"
|
||||
prep := "to"
|
||||
if operation == "removed" {
|
||||
verb = "Removed"
|
||||
prep = "from"
|
||||
}
|
||||
fmt.Printf("%s %s label '%s' %s %s\n", green("✓"), verb, label, prep, issueID)
|
||||
fmt.Printf("%s %s label '%s' %s %s\n", ui.RenderPass("✓"), verb, label, prep, issueID)
|
||||
}
|
||||
}
|
||||
if len(issueIDs) > 0 && daemonClient == nil {
|
||||
@@ -217,8 +217,7 @@ var labelListCmd = &cobra.Command{
|
||||
fmt.Printf("\n%s has no labels\n", issueID)
|
||||
return
|
||||
}
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s Labels for %s:\n", cyan("🏷"), issueID)
|
||||
fmt.Printf("\n%s Labels for %s:\n", ui.RenderAccent("🏷"), issueID)
|
||||
for _, label := range labels {
|
||||
fmt.Printf(" - %s\n", label)
|
||||
}
|
||||
@@ -302,8 +301,7 @@ var labelListAllCmd = &cobra.Command{
|
||||
outputJSON(result)
|
||||
return
|
||||
}
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s All labels (%d unique):\n", cyan("🏷"), len(labels))
|
||||
fmt.Printf("\n%s All labels (%d unique):\n", ui.RenderAccent("🏷"), len(labels))
|
||||
// Find longest label for alignment
|
||||
maxLen := 0
|
||||
for _, label := range labels {
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/validation"
|
||||
)
|
||||
|
||||
@@ -397,8 +397,7 @@ func createIssuesFromMarkdown(_ *cobra.Command, filepath string) {
|
||||
|
||||
// Report failures if any
|
||||
if len(failedIssues) > 0 {
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
fmt.Fprintf(os.Stderr, "\n%s Failed to create %d issues:\n", red("✗"), len(failedIssues))
|
||||
fmt.Fprintf(os.Stderr, "\n%s Failed to create %d issues:\n", ui.RenderFail("✗"), len(failedIssues))
|
||||
for _, title := range failedIssues {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", title)
|
||||
}
|
||||
@@ -407,8 +406,7 @@ func createIssuesFromMarkdown(_ *cobra.Command, filepath string) {
|
||||
if jsonOutput {
|
||||
outputJSON(createdIssues)
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Created %d issues from %s:\n", green("✓"), len(createdIssues), filepath)
|
||||
fmt.Printf("%s Created %d issues from %s:\n", ui.RenderPass("✓"), len(createdIssues), filepath)
|
||||
for _, issue := range createdIssues {
|
||||
fmt.Printf(" %s: %s [P%d, %s]\n", issue.ID, issue.Title, issue.Priority, issue.IssueType)
|
||||
}
|
||||
|
||||
@@ -8,20 +8,22 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
// TODO: Consider integrating into 'bd doctor' migration detection
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Migrate database to current version",
|
||||
Use: "migrate",
|
||||
GroupID: "maint",
|
||||
Short: "Migrate database to current version",
|
||||
Long: `Detect and migrate database files to the current version.
|
||||
|
||||
This command:
|
||||
@@ -140,12 +142,12 @@ This command:
|
||||
fmt.Printf(" Current database: %s\n", filepath.Base(currentDB.path))
|
||||
fmt.Printf(" Schema version: %s\n", currentDB.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 {
|
||||
color.Green(" ✓ Version matches\n")
|
||||
fmt.Printf(" %s\n", ui.RenderPass("✓ Version matches"))
|
||||
}
|
||||
} 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 {
|
||||
@@ -231,7 +233,7 @@ This command:
|
||||
os.Exit(1)
|
||||
}
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
if err := store.Close(); err != nil {
|
||||
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 {
|
||||
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 {
|
||||
if err := os.Remove(db.path); err != nil {
|
||||
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 {
|
||||
fmt.Printf("Removed %s\n", filepath.Base(db.path))
|
||||
@@ -369,7 +371,7 @@ This command:
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
fmt.Printf("\nWould migrate %d issues to hash-based IDs\n", len(mapping))
|
||||
} 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 {
|
||||
@@ -464,7 +466,7 @@ This command:
|
||||
if !dryRun {
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
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
|
||||
}
|
||||
@@ -693,7 +695,7 @@ func handleUpdateRepoID(dryRun bool, autoYes bool) {
|
||||
"new_repo_id": newRepoID[:8],
|
||||
})
|
||||
} 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(" New: %s\n", newRepoID[:8])
|
||||
}
|
||||
@@ -1016,7 +1018,7 @@ func handleToSeparateBranch(branch string, dryRun bool) {
|
||||
"message": "sync.branch already set to this value",
|
||||
})
|
||||
} 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")
|
||||
}
|
||||
return
|
||||
@@ -1044,7 +1046,7 @@ func handleToSeparateBranch(branch string, dryRun bool) {
|
||||
"message": "Enabled separate branch workflow",
|
||||
})
|
||||
} 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.Println("Next steps:")
|
||||
fmt.Println(" 1. Restart the daemon to create worktree and start committing to the branch:")
|
||||
|
||||
@@ -13,16 +13,18 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// TODO: Consider integrating into 'bd doctor' migration detection
|
||||
var migrateHashIDsCmd = &cobra.Command{
|
||||
Use: "migrate-hash-ids",
|
||||
Short: "Migrate sequential IDs to hash-based IDs (legacy)",
|
||||
Use: "migrate-hash-ids",
|
||||
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).
|
||||
|
||||
*** LEGACY COMMAND ***
|
||||
@@ -86,7 +88,7 @@ WARNING: Backup your database before running this command, even though it create
|
||||
os.Exit(1)
|
||||
}
|
||||
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")
|
||||
if err := saveMappingFile(mappingPath, mapping); err != nil {
|
||||
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 {
|
||||
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++
|
||||
}
|
||||
} 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.Println("\nNext steps:")
|
||||
fmt.Println(" 1. Run 'bd export' to update JSONL file")
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO: Consider integrating into 'bd doctor' migration detection
|
||||
var migrateTombstonesCmd = &cobra.Command{
|
||||
Use: "migrate-tombstones",
|
||||
Short: "Convert deletions.jsonl entries to inline tombstones",
|
||||
Use: "migrate-tombstones",
|
||||
GroupID: "maint",
|
||||
Short: "Convert deletions.jsonl entries to inline tombstones",
|
||||
Long: `Migrate legacy deletions.jsonl entries to inline tombstones in issues.jsonl.
|
||||
|
||||
This command converts existing deletion records from the legacy deletions.jsonl
|
||||
@@ -149,7 +151,7 @@ Examples:
|
||||
// Print warnings from loading
|
||||
for _, warning := range warnings {
|
||||
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 {
|
||||
// Warn but don't fail - tombstones were already created
|
||||
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 {
|
||||
fmt.Printf(" ✓ Archived deletions.jsonl to %s\n", filepath.Base(archivePath))
|
||||
@@ -305,7 +307,7 @@ Examples:
|
||||
"migrated_ids": migratedIDs,
|
||||
})
|
||||
} 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))
|
||||
if len(skippedIDs) > 0 {
|
||||
fmt.Printf(" Skipped: %d (already had tombstones)\n", len(skippedIDs))
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/hooks"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
var moleculeCmd = &cobra.Command{
|
||||
@@ -268,8 +268,7 @@ Examples:
|
||||
if jsonOutput {
|
||||
outputJSON(&issue)
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Created work item: %s (from template %s)\n", green("✓"), issue.ID, moleculeID)
|
||||
fmt.Printf("%s Created work item: %s (from template %s)\n", ui.RenderPass("✓"), issue.ID, moleculeID)
|
||||
fmt.Printf(" Title: %s\n", issue.Title)
|
||||
fmt.Printf(" Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf(" Status: %s\n", issue.Status)
|
||||
@@ -328,8 +327,7 @@ Examples:
|
||||
if jsonOutput {
|
||||
outputJSON(createdIssue)
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Created work item: %s (from template %s)\n", green("✓"), createdIssue.ID, moleculeID)
|
||||
fmt.Printf("%s Created work item: %s (from template %s)\n", ui.RenderPass("✓"), createdIssue.ID, moleculeID)
|
||||
fmt.Printf(" Title: %s\n", createdIssue.Title)
|
||||
fmt.Printf(" Priority: P%d\n", createdIssue.Priority)
|
||||
fmt.Printf(" Status: %s\n", createdIssue.Status)
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
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`" + ``
|
||||
|
||||
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 {
|
||||
_, err := fmt.Fprintf(w, format, args...)
|
||||
return err
|
||||
@@ -54,7 +50,7 @@ func renderOnboardInstructions(w io.Writer) error {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if err := writeln(agentsContent); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -108,8 +104,9 @@ func renderOnboardInstructions(w io.Writer) error {
|
||||
}
|
||||
|
||||
var onboardCmd = &cobra.Command{
|
||||
Use: "onboard",
|
||||
Short: "Display minimal snippet for AGENTS.md",
|
||||
Use: "onboard",
|
||||
GroupID: "setup",
|
||||
Short: "Display minimal snippet for AGENTS.md",
|
||||
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
|
||||
|
||||
@@ -5,16 +5,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var pinCmd = &cobra.Command{
|
||||
Use: "pin [id...]",
|
||||
Short: "Pin one or more issues as persistent context markers",
|
||||
Use: "pin [id...]",
|
||||
GroupID: "issues",
|
||||
Short: "Pin one or more issues 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
|
||||
@@ -78,8 +79,8 @@ Examples:
|
||||
pinnedIssues = append(pinnedIssues, &issue)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Pinned %s\n", green("📌"), fullID)
|
||||
|
||||
fmt.Printf("%s Pinned %s\n", ui.RenderPass("📌"), fullID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,98 +3,94 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
var quickstartCmd = &cobra.Command{
|
||||
Use: "quickstart",
|
||||
Short: "Quick start guide for bd",
|
||||
Use: "quickstart",
|
||||
GroupID: "setup",
|
||||
Short: "Quick start guide for bd",
|
||||
Long: `Display a quick start guide showing common bd workflows and patterns.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
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("\n%s\n\n", ui.RenderBold("bd - Dependency-Aware Issue Tracker"))
|
||||
fmt.Printf("Issues chained together like beads.\n\n")
|
||||
|
||||
fmt.Printf("%s\n", bold("GETTING STARTED"))
|
||||
fmt.Printf(" %s Initialize bd in your project\n", cyan("bd init"))
|
||||
fmt.Printf("%s\n", ui.RenderBold("GETTING STARTED"))
|
||||
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(" 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("%s\n", bold("CREATING ISSUES"))
|
||||
fmt.Printf(" %s\n", cyan("bd create \"Fix login bug\""))
|
||||
fmt.Printf(" %s\n", cyan("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", ui.RenderBold("CREATING ISSUES"))
|
||||
fmt.Printf(" %s\n", ui.RenderAccent("bd create \"Fix login bug\""))
|
||||
fmt.Printf(" %s\n", ui.RenderAccent("bd create \"Add auth\" -p 0 -t feature"))
|
||||
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 List all issues\n", cyan("bd list"))
|
||||
fmt.Printf(" %s List by status\n", cyan("bd list --status open"))
|
||||
fmt.Printf(" %s List by priority (0-4, 0=highest)\n", cyan("bd list --priority 0"))
|
||||
fmt.Printf(" %s Show issue details\n\n", cyan("bd show bd-1"))
|
||||
fmt.Printf("%s\n", ui.RenderBold("VIEWING ISSUES"))
|
||||
fmt.Printf(" %s List all issues\n", ui.RenderAccent("bd list"))
|
||||
fmt.Printf(" %s List by status\n", ui.RenderAccent("bd list --status open"))
|
||||
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", ui.RenderAccent("bd show bd-1"))
|
||||
|
||||
fmt.Printf("%s\n", bold("MANAGING DEPENDENCIES"))
|
||||
fmt.Printf(" %s Add dependency (bd-2 blocks bd-1)\n", cyan("bd dep add bd-1 bd-2"))
|
||||
fmt.Printf(" %s Visualize dependency tree\n", cyan("bd dep tree bd-1"))
|
||||
fmt.Printf(" %s Detect circular dependencies\n\n", cyan("bd dep cycles"))
|
||||
fmt.Printf("%s\n", ui.RenderBold("MANAGING DEPENDENCIES"))
|
||||
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", ui.RenderAccent("bd dep tree bd-1"))
|
||||
fmt.Printf(" %s Detect circular dependencies\n\n", ui.RenderAccent("bd dep cycles"))
|
||||
|
||||
fmt.Printf("%s\n", bold("DEPENDENCY TYPES"))
|
||||
fmt.Printf(" %s Task B must complete before task A\n", yellow("blocks"))
|
||||
fmt.Printf(" %s Soft connection, doesn't block progress\n", yellow("related"))
|
||||
fmt.Printf(" %s Epic/subtask hierarchical relationship\n", yellow("parent-child"))
|
||||
fmt.Printf(" %s Auto-created when AI discovers related work\n\n", yellow("discovered-from"))
|
||||
fmt.Printf("%s\n", ui.RenderBold("DEPENDENCY TYPES"))
|
||||
fmt.Printf(" %s Task B must complete before task A\n", ui.RenderWarn("blocks"))
|
||||
fmt.Printf(" %s Soft connection, doesn't block progress\n", ui.RenderWarn("related"))
|
||||
fmt.Printf(" %s Epic/subtask hierarchical relationship\n", ui.RenderWarn("parent-child"))
|
||||
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 Show issues ready to work on\n", cyan("bd ready"))
|
||||
fmt.Printf("%s\n", ui.RenderBold("READY WORK"))
|
||||
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(" Perfect for agents to claim next work!\n\n")
|
||||
|
||||
fmt.Printf("%s\n", bold("UPDATING ISSUES"))
|
||||
fmt.Printf(" %s\n", cyan("bd update bd-1 --status in_progress"))
|
||||
fmt.Printf(" %s\n", cyan("bd update bd-1 --priority 0"))
|
||||
fmt.Printf(" %s\n\n", cyan("bd update bd-1 --assignee bob"))
|
||||
fmt.Printf("%s\n", ui.RenderBold("UPDATING ISSUES"))
|
||||
fmt.Printf(" %s\n", ui.RenderAccent("bd update bd-1 --status in_progress"))
|
||||
fmt.Printf(" %s\n", ui.RenderAccent("bd update bd-1 --priority 0"))
|
||||
fmt.Printf(" %s\n\n", ui.RenderAccent("bd update bd-1 --assignee bob"))
|
||||
|
||||
fmt.Printf("%s\n", bold("CLOSING ISSUES"))
|
||||
fmt.Printf(" %s\n", cyan("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", ui.RenderBold("CLOSING ISSUES"))
|
||||
fmt.Printf(" %s\n", ui.RenderAccent("bd close bd-1"))
|
||||
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(" 1. %s flag\n", cyan("--db /path/to/db.db"))
|
||||
fmt.Printf(" 2. %s environment variable\n", cyan("$BEADS_DB"))
|
||||
fmt.Printf(" 3. %s in current directory or ancestors\n", cyan(".beads/*.db"))
|
||||
fmt.Printf(" 4. %s as fallback\n\n", cyan("~/.beads/default.db"))
|
||||
fmt.Printf(" 1. %s flag\n", ui.RenderAccent("--db /path/to/db.db"))
|
||||
fmt.Printf(" 2. %s environment variable\n", ui.RenderAccent("$BEADS_DB"))
|
||||
fmt.Printf(" 3. %s in current directory or ancestors\n", ui.RenderAccent(".beads/*.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(" • Agents create issues when discovering new work\n")
|
||||
fmt.Printf(" • %s shows unblocked work ready to claim\n", cyan("bd ready"))
|
||||
fmt.Printf(" • Use %s flags for programmatic parsing\n", cyan("--json"))
|
||||
fmt.Printf(" • %s shows unblocked work ready to claim\n", ui.RenderAccent("bd ready"))
|
||||
fmt.Printf(" • Use %s flags for programmatic parsing\n", ui.RenderAccent("--json"))
|
||||
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(" • Add your own tables (e.g., %s)\n", cyan("myapp_executions"))
|
||||
fmt.Printf(" • Join with %s table for powerful queries\n", cyan("issues"))
|
||||
fmt.Printf(" • Add your own tables (e.g., %s)\n", ui.RenderAccent("myapp_executions"))
|
||||
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(" %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(" • %s Export to JSONL after CRUD operations (5s debounce)\n", green("✓"))
|
||||
fmt.Printf(" • %s Import from JSONL when newer than DB (after %s)\n", green("✓"), cyan("git pull"))
|
||||
fmt.Printf(" • %s Works seamlessly across machines and team members\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", ui.RenderPass("✓"), ui.RenderAccent("git pull"))
|
||||
fmt.Printf(" • %s Works seamlessly across machines and team members\n", ui.RenderPass("✓"))
|
||||
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("Run %s to create your first issue.\n\n", cyan("bd create \"My first issue\""))
|
||||
fmt.Printf("%s\n", ui.RenderPass("Ready to start!"))
|
||||
fmt.Printf("Run %s to create your first issue.\n\n", ui.RenderAccent("bd create \"My first issue\""))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/util"
|
||||
)
|
||||
var readyCmd = &cobra.Command{
|
||||
@@ -105,20 +105,20 @@ var readyCmd = &cobra.Command{
|
||||
hasOpenIssues = stats.OpenIssues > 0 || stats.InProgressIssues > 0
|
||||
}
|
||||
}
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
if hasOpenIssues {
|
||||
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
|
||||
yellow("✨"))
|
||||
ui.RenderWarn("✨"))
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s No open issues\n\n", green("✨"))
|
||||
fmt.Printf("\n%s No open issues\n\n", ui.RenderPass("✨"))
|
||||
}
|
||||
return
|
||||
}
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", cyan("📋"), len(issues))
|
||||
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", ui.RenderAccent("📋"), len(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 {
|
||||
fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes)
|
||||
}
|
||||
@@ -175,21 +175,21 @@ var readyCmd = &cobra.Command{
|
||||
hasOpenIssues = stats.OpenIssues > 0 || stats.InProgressIssues > 0
|
||||
}
|
||||
if hasOpenIssues {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
|
||||
yellow("✨"))
|
||||
ui.RenderWarn("✨"))
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s No open issues\n\n", green("✨"))
|
||||
fmt.Printf("\n%s No open issues\n\n", ui.RenderPass("✨"))
|
||||
}
|
||||
// Show tip even when no ready work found
|
||||
maybeShowTip(store)
|
||||
return
|
||||
}
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", cyan("📋"), len(issues))
|
||||
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", ui.RenderAccent("📋"), len(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 {
|
||||
fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes)
|
||||
}
|
||||
@@ -233,14 +233,14 @@ var blockedCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
if len(blocked) == 0 {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s No blocked issues\n\n", green("✨"))
|
||||
fmt.Printf("\n%s No blocked issues\n\n", ui.RenderPass("✨"))
|
||||
return
|
||||
}
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
fmt.Printf("\n%s Blocked issues (%d):\n\n", red("🚫"), len(blocked))
|
||||
fmt.Printf("\n%s Blocked issues (%d):\n\n", ui.RenderFail("🚫"), len(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
|
||||
if blockedBy == nil {
|
||||
blockedBy = []string{}
|
||||
@@ -272,16 +272,13 @@ var statsCmd = &cobra.Command{
|
||||
outputJSON(stats)
|
||||
return
|
||||
}
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s Beads Statistics:\n\n", cyan("📊"))
|
||||
fmt.Printf("\n%s Beads Statistics:\n\n", ui.RenderAccent("📊"))
|
||||
fmt.Printf("Total Issues: %d\n", stats.TotalIssues)
|
||||
fmt.Printf("Open: %s\n", green(fmt.Sprintf("%d", stats.OpenIssues)))
|
||||
fmt.Printf("In Progress: %s\n", yellow(fmt.Sprintf("%d", stats.InProgressIssues)))
|
||||
fmt.Printf("Open: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.OpenIssues)))
|
||||
fmt.Printf("In Progress: %s\n", ui.RenderWarn(fmt.Sprintf("%d", stats.InProgressIssues)))
|
||||
fmt.Printf("Closed: %d\n", stats.ClosedIssues)
|
||||
fmt.Printf("Blocked: %d\n", stats.BlockedIssues)
|
||||
fmt.Printf("Ready: %s\n", green(fmt.Sprintf("%d", stats.ReadyIssues)))
|
||||
fmt.Printf("Blocked: %s\n", ui.RenderFail(fmt.Sprintf("%d", stats.BlockedIssues)))
|
||||
fmt.Printf("Ready: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.ReadyIssues)))
|
||||
if stats.TombstoneIssues > 0 {
|
||||
fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues)
|
||||
}
|
||||
@@ -316,16 +313,13 @@ var statsCmd = &cobra.Command{
|
||||
outputJSON(stats)
|
||||
return
|
||||
}
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s Beads Statistics:\n\n", cyan("📊"))
|
||||
fmt.Printf("\n%s Beads Statistics:\n\n", ui.RenderAccent("📊"))
|
||||
fmt.Printf("Total Issues: %d\n", stats.TotalIssues)
|
||||
fmt.Printf("Open: %s\n", green(fmt.Sprintf("%d", stats.OpenIssues)))
|
||||
fmt.Printf("In Progress: %s\n", yellow(fmt.Sprintf("%d", stats.InProgressIssues)))
|
||||
fmt.Printf("Open: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.OpenIssues)))
|
||||
fmt.Printf("In Progress: %s\n", ui.RenderWarn(fmt.Sprintf("%d", stats.InProgressIssues)))
|
||||
fmt.Printf("Closed: %d\n", stats.ClosedIssues)
|
||||
fmt.Printf("Blocked: %d\n", stats.BlockedIssues)
|
||||
fmt.Printf("Ready: %s\n", green(fmt.Sprintf("%d", stats.ReadyIssues)))
|
||||
fmt.Printf("Blocked: %s\n", ui.RenderFail(fmt.Sprintf("%d", stats.BlockedIssues)))
|
||||
fmt.Printf("Ready: %s\n", ui.RenderPass(fmt.Sprintf("%d", stats.ReadyIssues)))
|
||||
if stats.TombstoneIssues > 0 {
|
||||
fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues)
|
||||
}
|
||||
@@ -333,7 +327,7 @@ var statsCmd = &cobra.Command{
|
||||
fmt.Printf("Pinned: %d\n", stats.PinnedIssues)
|
||||
}
|
||||
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 {
|
||||
fmt.Printf("Avg Lead Time: %.1f hours\n", stats.AverageLeadTime)
|
||||
|
||||
@@ -5,16 +5,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var relateCmd = &cobra.Command{
|
||||
Use: "relate <id1> <id2>",
|
||||
Short: "Create a bidirectional relates_to link between issues",
|
||||
Use: "relate <id1> <id2>",
|
||||
GroupID: "deps",
|
||||
Short: "Create a bidirectional relates_to link between issues",
|
||||
Long: `Create a loose 'see also' relationship between two issues.
|
||||
|
||||
The relates_to link is bidirectional - both issues will reference each other.
|
||||
@@ -28,8 +29,9 @@ Examples:
|
||||
}
|
||||
|
||||
var unrelateCmd = &cobra.Command{
|
||||
Use: "unrelate <id1> <id2>",
|
||||
Short: "Remove a relates_to link between issues",
|
||||
Use: "unrelate <id1> <id2>",
|
||||
GroupID: "deps",
|
||||
Short: "Remove a relates_to link between issues",
|
||||
Long: `Remove a relates_to relationship between two issues.
|
||||
|
||||
Removes the link in both directions.
|
||||
@@ -177,8 +179,7 @@ func runRelate(cmd *cobra.Command, args []string) error {
|
||||
return encoder.Encode(result)
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Linked %s ↔ %s\n", green("✓"), id1, id2)
|
||||
fmt.Printf("%s Linked %s ↔ %s\n", ui.RenderPass("✓"), id1, id2)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -300,8 +301,7 @@ func runUnrelate(cmd *cobra.Command, args []string) error {
|
||||
return encoder.Encode(result)
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Unlinked %s ↔ %s\n", green("✓"), id1, id2)
|
||||
fmt.Printf("%s Unlinked %s ↔ %s\n", ui.RenderPass("✓"), id1, id2)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,18 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var renamePrefixCmd = &cobra.Command{
|
||||
Use: "rename-prefix <new-prefix>",
|
||||
Short: "Rename the issue prefix for all issues in the database",
|
||||
Use: "rename-prefix <new-prefix>",
|
||||
GroupID: "advanced",
|
||||
Short: "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.
|
||||
|
||||
@@ -99,12 +100,10 @@ NOTE: This is a rare operation. Most users never need this command.`,
|
||||
|
||||
if len(prefixes) > 1 {
|
||||
// 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 {
|
||||
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")
|
||||
|
||||
@@ -141,8 +140,7 @@ NOTE: This is a rare operation. Most users never need this command.`,
|
||||
}
|
||||
|
||||
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")
|
||||
for i, issue := range issues {
|
||||
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+"-"))
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
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 {
|
||||
result := map[string]interface{}{
|
||||
@@ -230,9 +226,6 @@ type issueSort struct {
|
||||
// Issues with the correct prefix are left unchanged.
|
||||
// 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 {
|
||||
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
|
||||
var correctIssues []*types.Issue
|
||||
@@ -290,7 +283,7 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t
|
||||
|
||||
if dryRun {
|
||||
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("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
|
||||
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
|
||||
}
|
||||
|
||||
// Perform the repairs
|
||||
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))
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -378,7 +371,7 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t
|
||||
markDirtyAndScheduleFullExport()
|
||||
|
||||
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))
|
||||
|
||||
if jsonOutput {
|
||||
|
||||
@@ -3,15 +3,16 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
var reopenCmd = &cobra.Command{
|
||||
Use: "reopen [id...]",
|
||||
Short: "Reopen one or more closed issues",
|
||||
Use: "reopen [id...]",
|
||||
GroupID: "issues",
|
||||
Short: "Reopen one or more closed issues",
|
||||
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.`,
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
blue := color.New(color.FgBlue).SprintFunc()
|
||||
reasonMsg := ""
|
||||
if 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 {
|
||||
@@ -120,12 +120,11 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
|
||||
reopenedIssues = append(reopenedIssues, issue)
|
||||
}
|
||||
} else {
|
||||
blue := color.New(color.FgBlue).SprintFunc()
|
||||
reasonMsg := ""
|
||||
if 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
|
||||
|
||||
@@ -5,15 +5,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"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{
|
||||
Use: "repair-deps",
|
||||
Short: "Find and fix orphaned dependency references",
|
||||
Use: "repair-deps",
|
||||
GroupID: "maint",
|
||||
Short: "Find and fix orphaned dependency references",
|
||||
Long: `Scans all issues for dependencies pointing to non-existent issues.
|
||||
|
||||
Reports orphaned dependencies and optionally removes them with --fix.
|
||||
@@ -105,13 +107,11 @@ Interactive mode with --interactive prompts for each orphan.`,
|
||||
|
||||
// Report results
|
||||
if len(orphans) == 0 {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s No orphaned dependencies found\n\n", green("✓"))
|
||||
fmt.Printf("\n%s No orphaned dependencies found\n\n", ui.RenderPass("✓"))
|
||||
return
|
||||
}
|
||||
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s Found %d orphaned dependencies:\n\n", yellow("⚠"), len(orphans))
|
||||
fmt.Printf("\n%s Found %d orphaned dependencies:\n\n", ui.RenderWarn("⚠"), len(orphans))
|
||||
|
||||
for i, o := range orphans {
|
||||
fmt.Printf("%d. %s → %s (%s) [%s does not exist]\n",
|
||||
@@ -142,8 +142,7 @@ Interactive mode with --interactive prompts for each orphan.`,
|
||||
}
|
||||
}
|
||||
markDirtyAndScheduleFlush()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s Fixed %d orphaned dependencies\n\n", green("✓"), fixed)
|
||||
fmt.Printf("\n%s Fixed %d orphaned dependencies\n\n", ui.RenderPass("✓"), fixed)
|
||||
} else if fix {
|
||||
db := store.UnderlyingDB()
|
||||
for _, o := range orphans {
|
||||
@@ -159,8 +158,7 @@ Interactive mode with --interactive prompts for each orphan.`,
|
||||
}
|
||||
}
|
||||
markDirtyAndScheduleFlush()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Fixed %d orphaned dependencies\n\n", green("✓"), len(orphans))
|
||||
fmt.Printf("%s Fixed %d orphaned dependencies\n\n", ui.RenderPass("✓"), len(orphans))
|
||||
} else {
|
||||
fmt.Printf("Run with --fix to automatically remove orphaned dependencies\n")
|
||||
fmt.Printf("Run with --interactive to review each dependency\n\n")
|
||||
|
||||
@@ -8,14 +8,15 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/git"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
var resetCmd = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Remove all beads data and configuration",
|
||||
Use: "reset",
|
||||
GroupID: "advanced",
|
||||
Short: "Remove all beads data and configuration",
|
||||
Long: `Reset beads to an uninitialized state, removing all local data.
|
||||
|
||||
This command removes:
|
||||
@@ -206,29 +207,26 @@ func showResetPreview(items []resetItem) {
|
||||
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("The following will be removed:")
|
||||
fmt.Println()
|
||||
|
||||
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" {
|
||||
fmt.Printf(" %s\n", item.Path)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(red("⚠ This operation cannot be undone!"))
|
||||
fmt.Println(ui.RenderFail("⚠ This operation cannot be undone!"))
|
||||
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) {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
|
||||
var errors []string
|
||||
|
||||
@@ -238,14 +236,14 @@ func performReset(items []resetItem, _, beadsDir string) {
|
||||
pidFile := filepath.Join(beadsDir, "daemon.pid")
|
||||
stopDaemonQuiet(pidFile)
|
||||
if !jsonOutput {
|
||||
fmt.Printf("%s Stopped daemon\n", green("✓"))
|
||||
fmt.Printf("%s Stopped daemon\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
case "hook":
|
||||
if err := os.Remove(item.Path); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("failed to remove hook %s: %v", item.Path, err))
|
||||
} 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
|
||||
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.name").Run()
|
||||
if !jsonOutput {
|
||||
fmt.Printf("%s Removed merge driver config\n", green("✓"))
|
||||
fmt.Printf("%s Removed merge driver config\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
case "gitattributes":
|
||||
if err := removeGitattributesEntry(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("failed to update .gitattributes: %v", err))
|
||||
} else if !jsonOutput {
|
||||
fmt.Printf("%s Updated .gitattributes\n", green("✓"))
|
||||
fmt.Printf("%s Updated .gitattributes\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
case "worktrees":
|
||||
if err := os.RemoveAll(item.Path); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("failed to remove worktrees: %v", err))
|
||||
} else if !jsonOutput {
|
||||
fmt.Printf("%s Removed sync worktrees\n", green("✓"))
|
||||
fmt.Printf("%s Removed sync worktrees\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
case "directory":
|
||||
if err := os.RemoveAll(item.Path); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("failed to remove .beads: %v", err))
|
||||
} 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)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("%s Reset complete\n", green("✓"))
|
||||
fmt.Printf("%s Reset complete\n", ui.RenderPass("✓"))
|
||||
fmt.Println()
|
||||
fmt.Println("To reinitialize beads, run: bd init")
|
||||
}
|
||||
|
||||
@@ -8,14 +8,15 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
var restoreCmd = &cobra.Command{
|
||||
Use: "restore <issue-id>",
|
||||
Short: "Restore full history of a compacted issue from git",
|
||||
Use: "restore <issue-id>",
|
||||
GroupID: "sync",
|
||||
Short: "Restore full history of a compacted issue from git",
|
||||
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:
|
||||
@@ -185,59 +186,54 @@ func readIssueFromJSONL(jsonlPath, issueID string) (*types.Issue, error) {
|
||||
|
||||
// displayRestoredIssue displays the restored issue in a readable format
|
||||
func displayRestoredIssue(issue *types.Issue, commitHash string) {
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
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))
|
||||
fmt.Printf("\n%s %s (restored from git commit %s)\n", ui.RenderAccent("📜"), ui.RenderBold(issue.ID), ui.RenderWarn(commitHash[:8]))
|
||||
fmt.Printf("%s\n\n", ui.RenderBold(issue.Title))
|
||||
|
||||
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 != "" {
|
||||
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 != "" {
|
||||
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 != "" {
|
||||
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",
|
||||
bold("Status:"), issue.Status,
|
||||
bold("Priority:"), issue.Priority,
|
||||
bold("Type:"), issue.IssueType,
|
||||
ui.RenderBold("Status:"), issue.Status,
|
||||
ui.RenderBold("Priority:"), issue.Priority,
|
||||
ui.RenderBold("Type:"), issue.IssueType,
|
||||
)
|
||||
|
||||
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 {
|
||||
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("%s %s\n", bold("Updated:"), issue.UpdatedAt.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", ui.RenderBold("Updated:"), issue.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||
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 {
|
||||
fmt.Printf("\n%s\n", bold("Dependencies:"))
|
||||
fmt.Printf("\n%s\n", ui.RenderBold("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 {
|
||||
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 {
|
||||
fmt.Printf(" at %s", issue.CompactedAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
@@ -9,21 +9,22 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/hooks"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
"github.com/steveyegge/beads/internal/validation"
|
||||
)
|
||||
|
||||
var showCmd = &cobra.Command{
|
||||
Use: "show [id...]",
|
||||
Short: "Show issue details",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Use: "show [id...]",
|
||||
GroupID: "issues",
|
||||
Short: "Show issue details",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
showThread, _ := cmd.Flags().GetBool("thread")
|
||||
@@ -118,8 +119,6 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
issue := &details.Issue
|
||||
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
// Format output (same as direct mode below)
|
||||
tierEmoji := ""
|
||||
statusSuffix := ""
|
||||
@@ -132,7 +131,7 @@ var showCmd = &cobra.Command{
|
||||
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)
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
@@ -299,8 +298,6 @@ var showCmd = &cobra.Command{
|
||||
fmt.Println("\n" + strings.Repeat("─", 60))
|
||||
}
|
||||
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
// Add compaction emoji to title line
|
||||
tierEmoji := ""
|
||||
statusSuffix := ""
|
||||
@@ -313,7 +310,7 @@ var showCmd = &cobra.Command{
|
||||
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)
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
@@ -463,9 +460,10 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update [id...]",
|
||||
Short: "Update one or more issues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Use: "update [id...]",
|
||||
GroupID: "issues",
|
||||
Short: "Update one or more issues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckReadonly("update")
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
@@ -659,8 +657,7 @@ var updateCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
if !jsonOutput {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
|
||||
fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,8 +751,7 @@ var updateCmd = &cobra.Command{
|
||||
updatedIssues = append(updatedIssues, updatedIssue)
|
||||
}
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
|
||||
fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,8 +767,9 @@ var updateCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var editCmd = &cobra.Command{
|
||||
Use: "edit [id]",
|
||||
Short: "Edit an issue field in $EDITOR",
|
||||
Use: "edit [id]",
|
||||
GroupID: "issues",
|
||||
Short: "Edit an issue field in $EDITOR",
|
||||
Long: `Edit an issue field using your configured $EDITOR.
|
||||
|
||||
By default, edits the description. Use flags to edit other fields.
|
||||
@@ -962,16 +959,16 @@ Examples:
|
||||
markDirtyAndScheduleFlush()
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
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{
|
||||
Use: "close [id...]",
|
||||
Short: "Close one or more issues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Use: "close [id...]",
|
||||
GroupID: "issues",
|
||||
Short: "Close one or more issues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckReadonly("close")
|
||||
reason, _ := cmd.Flags().GetString("reason")
|
||||
@@ -1053,8 +1050,7 @@ var closeCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
if !jsonOutput {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
|
||||
fmt.Printf("%s Closed %s: %s\n", ui.RenderPass("✓"), id, reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,8 +1096,7 @@ var closeCmd = &cobra.Command{
|
||||
closedIssues = append(closedIssues, closedIssue)
|
||||
}
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
|
||||
fmt.Printf("%s Closed %s: %s\n", ui.RenderPass("✓"), id, reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,10 +1216,7 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
}
|
||||
|
||||
// Display the thread
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
dim := color.New(color.Faint).SprintFunc()
|
||||
|
||||
fmt.Printf("\n%s Thread: %s\n", cyan("📬"), rootMsg.Title)
|
||||
fmt.Printf("\n%s Thread: %s\n", ui.RenderAccent("📬"), rootMsg.Title)
|
||||
fmt.Println(strings.Repeat("─", 66))
|
||||
|
||||
for _, msg := range threadMessages {
|
||||
@@ -1246,12 +1238,12 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
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)
|
||||
if parentID := repliesTo[msg.ID]; 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 != "" {
|
||||
// Indent the body
|
||||
bodyLines := strings.Split(msg.Description, "\n")
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
@@ -39,8 +39,9 @@ type InstantiateResult struct {
|
||||
}
|
||||
|
||||
var templateCmd = &cobra.Command{
|
||||
Use: "template",
|
||||
Short: "Manage issue templates",
|
||||
Use: "template",
|
||||
GroupID: "setup",
|
||||
Short: "Manage issue templates",
|
||||
Long: `Manage Beads templates for creating issue hierarchies.
|
||||
|
||||
Templates are epics with the "template" label. They can have child issues
|
||||
@@ -106,17 +107,14 @@ var templateListCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
fmt.Printf("%s\n", green("Templates (for bd template instantiate):"))
|
||||
fmt.Printf("%s\n", ui.RenderPass("Templates (for bd template instantiate):"))
|
||||
for _, tmpl := range beadsTemplates {
|
||||
vars := extractVariables(tmpl.Title + " " + tmpl.Description)
|
||||
varStr := ""
|
||||
if len(vars) > 0 {
|
||||
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()
|
||||
},
|
||||
@@ -175,25 +173,21 @@ func showBeadsTemplate(subgraph *TemplateSubgraph) {
|
||||
return
|
||||
}
|
||||
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
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("\n%s Template: %s\n", ui.RenderAccent("📋"), subgraph.Root.Title)
|
||||
fmt.Printf(" ID: %s\n", subgraph.Root.ID)
|
||||
fmt.Printf(" Issues: %d\n", len(subgraph.Issues))
|
||||
|
||||
// Show variables
|
||||
vars := extractAllVariables(subgraph)
|
||||
if len(vars) > 0 {
|
||||
fmt.Printf("\n%s Variables:\n", yellow("📝"))
|
||||
fmt.Printf("\n%s Variables:\n", ui.RenderWarn("📝"))
|
||||
for _, v := range vars {
|
||||
fmt.Printf(" {{%s}}\n", v)
|
||||
}
|
||||
}
|
||||
|
||||
// Show structure
|
||||
fmt.Printf("\n%s Structure:\n", green("🌲"))
|
||||
fmt.Printf("\n%s Structure:\n", ui.RenderPass("🌲"))
|
||||
printTemplateTree(subgraph, subgraph.Root.ID, 0, true)
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -309,8 +303,7 @@ Example:
|
||||
return
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Created %d issues from template\n", green("✓"), result.Created)
|
||||
fmt.Printf("%s Created %d issues from template\n", ui.RenderPass("✓"), result.Created)
|
||||
fmt.Printf(" New epic: %s\n", result.NewEpicID)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,16 +5,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var unpinCmd = &cobra.Command{
|
||||
Use: "unpin [id...]",
|
||||
Short: "Unpin one or more issues",
|
||||
Use: "unpin [id...]",
|
||||
GroupID: "issues",
|
||||
Short: "Unpin one or more issues",
|
||||
Long: `Unpin issues to remove their persistent context marker status.
|
||||
|
||||
This restores the issue to a normal work item that can be cleaned up
|
||||
@@ -78,8 +79,7 @@ Examples:
|
||||
unpinnedIssues = append(unpinnedIssues, &issue)
|
||||
}
|
||||
} else {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("%s Unpinned %s\n", yellow("📍"), id)
|
||||
fmt.Printf("%s Unpinned %s\n", ui.RenderWarn("📍"), id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +117,7 @@ Examples:
|
||||
unpinnedIssues = append(unpinnedIssues, issue)
|
||||
}
|
||||
} else {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("%s Unpinned %s\n", yellow("📍"), fullID)
|
||||
fmt.Printf("%s Unpinned %s\n", ui.RenderWarn("📍"), fullID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"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{
|
||||
Use: "validate",
|
||||
Short: "Run comprehensive database health checks",
|
||||
Use: "validate",
|
||||
GroupID: "maint",
|
||||
Short: "Run comprehensive database health checks",
|
||||
Long: `Run all validation checks to ensure database integrity:
|
||||
- Orphaned dependencies (references to deleted issues)
|
||||
- Duplicate issues (identical content)
|
||||
@@ -193,9 +195,6 @@ func (r *validationResults) toJSON() map[string]interface{} {
|
||||
return output
|
||||
}
|
||||
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("===================")
|
||||
totalIssues := 0
|
||||
@@ -204,33 +203,34 @@ func (r *validationResults) print(_ bool) {
|
||||
for _, name := range r.checkOrder {
|
||||
result := r.checks[name]
|
||||
prefix := "✓"
|
||||
colorFunc := green
|
||||
var coloredPrefix string
|
||||
if result.err != nil {
|
||||
prefix = "✗"
|
||||
colorFunc = red
|
||||
fmt.Printf("%s %s: ERROR - %v\n", colorFunc(prefix), result.name, result.err)
|
||||
coloredPrefix = ui.RenderFail(prefix)
|
||||
fmt.Printf("%s %s: ERROR - %v\n", coloredPrefix, result.name, result.err)
|
||||
} else if result.issueCount > 0 {
|
||||
prefix = "⚠"
|
||||
colorFunc = yellow
|
||||
coloredPrefix = ui.RenderWarn(prefix)
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
totalFixed += result.fixedCount
|
||||
}
|
||||
fmt.Println()
|
||||
if totalIssues == 0 {
|
||||
fmt.Printf("%s Database is healthy!\n", green("✓"))
|
||||
fmt.Printf("%s Database is healthy!\n", ui.RenderPass("✓"))
|
||||
} 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 {
|
||||
remaining := totalIssues - totalFixed
|
||||
fmt.Printf("%s Found %d issues", yellow("⚠"), totalIssues)
|
||||
fmt.Printf("%s Found %d issues", ui.RenderWarn("⚠"), totalIssues)
|
||||
if totalFixed > 0 {
|
||||
fmt.Printf(" (fixed %d, %d remaining)", totalFixed, remaining)
|
||||
}
|
||||
|
||||
114
docs/ui-philosophy.md
Normal file
114
docs/ui-philosophy.md
Normal 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
go.mod
2
go.mod
@@ -8,7 +8,6 @@ require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.19.0
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/ncruces/go-sqlite3 v0.30.3
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -39,7 +38,6 @@ require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.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-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -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/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/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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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/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/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/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-20220811171246-fbc7d0a398ab/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/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -11,8 +12,9 @@ import (
|
||||
// Ayu theme color palette
|
||||
// Dark: https://terminalcolors.com/themes/ayu/dark/
|
||||
// Light: https://terminalcolors.com/themes/ayu/light/
|
||||
// Source: https://github.com/ayu-theme/ayu-colors
|
||||
var (
|
||||
// Semantic status colors (Ayu theme - adaptive light/dark)
|
||||
// Core semantic colors (Ayu theme - adaptive light/dark)
|
||||
ColorPass = lipgloss.AdaptiveColor{
|
||||
Light: "#86b300", // ayu light bright green
|
||||
Dark: "#c2d94c", // ayu dark bright green
|
||||
@@ -33,9 +35,86 @@ var (
|
||||
Light: "#399ee6", // ayu light 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 (
|
||||
PassStyle = lipgloss.NewStyle().Foreground(ColorPass)
|
||||
WarnStyle = lipgloss.NewStyle().Foreground(ColorWarn)
|
||||
@@ -44,6 +123,36 @@ var (
|
||||
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
|
||||
var CategoryStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent)
|
||||
|
||||
@@ -128,3 +237,125 @@ func RenderSkipIcon() string {
|
||||
func RenderInfoIcon() string {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user