Add check to prevent 'bd init' from running inside a git worktree.
Worktrees should share the .beads database from the main repository,
not create their own.
The error message guides users to:
1. Run 'bd init' from the main repository
2. Use 'bd worktree create' to create worktrees with proper
redirects
This prevents the confusing behavior where init would create files
in unexpected locations or fail with "pathspec '.beads/.gitignore' did
not match any files" errors.
According to docs/WORKTREES.md, the proper workflow is:
- Initialize beads once in the main repository
- Use 'bd worktree create' to create worktrees with redirect files
- All worktrees share the single .beads/ database via redirects
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
743 lines
26 KiB
Go
743 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"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"
|
|
"github.com/steveyegge/beads/internal/git"
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/syncbranch"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
"github.com/steveyegge/beads/internal/ui"
|
|
"github.com/steveyegge/beads/internal/utils"
|
|
)
|
|
|
|
var initCmd = &cobra.Command{
|
|
Use: "init",
|
|
GroupID: "setup",
|
|
Short: "Initialize bd in the current directory",
|
|
Long: `Initialize bd in the current directory by creating a .beads/ directory
|
|
and database file. Optionally specify a custom issue prefix.
|
|
|
|
With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite database.
|
|
|
|
With --from-jsonl: imports from the current .beads/issues.jsonl file on disk instead
|
|
of scanning git history. Use this after manual JSONL cleanup (e.g., bd compact --purge-tombstones)
|
|
to prevent deleted issues from being resurrected during re-initialization.
|
|
|
|
With --stealth: configures per-repository git settings for invisible beads usage:
|
|
• .git/info/exclude to prevent beads files from being committed
|
|
• Claude Code settings with bd onboard instruction
|
|
Perfect for personal use without affecting repo collaborators.`,
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
prefix, _ := cmd.Flags().GetString("prefix")
|
|
quiet, _ := cmd.Flags().GetBool("quiet")
|
|
branch, _ := cmd.Flags().GetString("branch")
|
|
contributor, _ := cmd.Flags().GetBool("contributor")
|
|
team, _ := cmd.Flags().GetBool("team")
|
|
stealth, _ := cmd.Flags().GetBool("stealth")
|
|
skipMergeDriver, _ := cmd.Flags().GetBool("skip-merge-driver")
|
|
skipHooks, _ := cmd.Flags().GetBool("skip-hooks")
|
|
force, _ := cmd.Flags().GetBool("force")
|
|
fromJSONL, _ := cmd.Flags().GetBool("from-jsonl")
|
|
|
|
// Initialize config (PersistentPreRun doesn't run for init command)
|
|
if err := config.Initialize(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to initialize config: %v\n", err)
|
|
// Non-fatal - continue with defaults
|
|
}
|
|
|
|
// Safety guard: check for existing JSONL with issues
|
|
// 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 {
|
|
fmt.Fprintf(os.Stderr, "Error setting up stealth mode: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// In stealth mode, skip git hooks and merge driver installation
|
|
// since we handle it globally
|
|
skipHooks = true
|
|
skipMergeDriver = true
|
|
}
|
|
|
|
// Check BEADS_DB environment variable if --db flag not set
|
|
// (PersistentPreRun doesn't run for init command)
|
|
if dbPath == "" {
|
|
if envDB := os.Getenv("BEADS_DB"); envDB != "" {
|
|
dbPath = envDB
|
|
}
|
|
}
|
|
|
|
// Determine prefix with precedence: flag > config > auto-detect from git > auto-detect from directory name
|
|
if prefix == "" {
|
|
// Try to get from config file
|
|
prefix = config.GetString("issue-prefix")
|
|
}
|
|
|
|
// auto-detect prefix from first issue in JSONL file
|
|
if prefix == "" {
|
|
issueCount, jsonlPath, gitRef := checkGitForIssues()
|
|
if issueCount > 0 {
|
|
firstIssue, err := readFirstIssueFromGit(jsonlPath, gitRef)
|
|
if firstIssue != nil && err == nil {
|
|
prefix = utils.ExtractIssuePrefix(firstIssue.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// auto-detect prefix from directory name
|
|
if prefix == "" {
|
|
// Auto-detect from directory name
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
prefix = filepath.Base(cwd)
|
|
}
|
|
|
|
// Normalize prefix: strip trailing hyphens
|
|
// The hyphen is added automatically during ID generation
|
|
prefix = strings.TrimRight(prefix, "-")
|
|
|
|
// Create database
|
|
// Use global dbPath if set via --db flag or BEADS_DB env var, otherwise default to .beads/beads.db
|
|
initDBPath := dbPath
|
|
if initDBPath == "" {
|
|
initDBPath = filepath.Join(".beads", beads.CanonicalDatabaseName)
|
|
}
|
|
|
|
// Migrate old database files if they exist
|
|
if err := migrateOldDatabases(initDBPath, quiet); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error during database migration: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Determine if we should create .beads/ directory in CWD or main repo root
|
|
// For worktrees, .beads should always be in the main repository root
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check if we're in a git worktree
|
|
// Guard with isGitRepo() check first - on Windows, git commands may hang
|
|
// when run outside a git repository (GH#727)
|
|
isWorktree := false
|
|
if isGitRepo() {
|
|
isWorktree = git.IsWorktree()
|
|
}
|
|
|
|
// Prevent initialization from within a worktree
|
|
if isWorktree {
|
|
mainRepoRoot, err := git.GetMainRepoRoot()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to get main repository root: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "Error: cannot run 'bd init' from within a git worktree\n\n")
|
|
fmt.Fprintf(os.Stderr, "Git worktrees share the .beads database from the main repository.\n")
|
|
fmt.Fprintf(os.Stderr, "To fix this:\n\n")
|
|
fmt.Fprintf(os.Stderr, " 1. Initialize beads in the main repository:\n")
|
|
fmt.Fprintf(os.Stderr, " cd %s\n", mainRepoRoot)
|
|
fmt.Fprintf(os.Stderr, " bd init\n\n")
|
|
fmt.Fprintf(os.Stderr, " 2. Then create worktrees with beads support:\n")
|
|
fmt.Fprintf(os.Stderr, " bd worktree create <path> --branch <branch-name>\n\n")
|
|
fmt.Fprintf(os.Stderr, "For more information, see: https://github.com/steveyegge/beads/blob/main/docs/WORKTREES.md\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
var beadsDir string
|
|
// For regular repos, use current directory
|
|
beadsDir = filepath.Join(cwd, ".beads")
|
|
|
|
// Prevent nested .beads directories
|
|
// Check if current working directory is inside a .beads directory
|
|
if strings.Contains(filepath.Clean(cwd), string(filepath.Separator)+".beads"+string(filepath.Separator)) ||
|
|
strings.HasSuffix(filepath.Clean(cwd), string(filepath.Separator)+".beads") {
|
|
fmt.Fprintf(os.Stderr, "Error: cannot initialize bd inside a .beads directory\n")
|
|
fmt.Fprintf(os.Stderr, "Current directory: %s\n", cwd)
|
|
fmt.Fprintf(os.Stderr, "Please run 'bd init' from outside the .beads directory.\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
initDBDir := filepath.Dir(initDBPath)
|
|
|
|
// Convert both to absolute paths for comparison
|
|
beadsDirAbs, err := filepath.Abs(beadsDir)
|
|
if err != nil {
|
|
beadsDirAbs = filepath.Clean(beadsDir)
|
|
}
|
|
initDBDirAbs, err := filepath.Abs(initDBDir)
|
|
if err != nil {
|
|
initDBDirAbs = filepath.Clean(initDBDir)
|
|
}
|
|
|
|
useLocalBeads := filepath.Clean(initDBDirAbs) == filepath.Clean(beadsDirAbs)
|
|
|
|
if useLocalBeads {
|
|
// Create .beads directory
|
|
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to create .beads directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Handle --no-db mode: create issues.jsonl file instead of database
|
|
if noDb {
|
|
// Create empty issues.jsonl file
|
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
|
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
|
// nolint:gosec // G306: JSONL file needs to be readable by other tools
|
|
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to create issues.jsonl: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Create empty interactions.jsonl file (append-only agent audit log)
|
|
interactionsPath := filepath.Join(beadsDir, "interactions.jsonl")
|
|
if _, err := os.Stat(interactionsPath); os.IsNotExist(err) {
|
|
// nolint:gosec // G306: JSONL file needs to be readable by other tools
|
|
if err := os.WriteFile(interactionsPath, []byte{}, 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to create interactions.jsonl: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Create metadata.json for --no-db mode
|
|
cfg := configfile.DefaultConfig()
|
|
if err := cfg.Save(beadsDir); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to create metadata.json: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
|
|
// Create config.yaml with no-db: true
|
|
if err := createConfigYaml(beadsDir, true); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to create config.yaml: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
|
|
// Create README.md
|
|
if err := createReadme(beadsDir); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to create README.md: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
|
|
if !quiet {
|
|
fmt.Printf("\n%s bd initialized successfully in --no-db mode!\n\n", ui.RenderPass("✓"))
|
|
fmt.Printf(" Mode: %s\n", ui.RenderAccent("no-db (JSONL-only)"))
|
|
fmt.Printf(" Issues file: %s\n", ui.RenderAccent(jsonlPath))
|
|
fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
|
|
fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
|
|
fmt.Printf("Run %s to get started.\n\n", ui.RenderAccent("bd --no-db quickstart"))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Create/update .gitignore in .beads directory (idempotent - always update to latest)
|
|
gitignorePath := filepath.Join(beadsDir, ".gitignore")
|
|
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 interactions.jsonl exists (append-only agent audit log)
|
|
interactionsPath := filepath.Join(beadsDir, "interactions.jsonl")
|
|
if _, err := os.Stat(interactionsPath); os.IsNotExist(err) {
|
|
// nolint:gosec // G306: JSONL file needs to be readable by other tools
|
|
if err := os.WriteFile(interactionsPath, []byte{}, 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to create interactions.jsonl: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure parent directory exists for the database
|
|
if err := os.MkdirAll(initDBDir, 0750); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to create database directory %s: %v\n", initDBDir, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx := rootCtx
|
|
store, err := sqlite.New(ctx, initDBPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to create database: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// === CONFIGURATION METADATA (Pattern A: Fatal) ===
|
|
// Configuration metadata is essential for core functionality and must succeed.
|
|
// These settings define fundamental behavior (issue IDs, sync workflow).
|
|
// Failure here indicates a serious problem that prevents normal operation.
|
|
|
|
// Set the issue prefix in config
|
|
if err := store.SetConfig(ctx, "issue_prefix", prefix); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to set issue prefix: %v\n", err)
|
|
_ = store.Close()
|
|
os.Exit(1)
|
|
}
|
|
|
|
// === TRACKING METADATA (Pattern B: Warn and Continue) ===
|
|
// Tracking metadata enhances functionality (diagnostics, version checks, collision detection)
|
|
// but the system works without it. Failures here degrade gracefully - we warn but continue.
|
|
// Examples: bd_version enables upgrade warnings, repo_id/clone_id help with collision detection.
|
|
|
|
// Store the bd version in metadata (for version mismatch detection)
|
|
if err := store.SetMetadata(ctx, "bd_version", Version); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to store version metadata: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
|
|
// Compute and store repository fingerprint
|
|
repoID, err := beads.ComputeRepoID()
|
|
if err != nil {
|
|
if !quiet {
|
|
fmt.Fprintf(os.Stderr, "Warning: could not compute repository ID: %v\n", err)
|
|
}
|
|
} else {
|
|
if err := store.SetMetadata(ctx, "repo_id", repoID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to set repo_id: %v\n", err)
|
|
} else if !quiet {
|
|
fmt.Printf(" Repository ID: %s\n", repoID[:8])
|
|
}
|
|
}
|
|
|
|
// Store clone-specific ID
|
|
cloneID, err := beads.GetCloneID()
|
|
if err != nil {
|
|
if !quiet {
|
|
fmt.Fprintf(os.Stderr, "Warning: could not compute clone ID: %v\n", err)
|
|
}
|
|
} else {
|
|
if err := store.SetMetadata(ctx, "clone_id", cloneID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to set clone_id: %v\n", err)
|
|
} else if !quiet {
|
|
fmt.Printf(" Clone ID: %s\n", cloneID)
|
|
}
|
|
}
|
|
|
|
// Create or preserve metadata.json for database metadata (bd-zai fix)
|
|
if useLocalBeads {
|
|
// First, check if metadata.json already exists
|
|
existingCfg, err := configfile.Load(beadsDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to load existing metadata.json: %v\n", err)
|
|
}
|
|
|
|
var cfg *configfile.Config
|
|
if existingCfg != nil {
|
|
// Preserve existing config
|
|
cfg = existingCfg
|
|
} else {
|
|
// Create new config, detecting JSONL filename from existing files
|
|
cfg = configfile.DefaultConfig()
|
|
// Check if beads.jsonl exists but issues.jsonl doesn't (legacy)
|
|
issuesPath := filepath.Join(beadsDir, "issues.jsonl")
|
|
beadsPath := filepath.Join(beadsDir, "beads.jsonl")
|
|
if _, err := os.Stat(beadsPath); err == nil {
|
|
if _, err := os.Stat(issuesPath); os.IsNotExist(err) {
|
|
cfg.JSONLExport = "beads.jsonl" // Legacy filename
|
|
}
|
|
}
|
|
}
|
|
if err := cfg.Save(beadsDir); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to create metadata.json: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
|
|
// Create config.yaml template
|
|
if err := createConfigYaml(beadsDir, false); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to create config.yaml: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
|
|
// Create README.md
|
|
if err := createReadme(beadsDir); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to create README.md: %v\n", err)
|
|
// Non-fatal - continue anyway
|
|
}
|
|
}
|
|
|
|
// Set sync.branch only if explicitly specified via --branch flag
|
|
// GH#807: Do NOT auto-detect current branch - if sync.branch is set to main/master,
|
|
// the worktree created by bd sync will check out main, preventing the user from
|
|
// checking out main in their working directory (git error: "'main' is already checked out")
|
|
//
|
|
// When --branch is not specified, bd sync will commit directly to the current branch
|
|
// (the original behavior before sync branch feature)
|
|
//
|
|
// GH#927: This must run AFTER createConfigYaml() so that config.yaml exists
|
|
// and syncbranch.Set() can update it via config.SetYamlConfig() (PR#910 mechanism)
|
|
if branch != "" {
|
|
if err := syncbranch.Set(ctx, store, branch); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to set sync branch: %v\n", err)
|
|
_ = store.Close()
|
|
os.Exit(1)
|
|
}
|
|
if !quiet {
|
|
fmt.Printf(" Sync branch: %s\n", branch)
|
|
}
|
|
}
|
|
|
|
// Check if git has existing issues to import (fresh clone scenario)
|
|
// With --from-jsonl: import from local file instead of git history
|
|
if fromJSONL {
|
|
// Import from current working tree's JSONL file
|
|
localJSONLPath := filepath.Join(beadsDir, "issues.jsonl")
|
|
if _, err := os.Stat(localJSONLPath); err == nil {
|
|
issueCount, err := importFromLocalJSONL(ctx, initDBPath, store, localJSONLPath)
|
|
if err != nil {
|
|
if !quiet {
|
|
fmt.Fprintf(os.Stderr, "Warning: import from local JSONL failed: %v\n", err)
|
|
}
|
|
// Non-fatal - continue with empty database
|
|
} else if !quiet && issueCount > 0 {
|
|
fmt.Fprintf(os.Stderr, "✓ Imported %d issues from local %s\n\n", issueCount, localJSONLPath)
|
|
}
|
|
} else if !quiet {
|
|
fmt.Fprintf(os.Stderr, "Warning: --from-jsonl specified but %s not found\n", localJSONLPath)
|
|
}
|
|
} else {
|
|
// Default: import from git history
|
|
issueCount, jsonlPath, gitRef := checkGitForIssues()
|
|
if issueCount > 0 {
|
|
if !quiet {
|
|
fmt.Fprintf(os.Stderr, "\n✓ Database initialized. Found %d issues in git, importing...\n", issueCount)
|
|
}
|
|
|
|
if err := importFromGit(ctx, initDBPath, store, jsonlPath, gitRef); err != nil {
|
|
if !quiet {
|
|
fmt.Fprintf(os.Stderr, "Warning: auto-import failed: %v\n", err)
|
|
fmt.Fprintf(os.Stderr, "Try manually: git show %s:%s | bd import -i /dev/stdin\n", gitRef, jsonlPath)
|
|
}
|
|
// Non-fatal - continue with empty database
|
|
} else if !quiet {
|
|
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run contributor wizard if --contributor flag is set
|
|
if contributor {
|
|
if err := runContributorWizard(ctx, store); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error running contributor wizard: %v\n", err)
|
|
_ = store.Close()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Run team wizard if --team flag is set
|
|
if team {
|
|
if err := runTeamWizard(ctx, store); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error running team wizard: %v\n", err)
|
|
_ = store.Close()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if err := store.Close(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
|
|
}
|
|
|
|
// Fork detection: offer to configure .git/info/exclude (GH#742)
|
|
setupExclude, _ := cmd.Flags().GetBool("setup-exclude")
|
|
if setupExclude {
|
|
// Manual flag - always configure
|
|
if err := setupForkExclude(!quiet); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to configure git exclude: %v\n", err)
|
|
}
|
|
} else if !stealth && isGitRepo() {
|
|
// Auto-detect fork and prompt (skip if stealth - it handles exclude already)
|
|
if isFork, upstreamURL := detectForkSetup(); isFork {
|
|
if promptForkExclude(upstreamURL, quiet) {
|
|
if err := setupForkExclude(!quiet); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to configure git exclude: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we're in a git repo and hooks aren't installed
|
|
// Install by default unless --skip-hooks is passed
|
|
if !skipHooks && isGitRepo() && !hooksInstalled() {
|
|
if err := installGitHooks(); err != nil && !quiet {
|
|
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", ui.RenderWarn("⚠"), err)
|
|
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
|
|
}
|
|
}
|
|
|
|
// Check if we're in a git repo and merge driver isn't configured
|
|
// Install by default unless --skip-merge-driver is passed
|
|
if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() {
|
|
if err := installMergeDriver(); err != nil && !quiet {
|
|
fmt.Fprintf(os.Stderr, "\n%s Failed to install merge driver: %v\n", ui.RenderWarn("⚠"), err)
|
|
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
|
|
}
|
|
}
|
|
|
|
// Add "landing the plane" instructions to AGENTS.md and @AGENTS.md
|
|
// Skip in stealth mode (user wants invisible setup) and quiet mode (suppress all output)
|
|
if !stealth {
|
|
addLandingThePlaneInstructions(!quiet)
|
|
}
|
|
|
|
// Skip output if quiet mode
|
|
if quiet {
|
|
return
|
|
}
|
|
|
|
fmt.Printf("\n%s bd initialized successfully!\n\n", ui.RenderPass("✓"))
|
|
fmt.Printf(" Database: %s\n", ui.RenderAccent(initDBPath))
|
|
fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
|
|
fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
|
|
fmt.Printf("Run %s to get started.\n\n", ui.RenderAccent("bd quickstart"))
|
|
|
|
// Run bd doctor diagnostics to catch setup issues early
|
|
doctorResult := runDiagnostics(cwd)
|
|
// Check if there are any warnings or errors (not just critical failures)
|
|
hasIssues := false
|
|
for _, check := range doctorResult.Checks {
|
|
if check.Status != statusOK {
|
|
hasIssues = true
|
|
break
|
|
}
|
|
}
|
|
if hasIssues {
|
|
fmt.Printf("%s Setup incomplete. Some issues were detected:\n", ui.RenderWarn("⚠"))
|
|
// Show just the warnings/errors, not all checks
|
|
for _, check := range doctorResult.Checks {
|
|
if check.Status != statusOK {
|
|
fmt.Printf(" • %s: %s\n", check.Name, check.Message)
|
|
}
|
|
}
|
|
fmt.Printf("\nRun %s to see details and fix these issues.\n\n", ui.RenderAccent("bd doctor --fix"))
|
|
}
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
initCmd.Flags().StringP("prefix", "p", "", "Issue prefix (default: current directory name)")
|
|
initCmd.Flags().BoolP("quiet", "q", false, "Suppress output (quiet mode)")
|
|
initCmd.Flags().StringP("branch", "b", "", "Git branch for beads commits (default: current branch)")
|
|
initCmd.Flags().Bool("contributor", false, "Run OSS contributor setup wizard")
|
|
initCmd.Flags().Bool("team", false, "Run team workflow setup wizard")
|
|
initCmd.Flags().Bool("stealth", false, "Enable stealth mode: global gitattributes and gitignore, no local repo tracking")
|
|
initCmd.Flags().Bool("setup-exclude", false, "Configure .git/info/exclude to keep beads files local (for forks)")
|
|
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)")
|
|
initCmd.Flags().Bool("from-jsonl", false, "Import from current .beads/issues.jsonl file instead of git history (preserves manual cleanups)")
|
|
rootCmd.AddCommand(initCmd)
|
|
}
|
|
|
|
// migrateOldDatabases detects and migrates old database files to beads.db
|
|
func migrateOldDatabases(targetPath string, quiet bool) error {
|
|
targetDir := filepath.Dir(targetPath)
|
|
targetName := filepath.Base(targetPath)
|
|
|
|
// If target already exists, no migration needed
|
|
if _, err := os.Stat(targetPath); err == nil {
|
|
return nil
|
|
}
|
|
|
|
// Create .beads directory if it doesn't exist
|
|
if err := os.MkdirAll(targetDir, 0750); err != nil {
|
|
return fmt.Errorf("failed to create .beads directory: %w", err)
|
|
}
|
|
|
|
// Look for existing .db files in the .beads directory
|
|
pattern := filepath.Join(targetDir, "*.db")
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to search for existing databases: %w", err)
|
|
}
|
|
|
|
// Filter out the target file name and any backup files
|
|
var oldDBs []string
|
|
for _, match := range matches {
|
|
baseName := filepath.Base(match)
|
|
if baseName != targetName && !strings.HasSuffix(baseName, ".backup.db") {
|
|
oldDBs = append(oldDBs, match)
|
|
}
|
|
}
|
|
|
|
if len(oldDBs) == 0 {
|
|
// No old databases to migrate
|
|
return nil
|
|
}
|
|
|
|
if len(oldDBs) > 1 {
|
|
// Multiple databases found - ambiguous, require manual intervention
|
|
return fmt.Errorf("multiple database files found in %s: %v\nPlease manually rename the correct database to %s and remove others",
|
|
targetDir, oldDBs, targetName)
|
|
}
|
|
|
|
// Migrate the single old database
|
|
oldDB := oldDBs[0]
|
|
if !quiet {
|
|
fmt.Fprintf(os.Stderr, "→ Migrating database: %s → %s\n", filepath.Base(oldDB), targetName)
|
|
}
|
|
|
|
// Rename the old database to the new canonical name
|
|
if err := os.Rename(oldDB, targetPath); err != nil {
|
|
return fmt.Errorf("failed to migrate database %s to %s: %w", oldDB, targetPath, err)
|
|
}
|
|
|
|
if !quiet {
|
|
fmt.Fprintf(os.Stderr, "✓ Database migration complete\n\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
|
|
// readFirstIssueFromJSONL reads the first issue from a JSONL file
|
|
func readFirstIssueFromJSONL(path string) (*types.Issue, error) {
|
|
// #nosec G304 -- helper reads JSONL file chosen by current bd command
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open JSONL file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
lineNum := 0
|
|
for scanner.Scan() {
|
|
lineNum++
|
|
line := scanner.Text()
|
|
|
|
// skip empty lines
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
var issue types.Issue
|
|
if err := json.Unmarshal([]byte(line), &issue); err == nil {
|
|
return &issue, nil
|
|
} else {
|
|
// Skip malformed lines with warning
|
|
fmt.Fprintf(os.Stderr, "Warning: skipping malformed JSONL line %d: %v\n", lineNum, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error reading JSONL file: %w", err)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// readFirstIssueFromGit reads the first issue from a git ref (bd-0is: supports sync-branch)
|
|
func readFirstIssueFromGit(jsonlPath, gitRef string) (*types.Issue, error) {
|
|
output, err := readFromGitRef(jsonlPath, gitRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(output))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// skip empty lines
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
var issue types.Issue
|
|
if err := json.Unmarshal([]byte(line), &issue); err == nil {
|
|
return &issue, nil
|
|
}
|
|
// Skip malformed lines silently (called during auto-detection)
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error scanning git content: %w", err)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
|
|
// checkExistingBeadsData checks for existing database files
|
|
// and returns an error if found (safety guard for bd-emg)
|
|
//
|
|
// Note: This only blocks when a database already exists (workspace is initialized).
|
|
// Fresh clones with JSONL but no database are allowed - init will create the database
|
|
// and import from JSONL automatically (bd-4h9: fixes circular dependency with doctor --fix).
|
|
//
|
|
// For worktrees, checks the main repository root instead of current directory
|
|
// since worktrees should share the database with the main repository.
|
|
func checkExistingBeadsData(prefix string) error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil // Can't determine CWD, allow init to proceed
|
|
}
|
|
|
|
// Determine where to check for .beads directory
|
|
// Guard with isGitRepo() check first - on Windows, git commands may hang
|
|
// when run outside a git repository (GH#727)
|
|
var beadsDir string
|
|
if isGitRepo() && git.IsWorktree() {
|
|
// For worktrees, .beads should be in the main repository root
|
|
mainRepoRoot, err := git.GetMainRepoRoot()
|
|
if err != nil {
|
|
return nil // Can't determine main repo root, allow init to proceed
|
|
}
|
|
beadsDir = filepath.Join(mainRepoRoot, ".beads")
|
|
} else {
|
|
// For regular repos (or non-git directories), check current directory
|
|
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 {
|
|
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.`, ui.RenderWarn("⚠"), dbPath, ui.RenderAccent("bd list"), prefix)
|
|
}
|
|
|
|
// Fresh clones (JSONL exists but no database) are allowed - init will
|
|
// create the database and import from JSONL automatically.
|
|
// This fixes the circular dependency where init told users to run
|
|
// "bd doctor --fix" but doctor couldn't create a database (bd-4h9).
|
|
|
|
return nil // No database found, safe to init
|
|
}
|