The dolt storage backend requires CGO due to its gozstd dependency. This change makes the dolt backend optional using build tags, allowing `go install` to work on Windows where CGO is disabled by default. Changes: - Add BackendFactory registration pattern to factory package - Create factory_dolt.go with `//go:build cgo` constraint that registers the dolt backend only when CGO is available - Update init.go to use factory instead of direct dolt import - When dolt backend is requested without CGO, provide helpful error message directing users to pre-built binaries The sqlite backend (default) works without CGO and covers the majority of use cases. Users who need dolt can either: 1. Use pre-built binaries from GitHub releases 2. Enable CGO by installing a C compiler Fixes #1116
774 lines
28 KiB
Go
774 lines
28 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"
|
|
"github.com/steveyegge/beads/internal/storage/factory"
|
|
"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")
|
|
backend, _ := cmd.Flags().GetString("backend")
|
|
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")
|
|
|
|
// Validate backend flag
|
|
if backend != "" && backend != configfile.BackendSQLite && backend != configfile.BackendDolt {
|
|
fmt.Fprintf(os.Stderr, "Error: invalid backend '%s' (must be 'sqlite' or 'dolt')\n", backend)
|
|
os.Exit(1)
|
|
}
|
|
if backend == "" {
|
|
backend = configfile.BackendSQLite // Default to SQLite
|
|
}
|
|
|
|
// 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 and the prefix
|
|
if err := createConfigYaml(beadsDir, true, prefix); 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
|
|
|
|
// Create storage backend based on --backend flag
|
|
var storagePath string
|
|
var store storage.Storage
|
|
if backend == configfile.BackendDolt {
|
|
// Dolt uses a directory, not a file
|
|
storagePath = filepath.Join(beadsDir, "dolt")
|
|
store, err = factory.New(ctx, backend, storagePath)
|
|
} else {
|
|
storagePath = initDBPath
|
|
store, err = sqlite.New(ctx, storagePath)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to create %s database: %v\n", backend, 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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save backend choice (only store if non-default to keep metadata.json clean)
|
|
if backend != configfile.BackendSQLite {
|
|
cfg.Backend = backend
|
|
}
|
|
|
|
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 (prefix is stored in DB, not config.yaml)
|
|
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(" Backend: %s\n", ui.RenderAccent(backend))
|
|
fmt.Printf(" Database: %s\n", ui.RenderAccent(storagePath))
|
|
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().String("backend", "", "Storage backend: sqlite (default) or dolt (version-controlled)")
|
|
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
|
|
}
|