refactor: Split init.go from 1928 to 705 lines (bd-xtf5)
Extract init.go into multiple focused files: - init_git_hooks.go: Git hooks installation and merge driver setup (~480 lines) - init_stealth.go: Stealth mode and fork/exclude configuration (~310 lines) - init_agent.go: AGENTS.md and Claude settings setup (~170 lines) - init_templates.go: config.yaml and README.md templates (~180 lines) This reduces init.go from 1928 lines to 705 lines, meeting the <800 line target. Each extracted file handles a cohesive set of functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1223
cmd/bd/init.go
1223
cmd/bd/init.go
File diff suppressed because it is too large
Load Diff
182
cmd/bd/init_agent.go
Normal file
182
cmd/bd/init_agent.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// landingThePlaneSection is the "landing the plane" instructions for AI agents
|
||||
// This gets appended to AGENTS.md and @AGENTS.md during bd init
|
||||
const landingThePlaneSection = `
|
||||
## Landing the Plane (Session Completion)
|
||||
|
||||
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until ` + "`git push`" + ` succeeds.
|
||||
|
||||
**MANDATORY WORKFLOW:**
|
||||
|
||||
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
||||
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||||
3. **Update issue status** - Close finished work, update in-progress items
|
||||
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||
` + "```bash" + `
|
||||
git pull --rebase
|
||||
bd sync
|
||||
git push
|
||||
git status # MUST show "up to date with origin"
|
||||
` + "```" + `
|
||||
5. **Clean up** - Clear stashes, prune remote branches
|
||||
6. **Verify** - All changes committed AND pushed
|
||||
7. **Hand off** - Provide context for next session
|
||||
|
||||
**CRITICAL RULES:**
|
||||
- Work is NOT complete until ` + "`git push`" + ` succeeds
|
||||
- NEVER stop before pushing - that leaves work stranded locally
|
||||
- NEVER say "ready to push when you are" - YOU must push
|
||||
- If push fails, resolve and retry until it succeeds
|
||||
`
|
||||
|
||||
// addLandingThePlaneInstructions adds "landing the plane" instructions to AGENTS.md
|
||||
func addLandingThePlaneInstructions(verbose bool) {
|
||||
// File to update (AGENTS.md is the standard comprehensive documentation file)
|
||||
agentFile := "AGENTS.md"
|
||||
|
||||
if err := updateAgentFile(agentFile, verbose); err != nil {
|
||||
// Non-fatal - continue with other files
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to update %s: %v\n", agentFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateAgentFile creates or updates an agent instructions file with landing the plane section
|
||||
func updateAgentFile(filename string, verbose bool) error {
|
||||
// Check if file exists
|
||||
//nolint:gosec // G304: filename comes from hardcoded list in addLandingThePlaneInstructions
|
||||
content, err := os.ReadFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
// File doesn't exist - create it with basic structure
|
||||
newContent := fmt.Sprintf(`# Agent Instructions
|
||||
|
||||
This project uses **bd** (beads) for issue tracking. Run `+"`bd onboard`"+` to get started.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
`+"```bash"+`
|
||||
bd ready # Find available work
|
||||
bd show <id> # View issue details
|
||||
bd update <id> --status in_progress # Claim work
|
||||
bd close <id> # Complete work
|
||||
bd sync # Sync with git
|
||||
`+"```"+`
|
||||
%s
|
||||
`, landingThePlaneSection)
|
||||
|
||||
// #nosec G306 - markdown needs to be readable
|
||||
if err := os.WriteFile(filename, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", filename, err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf(" %s Created %s with landing-the-plane instructions\n", ui.RenderPass("✓"), filename)
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", filename, err)
|
||||
}
|
||||
|
||||
// File exists - check if it already has landing the plane section
|
||||
if strings.Contains(string(content), "Landing the Plane") {
|
||||
if verbose {
|
||||
fmt.Printf(" %s already has landing-the-plane instructions\n", filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Append the landing the plane section
|
||||
newContent := string(content)
|
||||
if !strings.HasSuffix(newContent, "\n") {
|
||||
newContent += "\n"
|
||||
}
|
||||
newContent += landingThePlaneSection
|
||||
|
||||
// #nosec G306 - markdown needs to be readable
|
||||
if err := os.WriteFile(filename, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", filename, err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf(" %s Added landing-the-plane instructions to %s\n", ui.RenderPass("✓"), filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupClaudeSettings creates or updates .claude/settings.local.json with onboard instruction
|
||||
func setupClaudeSettings(verbose bool) error {
|
||||
claudeDir := ".claude"
|
||||
settingsPath := filepath.Join(claudeDir, "settings.local.json")
|
||||
|
||||
// Create .claude directory if it doesn't exist
|
||||
if err := os.MkdirAll(claudeDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create .claude directory: %w", err)
|
||||
}
|
||||
|
||||
// Check if settings.local.json already exists
|
||||
var existingSettings map[string]interface{}
|
||||
// #nosec G304 - user config path
|
||||
if content, err := os.ReadFile(settingsPath); err == nil {
|
||||
if err := json.Unmarshal(content, &existingSettings); err != nil {
|
||||
// Don't silently overwrite - the user has a file with invalid JSON
|
||||
// that likely contains important settings they don't want to lose
|
||||
return fmt.Errorf("existing %s contains invalid JSON: %w\nPlease fix the JSON syntax manually before running bd init", settingsPath, err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
// File exists but couldn't be read (permissions issue, etc.)
|
||||
return fmt.Errorf("failed to read existing %s: %w", settingsPath, err)
|
||||
} else {
|
||||
// File doesn't exist - create new empty settings
|
||||
existingSettings = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Add or update the prompt with onboard instruction
|
||||
onboardPrompt := "Before starting any work, run 'bd onboard' to understand the current project state and available issues."
|
||||
|
||||
// Check if prompt already contains onboard instruction
|
||||
if promptValue, exists := existingSettings["prompt"]; exists {
|
||||
if promptStr, ok := promptValue.(string); ok {
|
||||
if strings.Contains(promptStr, "bd onboard") {
|
||||
if verbose {
|
||||
fmt.Printf("Claude settings already configured with bd onboard instruction\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Update existing prompt to include onboard instruction
|
||||
existingSettings["prompt"] = promptStr + "\n\n" + onboardPrompt
|
||||
} else {
|
||||
// Existing prompt is not a string, replace it
|
||||
existingSettings["prompt"] = onboardPrompt
|
||||
}
|
||||
} else {
|
||||
// Add new prompt with onboard instruction
|
||||
existingSettings["prompt"] = onboardPrompt
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
updatedContent, err := json.MarshalIndent(existingSettings, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal settings JSON: %w", err)
|
||||
}
|
||||
|
||||
// #nosec G306 - config file needs 0644
|
||||
if err := os.WriteFile(settingsPath, updatedContent, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write claude settings: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Configured Claude settings with bd onboard instruction\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
476
cmd/bd/init_git_hooks.go
Normal file
476
cmd/bd/init_git_hooks.go
Normal file
@@ -0,0 +1,476 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/git"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// hooksInstalled checks if bd git hooks are installed
|
||||
func hooksInstalled() bool {
|
||||
gitDir, err := git.GetGitDir()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
preCommit := filepath.Join(gitDir, "hooks", "pre-commit")
|
||||
postMerge := filepath.Join(gitDir, "hooks", "post-merge")
|
||||
|
||||
// Check if both hooks exist
|
||||
_, err1 := os.Stat(preCommit)
|
||||
_, err2 := os.Stat(postMerge)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify they're bd hooks by checking for signature comment
|
||||
// #nosec G304 - controlled path from git directory
|
||||
preCommitContent, err := os.ReadFile(preCommit)
|
||||
if err != nil || !strings.Contains(string(preCommitContent), "bd (beads) pre-commit hook") {
|
||||
return false
|
||||
}
|
||||
|
||||
// #nosec G304 - controlled path from git directory
|
||||
postMergeContent, err := os.ReadFile(postMerge)
|
||||
if err != nil || !strings.Contains(string(postMergeContent), "bd (beads) post-merge hook") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify hooks are executable
|
||||
preCommitInfo, err := os.Stat(preCommit)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if preCommitInfo.Mode().Perm()&0111 == 0 {
|
||||
return false // Not executable
|
||||
}
|
||||
|
||||
postMergeInfo, err := os.Stat(postMerge)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if postMergeInfo.Mode().Perm()&0111 == 0 {
|
||||
return false // Not executable
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// hookInfo contains information about an existing hook
|
||||
type hookInfo struct {
|
||||
name string
|
||||
path string
|
||||
exists bool
|
||||
isBdHook bool
|
||||
isPreCommit bool
|
||||
content string
|
||||
}
|
||||
|
||||
// detectExistingHooks scans for existing git hooks
|
||||
func detectExistingHooks() []hookInfo {
|
||||
gitDir, err := git.GetGitDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
hooksDir := filepath.Join(gitDir, "hooks")
|
||||
hooks := []hookInfo{
|
||||
{name: "pre-commit", path: filepath.Join(hooksDir, "pre-commit")},
|
||||
{name: "post-merge", path: filepath.Join(hooksDir, "post-merge")},
|
||||
{name: "pre-push", path: filepath.Join(hooksDir, "pre-push")},
|
||||
}
|
||||
|
||||
for i := range hooks {
|
||||
content, err := os.ReadFile(hooks[i].path)
|
||||
if err == nil {
|
||||
hooks[i].exists = true
|
||||
hooks[i].content = string(content)
|
||||
hooks[i].isBdHook = strings.Contains(hooks[i].content, "bd (beads)")
|
||||
// Only detect pre-commit framework if not a bd hook
|
||||
if !hooks[i].isBdHook {
|
||||
hooks[i].isPreCommit = strings.Contains(hooks[i].content, "pre-commit run") ||
|
||||
strings.Contains(hooks[i].content, ".pre-commit-config")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hooks
|
||||
}
|
||||
|
||||
// promptHookAction asks user what to do with existing hooks
|
||||
func promptHookAction(existingHooks []hookInfo) string {
|
||||
fmt.Printf("\n%s Found existing git hooks:\n", ui.RenderWarn("⚠"))
|
||||
for _, hook := range existingHooks {
|
||||
if hook.exists && !hook.isBdHook {
|
||||
hookType := "custom script"
|
||||
if hook.isPreCommit {
|
||||
hookType = "pre-commit framework"
|
||||
}
|
||||
fmt.Printf(" - %s (%s)\n", hook.name, hookType)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nHow should bd proceed?\n")
|
||||
fmt.Printf(" [1] Chain with existing hooks (recommended)\n")
|
||||
fmt.Printf(" [2] Overwrite existing hooks\n")
|
||||
fmt.Printf(" [3] Skip git hooks installation\n")
|
||||
fmt.Printf("Choice [1-3]: ")
|
||||
|
||||
var response string
|
||||
_, _ = fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// installGitHooks installs git hooks inline (no external dependencies)
|
||||
func installGitHooks() error {
|
||||
gitDir, err := git.GetGitDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hooksDir := filepath.Join(gitDir, "hooks")
|
||||
|
||||
// Ensure hooks directory exists
|
||||
if err := os.MkdirAll(hooksDir, 0750); err != nil {
|
||||
return fmt.Errorf("failed to create hooks directory: %w", err)
|
||||
}
|
||||
|
||||
// Detect existing hooks
|
||||
existingHooks := detectExistingHooks()
|
||||
|
||||
// Check if any non-bd hooks exist
|
||||
hasExistingHooks := false
|
||||
for _, hook := range existingHooks {
|
||||
if hook.exists && !hook.isBdHook {
|
||||
hasExistingHooks = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Determine installation mode
|
||||
chainHooks := false
|
||||
if hasExistingHooks {
|
||||
choice := promptHookAction(existingHooks)
|
||||
switch choice {
|
||||
case "1", "":
|
||||
chainHooks = true
|
||||
case "2":
|
||||
// Overwrite mode - backup existing hooks
|
||||
for _, hook := range existingHooks {
|
||||
if hook.exists && !hook.isBdHook {
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
backup := hook.path + ".backup-" + timestamp
|
||||
if err := os.Rename(hook.path, backup); err != nil {
|
||||
return fmt.Errorf("failed to backup %s: %w", hook.name, err)
|
||||
}
|
||||
fmt.Printf(" Backed up %s to %s\n", hook.name, filepath.Base(backup))
|
||||
}
|
||||
}
|
||||
case "3":
|
||||
fmt.Printf("Skipping git hooks installation.\n")
|
||||
fmt.Printf("You can install manually later with: %s\n", ui.RenderAccent("./examples/git-hooks/install.sh"))
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid choice: %s", choice)
|
||||
}
|
||||
}
|
||||
|
||||
// pre-commit hook
|
||||
preCommitPath := filepath.Join(hooksDir, "pre-commit")
|
||||
preCommitContent := buildPreCommitHook(chainHooks, existingHooks)
|
||||
|
||||
// post-merge hook
|
||||
postMergePath := filepath.Join(hooksDir, "post-merge")
|
||||
postMergeContent := buildPostMergeHook(chainHooks, existingHooks)
|
||||
|
||||
// Write pre-commit hook (executable scripts need 0700)
|
||||
// #nosec G306 - git hooks must be executable
|
||||
if err := os.WriteFile(preCommitPath, []byte(preCommitContent), 0700); err != nil {
|
||||
return fmt.Errorf("failed to write pre-commit hook: %w", err)
|
||||
}
|
||||
|
||||
// Write post-merge hook (executable scripts need 0700)
|
||||
// #nosec G306 - git hooks must be executable
|
||||
if err := os.WriteFile(postMergePath, []byte(postMergeContent), 0700); err != nil {
|
||||
return fmt.Errorf("failed to write post-merge hook: %w", err)
|
||||
}
|
||||
|
||||
if chainHooks {
|
||||
fmt.Printf("%s Chained bd hooks with existing hooks\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildPreCommitHook generates the pre-commit hook content
|
||||
func buildPreCommitHook(chainHooks bool, existingHooks []hookInfo) string {
|
||||
if chainHooks {
|
||||
// Find existing pre-commit hook
|
||||
var existingPreCommit string
|
||||
for _, hook := range existingHooks {
|
||||
if hook.name == "pre-commit" && hook.exists && !hook.isBdHook {
|
||||
existingPreCommit = hook.path + ".old"
|
||||
// Note: caller handles the rename
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `#!/bin/sh
|
||||
#
|
||||
# bd (beads) pre-commit hook (chained)
|
||||
#
|
||||
# This hook chains bd functionality with your existing pre-commit hook.
|
||||
|
||||
# Run existing hook first
|
||||
if [ -x "` + existingPreCommit + `" ]; then
|
||||
"` + existingPreCommit + `" "$@"
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
fi
|
||||
|
||||
` + preCommitHookBody()
|
||||
}
|
||||
|
||||
return `#!/bin/sh
|
||||
#
|
||||
# bd (beads) pre-commit hook
|
||||
#
|
||||
# This hook ensures that any pending bd issue changes are flushed to
|
||||
# .beads/issues.jsonl before the commit is created, preventing the
|
||||
# race condition where daemon auto-flush fires after the commit.
|
||||
|
||||
` + preCommitHookBody()
|
||||
}
|
||||
|
||||
// preCommitHookBody returns the common pre-commit hook logic
|
||||
func preCommitHookBody() string {
|
||||
return `# Check if bd is available
|
||||
if ! command -v bd >/dev/null 2>&1; then
|
||||
echo "Warning: bd command not found, skipping pre-commit flush" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if we're in a bd workspace
|
||||
# For worktrees, .beads is in the main repository root, not the worktree
|
||||
BEADS_DIR=""
|
||||
if git rev-parse --git-dir >/dev/null 2>&1; then
|
||||
# Check if we're in a worktree
|
||||
if [ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ]; then
|
||||
# Worktree: .beads is in main repo root
|
||||
MAIN_REPO_ROOT="$(git rev-parse --git-common-dir)"
|
||||
MAIN_REPO_ROOT="$(dirname "$MAIN_REPO_ROOT")"
|
||||
if [ -d "$MAIN_REPO_ROOT/.beads" ]; then
|
||||
BEADS_DIR="$MAIN_REPO_ROOT/.beads"
|
||||
fi
|
||||
else
|
||||
# Regular repo: check current directory
|
||||
if [ -d .beads ]; then
|
||||
BEADS_DIR=".beads"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$BEADS_DIR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Flush pending changes to JSONL
|
||||
if ! bd sync --flush-only >/dev/null 2>&1; then
|
||||
echo "Error: Failed to flush bd changes to JSONL" >&2
|
||||
echo "Run 'bd sync --flush-only' manually to diagnose" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If the JSONL file was modified, stage it
|
||||
# For worktrees, the JSONL is in the main repo's working tree, not the worktree,
|
||||
# so we can't use git add. Skip this step for worktrees.
|
||||
if [ -f "$BEADS_DIR/issues.jsonl" ]; then
|
||||
if [ "$(git rev-parse --git-dir)" = "$(git rev-parse --git-common-dir)" ]; then
|
||||
# Regular repo: file is in the working tree, safe to add
|
||||
git add "$BEADS_DIR/issues.jsonl" 2>/dev/null || true
|
||||
fi
|
||||
# For worktrees: .beads is in the main repo's working tree, not this worktree
|
||||
# Git rejects adding files outside the worktree, so we skip it.
|
||||
# The main repo will see the changes on the next pull/sync.
|
||||
fi
|
||||
|
||||
exit 0
|
||||
`
|
||||
}
|
||||
|
||||
// buildPostMergeHook generates the post-merge hook content
|
||||
func buildPostMergeHook(chainHooks bool, existingHooks []hookInfo) string {
|
||||
if chainHooks {
|
||||
// Find existing post-merge hook
|
||||
var existingPostMerge string
|
||||
for _, hook := range existingHooks {
|
||||
if hook.name == "post-merge" && hook.exists && !hook.isBdHook {
|
||||
existingPostMerge = hook.path + ".old"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `#!/bin/sh
|
||||
#
|
||||
# bd (beads) post-merge hook (chained)
|
||||
#
|
||||
# This hook chains bd functionality with your existing post-merge hook.
|
||||
|
||||
# Run existing hook first
|
||||
if [ -x "` + existingPostMerge + `" ]; then
|
||||
"` + existingPostMerge + `" "$@"
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
fi
|
||||
|
||||
` + postMergeHookBody()
|
||||
}
|
||||
|
||||
return `#!/bin/sh
|
||||
#
|
||||
# bd (beads) post-merge hook
|
||||
#
|
||||
# This hook imports updated issues from .beads/issues.jsonl after a
|
||||
# git pull or merge, ensuring the database stays in sync with git.
|
||||
|
||||
` + postMergeHookBody()
|
||||
}
|
||||
|
||||
// postMergeHookBody returns the common post-merge hook logic
|
||||
func postMergeHookBody() string {
|
||||
return `# Check if bd is available
|
||||
if ! command -v bd >/dev/null 2>&1; then
|
||||
echo "Warning: bd command not found, skipping post-merge import" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if we're in a bd workspace
|
||||
# For worktrees, .beads is in the main repository root, not the worktree
|
||||
BEADS_DIR=""
|
||||
if git rev-parse --git-dir >/dev/null 2>&1; then
|
||||
# Check if we're in a worktree
|
||||
if [ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ]; then
|
||||
# Worktree: .beads is in main repo root
|
||||
MAIN_REPO_ROOT="$(git rev-parse --git-common-dir)"
|
||||
MAIN_REPO_ROOT="$(dirname "$MAIN_REPO_ROOT")"
|
||||
if [ -d "$MAIN_REPO_ROOT/.beads" ]; then
|
||||
BEADS_DIR="$MAIN_REPO_ROOT/.beads"
|
||||
fi
|
||||
else
|
||||
# Regular repo: check current directory
|
||||
if [ -d .beads ]; then
|
||||
BEADS_DIR=".beads"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$BEADS_DIR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if issues.jsonl exists and was updated
|
||||
if [ ! -f "$BEADS_DIR/issues.jsonl" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Import the updated JSONL
|
||||
if ! bd import -i "$BEADS_DIR/issues.jsonl" >/dev/null 2>&1; then
|
||||
echo "Warning: Failed to import bd changes after merge" >&2
|
||||
echo "Run 'bd import -i $BEADS_DIR/issues.jsonl' manually to see the error" >&2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
`
|
||||
}
|
||||
|
||||
// mergeDriverInstalled checks if bd merge driver is configured correctly
|
||||
func mergeDriverInstalled() bool {
|
||||
// Check git config for merge driver
|
||||
cmd := exec.Command("git", "config", "merge.beads.driver")
|
||||
output, err := cmd.Output()
|
||||
if err != nil || len(output) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if using old invalid placeholders (%L/%R from versions <0.24.0)
|
||||
// Git only supports %O (base), %A (current), %B (other)
|
||||
driverConfig := strings.TrimSpace(string(output))
|
||||
if strings.Contains(driverConfig, "%L") || strings.Contains(driverConfig, "%R") {
|
||||
// Stale config with invalid placeholders - needs repair
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if .gitattributes has the merge driver configured
|
||||
gitattributesPath := ".gitattributes"
|
||||
content, err := os.ReadFile(gitattributesPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for beads JSONL merge attribute (either canonical or legacy filename)
|
||||
hasCanonical := strings.Contains(string(content), ".beads/issues.jsonl") &&
|
||||
strings.Contains(string(content), "merge=beads")
|
||||
hasLegacy := strings.Contains(string(content), ".beads/beads.jsonl") &&
|
||||
strings.Contains(string(content), "merge=beads")
|
||||
return hasCanonical || hasLegacy
|
||||
}
|
||||
|
||||
// installMergeDriver configures git to use bd merge for JSONL files
|
||||
func installMergeDriver() error {
|
||||
// Configure git merge driver
|
||||
cmd := exec.Command("git", "config", "merge.beads.driver", "bd merge %A %O %A %B")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to configure git merge driver: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
cmd = exec.Command("git", "config", "merge.beads.name", "bd JSONL merge driver")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
// Non-fatal, the name is just descriptive
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to set merge driver name: %v\n%s", err, output)
|
||||
}
|
||||
|
||||
// Create or update .gitattributes
|
||||
gitattributesPath := ".gitattributes"
|
||||
|
||||
// Read existing .gitattributes if it exists
|
||||
var existingContent string
|
||||
content, err := os.ReadFile(gitattributesPath)
|
||||
if err == nil {
|
||||
existingContent = string(content)
|
||||
}
|
||||
|
||||
// Check if beads merge driver is already configured
|
||||
// Check for either pattern (issues.jsonl is canonical, beads.jsonl is legacy)
|
||||
hasBeadsMerge := (strings.Contains(existingContent, ".beads/issues.jsonl") ||
|
||||
strings.Contains(existingContent, ".beads/beads.jsonl")) &&
|
||||
strings.Contains(existingContent, "merge=beads")
|
||||
|
||||
if !hasBeadsMerge {
|
||||
// Append beads merge driver configuration (issues.jsonl is canonical)
|
||||
beadsMergeAttr := "\n# Use bd merge for beads JSONL files\n.beads/issues.jsonl merge=beads\n"
|
||||
|
||||
newContent := existingContent
|
||||
if !strings.HasSuffix(newContent, "\n") && len(newContent) > 0 {
|
||||
newContent += "\n"
|
||||
}
|
||||
newContent += beadsMergeAttr
|
||||
|
||||
// Write updated .gitattributes (0644 is standard for .gitattributes)
|
||||
// #nosec G306 - .gitattributes needs to be readable
|
||||
if err := os.WriteFile(gitattributesPath, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to update .gitattributes: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
322
cmd/bd/init_stealth.go
Normal file
322
cmd/bd/init_stealth.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// setupStealthMode configures git settings for stealth operation
|
||||
// Uses .git/info/exclude (per-repository) instead of global gitignore because:
|
||||
// - Global gitignore doesn't support absolute paths (GitHub #704)
|
||||
// - .git/info/exclude is designed for user-specific, repo-local ignores
|
||||
// - Patterns are relative to repo root, so ".beads/" works correctly
|
||||
func setupStealthMode(verbose bool) error {
|
||||
// Setup per-repository git exclude file
|
||||
if err := setupGitExclude(verbose); err != nil {
|
||||
return fmt.Errorf("failed to setup git exclude: %w", err)
|
||||
}
|
||||
|
||||
// Setup claude settings
|
||||
if err := setupClaudeSettings(verbose); err != nil {
|
||||
return fmt.Errorf("failed to setup claude settings: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("\n%s Stealth mode configured successfully!\n\n", ui.RenderPass("✓"))
|
||||
fmt.Printf(" Git exclude: %s\n", ui.RenderAccent(".git/info/exclude configured"))
|
||||
fmt.Printf(" Claude settings: %s\n\n", ui.RenderAccent("configured with bd onboard instruction"))
|
||||
fmt.Printf("Your beads setup is now %s - other repo collaborators won't see any beads-related files.\n\n", ui.RenderAccent("invisible"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupGitExclude configures .git/info/exclude to ignore beads and claude files
|
||||
// This is the correct approach for per-repository user-specific ignores (GitHub #704).
|
||||
// Unlike global gitignore, patterns here are relative to the repo root.
|
||||
func setupGitExclude(verbose bool) error {
|
||||
// Find the .git directory (handles both regular repos and worktrees)
|
||||
gitDir, err := exec.Command("git", "rev-parse", "--git-dir").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not a git repository")
|
||||
}
|
||||
gitDirPath := strings.TrimSpace(string(gitDir))
|
||||
|
||||
// Path to the exclude file
|
||||
excludePath := filepath.Join(gitDirPath, "info", "exclude")
|
||||
|
||||
// Ensure the info directory exists
|
||||
infoDir := filepath.Join(gitDirPath, "info")
|
||||
if err := os.MkdirAll(infoDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create git info directory: %w", err)
|
||||
}
|
||||
|
||||
// Read existing exclude file if it exists
|
||||
var existingContent string
|
||||
// #nosec G304 - git config path
|
||||
if content, err := os.ReadFile(excludePath); err == nil {
|
||||
existingContent = string(content)
|
||||
}
|
||||
|
||||
// Use relative patterns (these work correctly in .git/info/exclude)
|
||||
beadsPattern := ".beads/"
|
||||
claudePattern := ".claude/settings.local.json"
|
||||
|
||||
hasBeads := strings.Contains(existingContent, beadsPattern)
|
||||
hasClaude := strings.Contains(existingContent, claudePattern)
|
||||
|
||||
if hasBeads && hasClaude {
|
||||
if verbose {
|
||||
fmt.Printf("Git exclude already configured for stealth mode\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Append missing patterns
|
||||
newContent := existingContent
|
||||
if !strings.HasSuffix(newContent, "\n") && len(newContent) > 0 {
|
||||
newContent += "\n"
|
||||
}
|
||||
|
||||
if !hasBeads || !hasClaude {
|
||||
newContent += "\n# Beads stealth mode (added by bd init --stealth)\n"
|
||||
}
|
||||
|
||||
if !hasBeads {
|
||||
newContent += beadsPattern + "\n"
|
||||
}
|
||||
if !hasClaude {
|
||||
newContent += claudePattern + "\n"
|
||||
}
|
||||
|
||||
// Write the updated exclude file
|
||||
// #nosec G306 - config file needs 0644
|
||||
if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write git exclude file: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Configured git exclude for stealth mode: %s\n", excludePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupForkExclude configures .git/info/exclude for fork workflows (GH#742)
|
||||
// Adds beads files and Claude artifacts to keep PRs to upstream clean.
|
||||
// This is separate from stealth mode - fork protection is specifically about
|
||||
// preventing beads/Claude files from appearing in upstream PRs.
|
||||
func setupForkExclude(verbose bool) error {
|
||||
gitDir, err := exec.Command("git", "rev-parse", "--git-dir").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not a git repository")
|
||||
}
|
||||
gitDirPath := strings.TrimSpace(string(gitDir))
|
||||
excludePath := filepath.Join(gitDirPath, "info", "exclude")
|
||||
|
||||
// Ensure info directory exists
|
||||
if err := os.MkdirAll(filepath.Join(gitDirPath, "info"), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create git info directory: %w", err)
|
||||
}
|
||||
|
||||
// Read existing content
|
||||
var existingContent string
|
||||
// #nosec G304 - git config path
|
||||
if content, err := os.ReadFile(excludePath); err == nil {
|
||||
existingContent = string(content)
|
||||
}
|
||||
|
||||
// Patterns to add for fork protection
|
||||
patterns := []string{".beads/", "**/RECOVERY*.md", "**/SESSION*.md"}
|
||||
var toAdd []string
|
||||
for _, p := range patterns {
|
||||
// Check for exact line match (pattern alone on a line)
|
||||
// This avoids false positives like ".beads/issues.jsonl" matching ".beads/"
|
||||
if !containsExactPattern(existingContent, p) {
|
||||
toAdd = append(toAdd, p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toAdd) == 0 {
|
||||
if verbose {
|
||||
fmt.Printf("%s Git exclude already configured\n", ui.RenderPass("✓"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Append patterns
|
||||
newContent := existingContent
|
||||
if !strings.HasSuffix(newContent, "\n") && len(newContent) > 0 {
|
||||
newContent += "\n"
|
||||
}
|
||||
newContent += "\n# Beads fork protection (bd init)\n"
|
||||
for _, p := range toAdd {
|
||||
newContent += p + "\n"
|
||||
}
|
||||
|
||||
// #nosec G306 - config file needs 0644
|
||||
if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write git exclude: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("\n%s Added to .git/info/exclude:\n", ui.RenderPass("✓"))
|
||||
for _, p := range toAdd {
|
||||
fmt.Printf(" %s\n", p)
|
||||
}
|
||||
fmt.Println("\nNote: .git/info/exclude is local-only and won't affect upstream.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsExactPattern checks if content contains the pattern as an exact line
|
||||
// This avoids false positives like ".beads/issues.jsonl" matching ".beads/"
|
||||
func containsExactPattern(content, pattern string) bool {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
if strings.TrimSpace(line) == pattern {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// promptForkExclude asks if user wants to configure .git/info/exclude for fork workflow (GH#742)
|
||||
func promptForkExclude(upstreamURL string, quiet bool) bool {
|
||||
if quiet {
|
||||
return false // Don't prompt in quiet mode
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s Detected fork (upstream: %s)\n\n", ui.RenderAccent("▶"), upstreamURL)
|
||||
fmt.Println("Would you like to configure .git/info/exclude to keep beads files local?")
|
||||
fmt.Println("This prevents beads from appearing in PRs to upstream.")
|
||||
fmt.Print("\n[Y/n]: ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
|
||||
// Default to yes (empty or "y" or "yes")
|
||||
return response == "" || response == "y" || response == "yes"
|
||||
}
|
||||
|
||||
// setupGlobalGitIgnore configures global gitignore to ignore beads and claude files for a specific project
|
||||
// DEPRECATED: This function uses absolute paths which don't work in gitignore (GitHub #704).
|
||||
// Use setupGitExclude instead for new code.
|
||||
func setupGlobalGitIgnore(homeDir string, projectPath string, verbose bool) error {
|
||||
// Check if user already has a global gitignore file configured
|
||||
cmd := exec.Command("git", "config", "--global", "core.excludesfile")
|
||||
output, err := cmd.Output()
|
||||
|
||||
var ignorePath string
|
||||
|
||||
if err == nil && len(output) > 0 {
|
||||
// User has already configured a global gitignore file, use it
|
||||
ignorePath = strings.TrimSpace(string(output))
|
||||
|
||||
// Expand tilde if present (git config may return ~/... which Go doesn't expand)
|
||||
if strings.HasPrefix(ignorePath, "~/") {
|
||||
ignorePath = filepath.Join(homeDir, ignorePath[2:])
|
||||
} else if ignorePath == "~" {
|
||||
ignorePath = homeDir
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Using existing configured global gitignore file: %s\n", ignorePath)
|
||||
}
|
||||
} else {
|
||||
// No global gitignore file configured, check if standard location exists
|
||||
configDir := filepath.Join(homeDir, ".config", "git")
|
||||
standardIgnorePath := filepath.Join(configDir, "ignore")
|
||||
|
||||
if _, err := os.Stat(standardIgnorePath); err == nil {
|
||||
// Standard global gitignore file exists, use it
|
||||
// No need to set git config - git automatically uses this standard location
|
||||
ignorePath = standardIgnorePath
|
||||
if verbose {
|
||||
fmt.Printf("Using existing global gitignore file: %s\n", ignorePath)
|
||||
}
|
||||
} else {
|
||||
// No global gitignore file exists, create one in standard location
|
||||
// No need to set git config - git automatically uses this standard location
|
||||
ignorePath = standardIgnorePath
|
||||
|
||||
// Ensure config directory exists
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create git config directory: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Creating new global gitignore file: %s\n", ignorePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read existing ignore file if it exists
|
||||
var existingContent string
|
||||
// #nosec G304 - user config path
|
||||
if content, err := os.ReadFile(ignorePath); err == nil {
|
||||
existingContent = string(content)
|
||||
}
|
||||
|
||||
// Use absolute paths for this specific project (fixes GitHub #538)
|
||||
// This allows other projects to use beads openly while this one stays stealth
|
||||
beadsPattern := projectPath + "/.beads/"
|
||||
claudePattern := projectPath + "/.claude/settings.local.json"
|
||||
|
||||
hasBeads := strings.Contains(existingContent, beadsPattern)
|
||||
hasClaude := strings.Contains(existingContent, claudePattern)
|
||||
|
||||
if hasBeads && hasClaude {
|
||||
if verbose {
|
||||
fmt.Printf("Global gitignore already configured for stealth mode in %s\n", projectPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Append missing patterns
|
||||
newContent := existingContent
|
||||
if !strings.HasSuffix(newContent, "\n") && len(newContent) > 0 {
|
||||
newContent += "\n"
|
||||
}
|
||||
|
||||
if !hasBeads || !hasClaude {
|
||||
newContent += fmt.Sprintf("\n# Beads stealth mode: %s (added by bd init --stealth)\n", projectPath)
|
||||
}
|
||||
|
||||
if !hasBeads {
|
||||
newContent += beadsPattern + "\n"
|
||||
}
|
||||
if !hasClaude {
|
||||
newContent += claudePattern + "\n"
|
||||
}
|
||||
|
||||
// Write the updated ignore file
|
||||
// #nosec G306 - config file needs 0644
|
||||
if err := os.WriteFile(ignorePath, []byte(newContent), 0644); err != nil {
|
||||
fmt.Printf("\nUnable to write to %s (file is read-only)\n\n", ignorePath)
|
||||
fmt.Printf("To enable stealth mode, add these lines to your global gitignore:\n\n")
|
||||
if !hasBeads || !hasClaude {
|
||||
fmt.Printf("# Beads stealth mode: %s\n", projectPath)
|
||||
}
|
||||
if !hasBeads {
|
||||
fmt.Printf("%s\n", beadsPattern)
|
||||
}
|
||||
if !hasClaude {
|
||||
fmt.Printf("%s\n", claudePattern)
|
||||
}
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Configured global gitignore for stealth mode in %s\n", projectPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
193
cmd/bd/init_templates.go
Normal file
193
cmd/bd/init_templates.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// createConfigYaml creates the config.yaml template in the specified directory
|
||||
func createConfigYaml(beadsDir string, noDbMode bool) error {
|
||||
configYamlPath := filepath.Join(beadsDir, "config.yaml")
|
||||
|
||||
// Skip if already exists
|
||||
if _, err := os.Stat(configYamlPath); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
noDbLine := "# no-db: false"
|
||||
if noDbMode {
|
||||
noDbLine = "no-db: true # JSONL-only mode, no SQLite database"
|
||||
}
|
||||
|
||||
configYamlTemplate := fmt.Sprintf(`# Beads Configuration File
|
||||
# This file configures default behavior for all bd commands in this repository
|
||||
# All settings can also be set via environment variables (BD_* prefix)
|
||||
# or overridden with command-line flags
|
||||
|
||||
# Issue prefix for this repository (used by bd init)
|
||||
# If not set, bd init will auto-detect from directory name
|
||||
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
|
||||
# issue-prefix: ""
|
||||
|
||||
# Use no-db mode: load from JSONL, no SQLite, write back after each command
|
||||
# When true, bd will use .beads/issues.jsonl as the source of truth
|
||||
# instead of SQLite database
|
||||
%s
|
||||
|
||||
# Disable daemon for RPC communication (forces direct database access)
|
||||
# no-daemon: false
|
||||
|
||||
# Disable auto-flush of database to JSONL after mutations
|
||||
# no-auto-flush: false
|
||||
|
||||
# Disable auto-import from JSONL when it's newer than database
|
||||
# no-auto-import: false
|
||||
|
||||
# Enable JSON output by default
|
||||
# json: false
|
||||
|
||||
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
|
||||
# actor: ""
|
||||
|
||||
# Path to database (overridden by BEADS_DB or --db)
|
||||
# db: ""
|
||||
|
||||
# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
|
||||
# auto-start-daemon: true
|
||||
|
||||
# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
|
||||
# flush-debounce: "5s"
|
||||
|
||||
# Git branch for beads commits (bd sync will commit to this branch)
|
||||
# IMPORTANT: Set this for team projects so all clones use the same sync branch.
|
||||
# This setting persists across clones (unlike database config which is gitignored).
|
||||
# Can also use BEADS_SYNC_BRANCH env var for local override.
|
||||
# If not set, bd sync will require you to run 'bd config set sync.branch <branch>'.
|
||||
# sync-branch: "beads-sync"
|
||||
|
||||
# Multi-repo configuration (experimental - bd-307)
|
||||
# Allows hydrating from multiple repositories and routing writes to the correct JSONL
|
||||
# repos:
|
||||
# primary: "." # Primary repo (where this database lives)
|
||||
# additional: # Additional repos to hydrate from (read-only)
|
||||
# - ~/beads-planning # Personal planning repo
|
||||
# - ~/work-planning # Work planning repo
|
||||
|
||||
# Integration settings (access with 'bd config get/set')
|
||||
# These are stored in the database, not in this file:
|
||||
# - jira.url
|
||||
# - jira.project
|
||||
# - linear.url
|
||||
# - linear.api-key
|
||||
# - github.org
|
||||
# - github.repo
|
||||
`, noDbLine)
|
||||
|
||||
if err := os.WriteFile(configYamlPath, []byte(configYamlTemplate), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write config.yaml: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createReadme creates the README.md file in the .beads directory
|
||||
func createReadme(beadsDir string) error {
|
||||
readmePath := filepath.Join(beadsDir, "README.md")
|
||||
|
||||
// Skip if already exists
|
||||
if _, err := os.Stat(readmePath); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
readmeTemplate := `# Beads - AI-Native Issue Tracking
|
||||
|
||||
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
|
||||
|
||||
## What is Beads?
|
||||
|
||||
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
|
||||
|
||||
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Essential Commands
|
||||
|
||||
` + "```bash" + `
|
||||
# Create new issues
|
||||
bd create "Add user authentication"
|
||||
|
||||
# View all issues
|
||||
bd list
|
||||
|
||||
# View issue details
|
||||
bd show <issue-id>
|
||||
|
||||
# Update issue status
|
||||
bd update <issue-id> --status in_progress
|
||||
bd update <issue-id> --status done
|
||||
|
||||
# Sync with git remote
|
||||
bd sync
|
||||
` + "```" + `
|
||||
|
||||
### Working with Issues
|
||||
|
||||
Issues in Beads are:
|
||||
- **Git-native**: Stored in ` + "`.beads/issues.jsonl`" + ` and synced like code
|
||||
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
|
||||
- **Branch-aware**: Issues can follow your branch workflow
|
||||
- **Always in sync**: Auto-syncs with your commits
|
||||
|
||||
## Why Beads?
|
||||
|
||||
✨ **AI-Native Design**
|
||||
- Built specifically for AI-assisted development workflows
|
||||
- CLI-first interface works seamlessly with AI coding agents
|
||||
- No context switching to web UIs
|
||||
|
||||
🚀 **Developer Focused**
|
||||
- Issues live in your repo, right next to your code
|
||||
- Works offline, syncs when you push
|
||||
- Fast, lightweight, and stays out of your way
|
||||
|
||||
🔧 **Git Integration**
|
||||
- Automatic sync with git commits
|
||||
- Branch-aware issue tracking
|
||||
- Intelligent JSONL merge resolution
|
||||
|
||||
## Get Started with Beads
|
||||
|
||||
Try Beads in your own projects:
|
||||
|
||||
` + "```bash" + `
|
||||
# Install Beads
|
||||
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
|
||||
|
||||
# Initialize in your repo
|
||||
bd init
|
||||
|
||||
# Create your first issue
|
||||
bd create "Try out Beads"
|
||||
` + "```" + `
|
||||
|
||||
## Learn More
|
||||
|
||||
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
|
||||
- **Quick Start Guide**: Run ` + "`bd quickstart`" + `
|
||||
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
|
||||
|
||||
---
|
||||
|
||||
*Beads: Issue tracking that moves at the speed of thought* ⚡
|
||||
`
|
||||
|
||||
// Write README.md (0644 is standard for markdown files)
|
||||
// #nosec G306 - README needs to be readable
|
||||
if err := os.WriteFile(readmePath, []byte(readmeTemplate), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write README.md: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user