Files
beads/cmd/bd/clean.go
Ryan Snodgrass 6ca141712c 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
2025-12-20 17:09:50 -08:00

170 lines
4.4 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"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",
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.
It does NOT delete issues from the database - use 'bd cleanup' for that.
Files removed:
- 3-way merge snapshots (beads.base.jsonl, beads.left.jsonl, beads.right.jsonl)
- Merge metadata (*.meta.json)
- Git merge driver temp files (*.json[0-9], *.jsonl[0-9])
Files preserved:
- issues.jsonl (source of truth)
- beads.db (SQLite database)
- metadata.json
- config.yaml
- All daemon files
EXAMPLES:
Clean up temporary files:
bd clean
Preview what would be deleted:
bd clean --dry-run
SEE ALSO:
bd cleanup Delete closed issues from database`,
Run: func(cmd *cobra.Command, args []string) {
dryRun, _ := cmd.Flags().GetBool("dry-run")
// Find beads directory
beadsDir := findBeadsDir()
if beadsDir == "" {
fmt.Fprintf(os.Stderr, "Error: .beads directory not found\n")
os.Exit(1)
}
// Read patterns from .beads/.gitignore (only merge artifacts section)
cleanPatterns, err := readMergeArtifactPatterns(beadsDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading .gitignore: %v\n", err)
os.Exit(1)
}
// Collect files to delete
var filesToDelete []string
for _, pattern := range cleanPatterns {
matches, err := filepath.Glob(filepath.Join(beadsDir, pattern))
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: error matching pattern %s: %v\n", pattern, err)
continue
}
filesToDelete = append(filesToDelete, matches...)
}
if len(filesToDelete) == 0 {
fmt.Println("Nothing to clean - all artifacts already removed")
return
}
// Just run by default, no --force needed
if dryRun {
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 {
relPath, err := filepath.Rel(beadsDir, file)
if err != nil {
relPath = file
}
fmt.Printf(" %s\n", relPath)
}
if dryRun {
return
}
// Actually delete the files
deletedCount := 0
errorCount := 0
for _, file := range filesToDelete {
if err := os.Remove(file); err != nil {
if !os.IsNotExist(err) {
relPath, _ := filepath.Rel(beadsDir, file)
fmt.Fprintf(os.Stderr, "Warning: failed to delete %s: %v\n", relPath, err)
errorCount++
}
} else {
deletedCount++
}
}
fmt.Printf("\nDeleted %d file(s)", deletedCount)
if errorCount > 0 {
fmt.Printf(" (%d error(s))", errorCount)
}
fmt.Println()
},
}
// readMergeArtifactPatterns reads the .beads/.gitignore file and extracts
// patterns from the "Merge artifacts" section
func readMergeArtifactPatterns(beadsDir string) ([]string, error) {
gitignorePath := filepath.Join(beadsDir, ".gitignore")
// #nosec G304 -- gitignorePath is safely constructed via filepath.Join from beadsDir
// (which comes from findBeadsDir searching upward for .beads). This can only open
// .gitignore within the project's .beads directory. See TestReadMergeArtifactPatterns_PathTraversal
file, err := os.Open(gitignorePath)
if err != nil {
return nil, fmt.Errorf("failed to open .gitignore: %w", err)
}
defer file.Close()
var patterns []string
inMergeSection := false
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Look for the merge artifacts section
if strings.Contains(line, "Merge artifacts") {
inMergeSection = true
continue
}
// Stop at the next section (starts with #)
if inMergeSection && strings.HasPrefix(line, "#") {
break
}
// Collect patterns from merge section
if inMergeSection && line != "" && !strings.HasPrefix(line, "#") {
// Skip negation patterns (starting with !)
if !strings.HasPrefix(line, "!") {
patterns = append(patterns, line)
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading .gitignore: %w", err)
}
return patterns, nil
}
func init() {
cleanCmd.Flags().Bool("dry-run", false, "Preview what would be deleted without making changes")
rootCmd.AddCommand(cleanCmd)
}