Fix #274: Add automatic .beads/.gitignore upgrade (#300)

* Fix #274: Add automatic .beads/.gitignore upgrade

Implements three mechanisms to ensure users get updated gitignore:

1. bd doctor --fix: Manually upgrade gitignore
2. Daemon auto-upgrade: Upgrades on startup if outdated
3. bd init idempotent: Safe to re-run, always updates gitignore

The gitignore template now lives in cmd/bd/doctor/gitignore.go
for consistent updates across all three mechanisms.

Fixes: #274

* Remove test binary

Amp-Thread-ID: https://ampcode.com/threads/T-7042cfcc-ac97-43d7-a40f-3fa1bb4e1c2b
Co-authored-by: Amp <amp@ampcode.com>

* Fix critical issues: remove merge artifact and apply gitignore template

- Remove .beads/beads.left.jsonl (merge artifact that shouldn't be committed)
- Apply new gitignore template to .beads/.gitignore (was missing patterns)

Amp-Thread-ID: https://ampcode.com/threads/T-7042cfcc-ac97-43d7-a40f-3fa1bb4e1c2b
Co-authored-by: Amp <amp@ampcode.com>

* bd sync: 2025-11-12 11:09:30

* Retrigger CI

Amp-Thread-ID: https://ampcode.com/threads/T-8d532264-6d5e-4b68-88e9-e4511851b64a
Co-authored-by: Amp <amp@ampcode.com>

* Fix duplicate DoctorCheck type definition

* Trigger CI after fixing type conflict

Amp-Thread-ID: https://ampcode.com/threads/T-8d532264-6d5e-4b68-88e9-e4511851b64a
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-11-12 12:46:27 -08:00
committed by GitHub
parent b53d74a1c8
commit f4a2f87aff
6 changed files with 204 additions and 73 deletions

15
.beads/.gitignore vendored
View File

@@ -1,5 +1,6 @@
# SQLite databases
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
@@ -14,11 +15,15 @@ bd.sock
db.sqlite
bd.db
# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
beads.left.jsonl
beads.left.meta.json
beads.right.jsonl
beads.right.meta.json
# Keep JSONL exports and config (source of truth for git)
!*.jsonl
!issues.jsonl
!metadata.json
!config.json
# 3-way merge snapshot files (local-only, for deletion tracking)
beads.base.jsonl
beads.left.jsonl

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/daemon"
"github.com/steveyegge/beads/internal/rpc"
@@ -308,6 +309,17 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p
defer func() { _ = store.Close() }()
log.log("Database opened: %s", daemonDBPath)
// Auto-upgrade .beads/.gitignore if outdated
gitignoreCheck := doctor.CheckGitignore()
if gitignoreCheck.Status == "warning" || gitignoreCheck.Status == "error" {
log.log("Upgrading .beads/.gitignore...")
if err := doctor.FixGitignore(); err != nil {
log.log("Warning: failed to upgrade .gitignore: %v", err)
} else {
log.log("Successfully upgraded .beads/.gitignore")
}
}
// Hydrate from multi-repo if configured
hydrateCtx := context.Background()
if results, err := store.HydrateFromMultiRepo(hydrateCtx); err != nil {

View File

@@ -44,6 +44,10 @@ type doctorResult struct {
CLIVersion string `json:"cli_version"`
}
var (
doctorFix bool
)
var doctorCmd = &cobra.Command{
Use: "doctor [path]",
Short: "Check beads installation health",
@@ -62,11 +66,13 @@ This command checks:
- File permissions
- Circular dependencies
- Git hooks (pre-commit, post-merge, pre-push)
- .beads/.gitignore up to date
Examples:
bd doctor # Check current directory
bd doctor /path/to/repo # Check specific repository
bd doctor --json # Machine-readable output`,
bd doctor --json # Machine-readable output
bd doctor --fix # Automatically fix issues`,
Run: func(cmd *cobra.Command, args []string) {
// Use global jsonOutput set by PersistentPreRun
@@ -86,6 +92,13 @@ Examples:
// Run diagnostics
result := runDiagnostics(absPath)
// Apply fixes if requested
if doctorFix {
applyFixes(result)
// Re-run diagnostics to show results
result = runDiagnostics(absPath)
}
// Output results
if jsonOutput {
outputJSON(result)
@@ -100,7 +113,27 @@ Examples:
},
}
func runDiagnostics(path string) doctorResult {
func init() {
doctorCmd.Flags().BoolVar(&doctorFix, "fix", false, "Automatically fix issues where possible")
}
func applyFixes(result doctorResult) {
for _, check := range result.Checks {
if check.Status == statusWarning || check.Status == statusError {
switch check.Name {
case "Gitignore":
fmt.Println("Fixing .beads/.gitignore...")
if err := doctor.FixGitignore(); err != nil {
fmt.Fprintf(os.Stderr, " Error: %v\n", err)
} else {
fmt.Println(" ✓ Updated .beads/.gitignore")
}
}
}
}
}
func runDiagnostics(path string) doctorResult{
result := doctorResult{
Path: path,
CLIVersion: Version,
@@ -202,6 +235,11 @@ func runDiagnostics(path string) doctorResult {
result.Checks = append(result.Checks, legacyDocsCheck)
// Don't fail overall check for legacy docs, just warn
// Check 13: Gitignore up to date
gitignoreCheck := convertDoctorCheck(doctor.CheckGitignore())
result.Checks = append(result.Checks, gitignoreCheck)
// Don't fail overall check for gitignore, just warn
return result
}

104
cmd/bd/doctor/gitignore.go Normal file
View File

@@ -0,0 +1,104 @@
package doctor
import (
"os"
"path/filepath"
"strings"
)
// GitignoreTemplate is the canonical .beads/.gitignore content
const GitignoreTemplate = `# SQLite databases
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
# Legacy database files
db.sqlite
bd.db
# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
beads.left.jsonl
beads.left.meta.json
beads.right.jsonl
beads.right.meta.json
# Keep JSONL exports and config (source of truth for git)
!issues.jsonl
!metadata.json
!config.json
`
// requiredPatterns are patterns that MUST be in .beads/.gitignore
var requiredPatterns = []string{
"beads.base.jsonl",
"beads.left.jsonl",
"beads.right.jsonl",
"beads.base.meta.json",
"beads.left.meta.json",
"beads.right.meta.json",
"*.db?*",
}
// CheckGitignore checks if .beads/.gitignore is up to date
func CheckGitignore() DoctorCheck {
gitignorePath := filepath.Join(".beads", ".gitignore")
// Check if file exists
content, err := os.ReadFile(gitignorePath) // #nosec G304 -- path is hardcoded
if err != nil {
return DoctorCheck{
Name: "Gitignore",
Status: "warning",
Message: ".beads/.gitignore not found",
Fix: "Run: bd init (safe to re-run) or bd doctor --fix",
}
}
// Check for required patterns
contentStr := string(content)
var missing []string
for _, pattern := range requiredPatterns {
if !strings.Contains(contentStr, pattern) {
missing = append(missing, pattern)
}
}
if len(missing) > 0 {
return DoctorCheck{
Name: "Gitignore",
Status: "warning",
Message: "Outdated .beads/.gitignore (missing merge artifact patterns)",
Detail: "Missing: " + strings.Join(missing, ", "),
Fix: "Run: bd doctor --fix or bd init (safe to re-run)",
}
}
return DoctorCheck{
Name: "Gitignore",
Status: "ok",
Message: "Up to date",
}
}
// FixGitignore updates .beads/.gitignore to the current template
func FixGitignore() error {
gitignorePath := filepath.Join(".beads", ".gitignore")
// Write canonical template
// #nosec G306 -- 0600 is appropriate for gitignore
if err := os.WriteFile(gitignorePath, []byte(GitignoreTemplate), 0600); err != nil {
return err
}
return nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/configfile"
@@ -175,43 +176,13 @@ With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite
return
}
// Create .gitignore in .beads directory
// Create/update .gitignore in .beads directory (idempotent - always update to latest)
gitignorePath := filepath.Join(localBeadsDir, ".gitignore")
gitignoreContent := `# SQLite databases
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
# Legacy database files
db.sqlite
bd.db
# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
beads.left.jsonl
beads.left.meta.json
beads.right.jsonl
beads.right.meta.json
# Keep JSONL exports and config (source of truth for git)
!issues.jsonl
!metadata.json
!config.json
`
if err := os.WriteFile(gitignorePath, []byte(gitignoreContent), 0600); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create .gitignore: %v\n", err)
// Non-fatal - continue anyway
}
if err := os.WriteFile(gitignorePath, []byte(doctor.GitignoreTemplate), 0600); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create/update .gitignore: %v\n", err)
// Non-fatal - continue anyway
}
}
// Ensure parent directory exists for the database
if err := os.MkdirAll(initDBDir, 0750); err != nil {