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:
136
cmd/bd/main.go
136
cmd/bd/main.go
@@ -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{
|
||||
|
||||
@@ -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\""))
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user