fix(init): add safety guard to prevent overwriting existing data

bd init now refuses when:
- JSONL file exists with >0 issues (fresh clone scenario)
- Database file already exists (already initialized)

Suggests `bd doctor --fix` for fresh clones and provides clear guidance.
Added --force flag to bypass the safety guard when needed.

Closes: bd-emg

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-28 22:15:13 -08:00
parent a34c56c43a
commit 3a4840169d
3 changed files with 197 additions and 78 deletions

View File

@@ -43,6 +43,7 @@ With --stealth: configures global git settings for invisible beads usage:
stealth, _ := cmd.Flags().GetBool("stealth")
skipMergeDriver, _ := cmd.Flags().GetBool("skip-merge-driver")
skipHooks, _ := cmd.Flags().GetBool("skip-hooks")
force, _ := cmd.Flags().GetBool("force")
// Initialize config (PersistentPreRun doesn't run for init command)
if err := config.Initialize(); err != nil {
@@ -50,6 +51,15 @@ With --stealth: configures global git settings for invisible beads usage:
// Non-fatal - continue with defaults
}
// Safety guard: check for existing JSONL with issues (bd-emg)
// This prevents accidental re-initialization in fresh clones
if !force {
if err := checkExistingBeadsData(prefix); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
// Handle stealth mode setup
if stealth {
if err := setupStealthMode(!quiet); err != nil {
@@ -443,6 +453,7 @@ func init() {
initCmd.Flags().Bool("stealth", false, "Enable stealth mode: global gitattributes and gitignore, no local repo tracking")
initCmd.Flags().Bool("skip-hooks", false, "Skip git hooks installation")
initCmd.Flags().Bool("skip-merge-driver", false, "Skip git merge driver setup")
initCmd.Flags().Bool("force", false, "Force re-initialization even if JSONL already has issues (may cause data loss)")
rootCmd.AddCommand(initCmd)
}
@@ -1309,6 +1320,113 @@ func setupGlobalGitIgnore(homeDir string, verbose bool) error {
return nil
}
// checkExistingBeadsData checks for existing JSONL or database files
// and returns an error if found (safety guard for bd-emg)
func checkExistingBeadsData(prefix string) error {
cwd, err := os.Getwd()
if err != nil {
return nil // Can't determine CWD, allow init to proceed
}
beadsDir := filepath.Join(cwd, ".beads")
// Check if .beads directory exists
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
return nil // No .beads directory, safe to init
}
// 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
This workspace is already initialized.
To use the existing database:
Just run bd commands normally (e.g., %s)
To completely reinitialize (data loss warning):
rm -rf .beads && bd init --prefix %s
Aborting.`, yellow("⚠"), dbPath, cyan("bd list"), prefix)
}
// Check for existing JSONL files with issues
jsonlCandidates := []string{
filepath.Join(beadsDir, "issues.jsonl"),
filepath.Join(beadsDir, "beads.jsonl"),
}
for _, jsonlPath := range jsonlCandidates {
if _, err := os.Stat(jsonlPath); err == nil {
// JSONL exists, count issues
issueCount := countIssuesInJSONLFile(jsonlPath)
if issueCount > 0 {
yellow := color.New(color.FgYellow).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
prefixHint := prefix
if prefixHint == "" {
prefixHint = "<prefix>"
}
return fmt.Errorf(`
%s Found existing %s with %d issues.
This appears to be a fresh clone, not a new project.
To hydrate the database from existing JSONL:
%s
To force re-initialization (may cause data loss):
bd init --prefix %s --force
Aborting.`, yellow("⚠"), filepath.Base(jsonlPath), issueCount, cyan("bd doctor --fix"), prefixHint)
}
// Empty JSONL (0 issues) is OK - likely a new project
}
}
return nil // No existing data found, safe to init
}
// countIssuesInJSONLFile counts valid issue lines in a JSONL file
func countIssuesInJSONLFile(path string) int {
// #nosec G304 -- helper reads JSONL file in .beads directory
file, err := os.Open(path)
if err != nil {
return 0
}
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
// Use larger buffer for potentially large JSONL lines
scanner.Buffer(make([]byte, 0, 64*1024), 10*1024*1024)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
// Quick validation: check if it's valid JSON with an "id" field
var issue struct {
ID string `json:"id"`
}
if err := json.Unmarshal([]byte(line), &issue); err == nil && issue.ID != "" {
count++
}
}
return count
}
// setupClaudeSettings creates or updates .claude/settings.local.json with onboard instruction
func setupClaudeSettings(verbose bool) error {
claudeDir := ".claude"