From e8355c26f0b23045ab0278c931deb4355f926c21 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 20 Nov 2025 20:16:56 -0500 Subject: [PATCH] Add 'bd clean' command to remove temporary merge artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This command cleans up temporary files created during git merges by reading patterns directly from .beads/.gitignore (Merge artifacts section). 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: - beads.jsonl (source of truth) - beads.db (SQLite database) - metadata.json, config.yaml - All daemon files Usage: bd clean # Clean up temporary files bd clean --dry-run # Preview what would be deleted Implementation: - Reads patterns from .beads/.gitignore instead of hardcoding them - No --force flag needed - just runs by default - Only cleans truly temporary merge artifacts, never the database Also: - Restored beads.jsonl to 538 issues from commit 6cd3a32 - Set issue-prefix to "bd" in config.yaml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .beads/config.yaml | 2 +- cmd/bd/clean.go | 161 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 cmd/bd/clean.go diff --git a/.beads/config.yaml b/.beads/config.yaml index dabc20c1..dc1e19ed 100644 --- a/.beads/config.yaml +++ b/.beads/config.yaml @@ -6,7 +6,7 @@ # Issue prefix for this repository (used by bd init) # If not set, bd init will auto-detect from directory name # Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. -# issue-prefix: "" +issue-prefix: "bd" # Use no-db mode: load from JSONL, no SQLite, write back after each command # When true, bd will use .beads/issues.jsonl as the source of truth diff --git a/cmd/bd/clean.go b/cmd/bd/clean.go new file mode 100644 index 00000000..f7bfc18b --- /dev/null +++ b/cmd/bd/clean.go @@ -0,0 +1,161 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +var cleanCmd = &cobra.Command{ + Use: "clean", + Short: "Clean up temporary beads artifacts", + Long: `Delete temporary beads artifacts to clean up after git operations. + +This removes temporary files created during git merges and conflicts from the +.beads directory. + +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: +- beads.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`, + 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(color.YellowString("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") + 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) +}