Implement auto-import to complete automatic git sync workflow

Adds auto-import feature to complement bd-35's auto-export, completing
the automatic sync workflow for git collaboration.

**Implementation:**
- Auto-import checks if JSONL is newer than DB on command startup
- Silently imports JSONL when modification time is newer
- Skips import command itself to avoid recursion
- Can be disabled with --no-auto-import flag

**Documentation updates:**
- Updated README.md git workflow section
- Updated CLAUDE.md workflow and pro tips
- Updated bd quickstart with auto-sync section
- Updated git hooks README to clarify they're now optional

**Testing:**
- Tested auto-import by touching JSONL and running commands
- Tested auto-export with create/close operations
- Complete workflow verified working

Closes bd-33

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-10-13 22:48:30 -07:00
parent 026940c8ae
commit 584cd1ebfc
6 changed files with 255 additions and 77 deletions

View File

@@ -1,6 +1,7 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
@@ -31,6 +32,9 @@ var (
flushDebounce = 5 * time.Second
storeMutex sync.Mutex // Protects store access from background goroutine
storeActive = false // Tracks if store is available
// Auto-import state
autoImportEnabled = true // Can be disabled with --no-auto-import
)
var rootCmd = &cobra.Command{
@@ -46,6 +50,9 @@ var rootCmd = &cobra.Command{
// Set auto-flush based on flag (invert no-auto-flush)
autoFlushEnabled = !noAutoFlush
// Set auto-import based on flag (invert no-auto-import)
autoImportEnabled = !noAutoImport
// Initialize storage
if dbPath == "" {
// Try to find database in order:
@@ -81,6 +88,12 @@ var rootCmd = &cobra.Command{
actor = "unknown"
}
}
// Auto-import if JSONL is newer than DB (e.g., after git pull)
// Skip for import command itself to avoid recursion
if cmd.Name() != "import" && autoImportEnabled {
autoImportIfNewer()
}
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
// Signal that store is closing (prevents background flush from accessing closed store)
@@ -201,6 +214,123 @@ func findJSONLPath() string {
return filepath.Join(dbDir, "issues.jsonl")
}
// autoImportIfNewer checks if JSONL is newer than DB and imports if so
func autoImportIfNewer() {
// Find JSONL path
jsonlPath := findJSONLPath()
// Check if JSONL exists
jsonlInfo, err := os.Stat(jsonlPath)
if err != nil {
// JSONL doesn't exist or can't be accessed, skip import
return
}
// Check if DB exists
dbInfo, err := os.Stat(dbPath)
if err != nil {
// DB doesn't exist (new init?), skip import
return
}
// Compare modification times
if !jsonlInfo.ModTime().After(dbInfo.ModTime()) {
// JSONL is not newer than DB, skip import
return
}
// JSONL is newer, perform silent import
ctx := context.Background()
// Read and parse JSONL
f, err := os.Open(jsonlPath)
if err != nil {
// Can't open JSONL, skip import
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
var allIssues []*types.Issue
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
// Parse error, skip this import
return
}
allIssues = append(allIssues, &issue)
}
if err := scanner.Err(); err != nil {
return
}
// Import issues (create new, update existing)
for _, issue := range allIssues {
existing, err := store.GetIssue(ctx, issue.ID)
if err != nil {
continue
}
if existing != nil {
// Update existing issue
updates := make(map[string]interface{})
updates["title"] = issue.Title
updates["description"] = issue.Description
updates["design"] = issue.Design
updates["acceptance_criteria"] = issue.AcceptanceCriteria
updates["notes"] = issue.Notes
updates["status"] = issue.Status
updates["priority"] = issue.Priority
updates["issue_type"] = issue.IssueType
updates["assignee"] = issue.Assignee
if issue.EstimatedMinutes != nil {
updates["estimated_minutes"] = *issue.EstimatedMinutes
}
_ = store.UpdateIssue(ctx, issue.ID, updates, "auto-import")
} else {
// Create new issue
_ = store.CreateIssue(ctx, issue, "auto-import")
}
}
// Import dependencies
for _, issue := range allIssues {
if len(issue.Dependencies) == 0 {
continue
}
// Get existing dependencies
existingDeps, err := store.GetDependencyRecords(ctx, issue.ID)
if err != nil {
continue
}
// Add missing dependencies
for _, dep := range issue.Dependencies {
exists := false
for _, existing := range existingDeps {
if existing.DependsOnID == dep.DependsOnID && existing.Type == dep.Type {
exists = true
break
}
}
if !exists {
_ = store.AddDependency(ctx, dep, "auto-import")
}
}
}
}
// markDirtyAndScheduleFlush marks the database as dirty and schedules a flush
func markDirtyAndScheduleFlush() {
if !autoFlushEnabled {
@@ -307,13 +437,17 @@ func flushToJSONL() {
}
}
var noAutoFlush bool
var (
noAutoFlush bool
noAutoImport bool
)
func init() {
rootCmd.PersistentFlags().StringVar(&dbPath, "db", "", "Database path (default: auto-discover .beads/*.db or ~/.beads/default.db)")
rootCmd.PersistentFlags().StringVar(&actor, "actor", "", "Actor name for audit trail (default: $USER)")
rootCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "Output in JSON format")
rootCmd.PersistentFlags().BoolVar(&noAutoFlush, "no-auto-flush", false, "Disable automatic JSONL sync after CRUD operations")
rootCmd.PersistentFlags().BoolVar(&noAutoImport, "no-auto-import", false, "Disable automatic JSONL import when newer than DB")
}
var createCmd = &cobra.Command{

View File

@@ -84,6 +84,14 @@ var quickstartCmd = &cobra.Command{
fmt.Printf(" • Join with %s table for powerful queries\n", cyan("issues"))
fmt.Printf(" • See %s for integration patterns\n\n", cyan("EXTENDING.md"))
fmt.Printf("%s\n", bold("GIT WORKFLOW (AUTO-SYNC)"))
fmt.Printf(" bd automatically keeps git in sync:\n")
fmt.Printf(" • %s Export to JSONL after CRUD operations (5s debounce)\n", green("✓"))
fmt.Printf(" • %s Import from JSONL when newer than DB (after %s)\n", green("✓"), cyan("git pull"))
fmt.Printf(" • %s Works seamlessly across machines and team members\n", green("✓"))
fmt.Printf(" • No manual export/import needed!\n")
fmt.Printf(" Disable with: %s or %s\n\n", cyan("--no-auto-flush"), cyan("--no-auto-import"))
fmt.Printf("%s\n", green("Ready to start!"))
fmt.Printf("Run %s to create your first issue.\n\n", cyan("bd create \"My first issue\""))
},