* 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:
15
.beads/.gitignore
vendored
15
.beads/.gitignore
vendored
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
104
cmd/bd/doctor/gitignore.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user