Merge remote-tracking branch 'origin/main'
Amp-Thread-ID: https://ampcode.com/threads/T-aa765a68-5cc4-465b-a2f6-aa008933c11e Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
59
.agent/workflows/resolve-beads-conflict.md
Normal file
59
.agent/workflows/resolve-beads-conflict.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
description: How to resolve merge conflicts in .beads/beads.jsonl
|
||||
---
|
||||
|
||||
# Resolving `beads.jsonl` Merge Conflicts
|
||||
|
||||
If you encounter a merge conflict in `.beads/beads.jsonl` that doesn't have standard git conflict markers (or if `bd merge` failed automatically), follow this procedure.
|
||||
|
||||
## 1. Identify the Conflict
|
||||
Check if `beads.jsonl` is in conflict:
|
||||
```powershell
|
||||
git status
|
||||
```
|
||||
|
||||
## 2. Extract the 3 Versions
|
||||
Git stores three versions of conflicted files in its index:
|
||||
1. Base (common ancestor)
|
||||
2. Ours (current branch)
|
||||
3. Theirs (incoming branch)
|
||||
|
||||
Extract them to temporary files:
|
||||
```powershell
|
||||
git show :1:.beads/beads.jsonl > beads.base.jsonl
|
||||
git show :2:.beads/beads.jsonl > beads.ours.jsonl
|
||||
git show :3:.beads/beads.jsonl > beads.theirs.jsonl
|
||||
```
|
||||
|
||||
## 3. Run `bd merge` Manually
|
||||
Run the `bd merge` tool manually with the `--debug` flag to see what's happening.
|
||||
Syntax: `bd merge <output> <base> <ours> <theirs>`
|
||||
|
||||
```powershell
|
||||
bd merge beads.merged.jsonl beads.base.jsonl beads.ours.jsonl beads.theirs.jsonl --debug
|
||||
```
|
||||
|
||||
## 4. Verify the Result
|
||||
Check the output of the command.
|
||||
- **Exit Code 0**: Success. `beads.merged.jsonl` contains the clean merge.
|
||||
- **Exit Code 1**: Conflicts remain. `beads.merged.jsonl` will contain conflict markers. You must edit it manually to resolve them.
|
||||
|
||||
Optionally, verify the content (e.g., check for missing IDs if you suspect data loss).
|
||||
|
||||
## 5. Apply the Merge
|
||||
Overwrite the conflicted file with the resolved version:
|
||||
```powershell
|
||||
cp beads.merged.jsonl .beads/beads.jsonl
|
||||
```
|
||||
|
||||
## 6. Cleanup and Continue
|
||||
Stage the resolved file and continue the merge:
|
||||
```powershell
|
||||
git add .beads/beads.jsonl
|
||||
git merge --continue
|
||||
```
|
||||
|
||||
## 7. Cleanup Temporary Files
|
||||
```powershell
|
||||
rm beads.base.jsonl beads.ours.jsonl beads.theirs.jsonl beads.merged.jsonl
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
@@ -307,6 +307,14 @@ func createExportFunc(ctx context.Context, store storage.Storage, autoCommit, au
|
||||
}
|
||||
log.log("Exported to JSONL")
|
||||
|
||||
// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
// with "JSONL is newer than database" after daemon auto-export
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
log.log("Warning: failed to update database mtime: %v", err)
|
||||
}
|
||||
|
||||
// Auto-commit if enabled
|
||||
if autoCommit {
|
||||
// Try sync branch commit first
|
||||
@@ -502,6 +510,13 @@ func createSyncFunc(ctx context.Context, store storage.Storage, autoCommit, auto
|
||||
}
|
||||
log.log("Exported to JSONL")
|
||||
|
||||
// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
log.log("Warning: failed to update database mtime: %v", err)
|
||||
}
|
||||
|
||||
// Capture left snapshot (pre-pull state) for 3-way merge
|
||||
// This is mandatory for deletion tracking integrity
|
||||
// In multi-repo mode, capture snapshots for all JSONL files
|
||||
@@ -597,6 +612,12 @@ func createSyncFunc(ctx context.Context, store storage.Storage, autoCommit, auto
|
||||
}
|
||||
log.log("Imported from JSONL")
|
||||
|
||||
// Update database mtime after import (fixes #278, #301, #321)
|
||||
// Sync branch import can update JSONL timestamp, so ensure DB >= JSONL
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
log.log("Warning: failed to update database mtime: %v", err)
|
||||
}
|
||||
|
||||
// Validate import didn't cause data loss
|
||||
afterCount, err := countDBIssues(syncCtx, store)
|
||||
if err != nil {
|
||||
|
||||
@@ -385,6 +385,18 @@ Output to stdout by default, or use -o flag for file output.`,
|
||||
fmt.Fprintf(os.Stderr, " Mismatch indicates export failed to write all issues\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
|
||||
// Only do this when exporting to default JSONL path (not arbitrary outputs)
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
if output == "" || output == findJSONLPath() {
|
||||
beadsDir := filepath.Dir(finalPath)
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := TouchDatabaseFile(dbPath, finalPath); err != nil {
|
||||
// Log warning but don't fail export
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to update database mtime: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Output statistics if JSON format requested
|
||||
|
||||
242
cmd/bd/export_mtime_test.go
Normal file
242
cmd/bd/export_mtime_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// TestExportUpdatesDatabaseMtime verifies that export updates database mtime
|
||||
// to be >= JSONL mtime, fixing issues #278, #301, #321
|
||||
func TestExportUpdatesDatabaseMtime(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow test in short mode")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
// Create and populate database
|
||||
store, err := sqlite.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Initialize database with issue_prefix
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set issue_prefix: %v", err)
|
||||
}
|
||||
|
||||
// Create a test issue
|
||||
issue := &types.Issue{
|
||||
ID: "test-1",
|
||||
Title: "Test Issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
|
||||
t.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Wait a bit to ensure mtime difference
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Export to JSONL (simulates daemon export)
|
||||
if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
|
||||
t.Fatalf("Export failed: %v", err)
|
||||
}
|
||||
|
||||
// Get JSONL mtime
|
||||
jsonlInfo, err := os.Stat(jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat JSONL after export: %v", err)
|
||||
}
|
||||
|
||||
// WITHOUT the fix, JSONL would be newer than DB here
|
||||
// Simulating the old buggy behavior before calling TouchDatabaseFile
|
||||
dbInfoAfterExport, err := os.Stat(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat database after export: %v", err)
|
||||
}
|
||||
|
||||
// In old buggy behavior, JSONL mtime > DB mtime
|
||||
t.Logf("Before TouchDatabaseFile: DB mtime=%v, JSONL mtime=%v",
|
||||
dbInfoAfterExport.ModTime(), jsonlInfo.ModTime())
|
||||
|
||||
// Now apply the fix
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
t.Fatalf("TouchDatabaseFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Get final database mtime
|
||||
dbInfoAfterTouch, err := os.Stat(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat database after touch: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("After TouchDatabaseFile: DB mtime=%v, JSONL mtime=%v",
|
||||
dbInfoAfterTouch.ModTime(), jsonlInfo.ModTime())
|
||||
|
||||
// VERIFY: Database mtime should be >= JSONL mtime
|
||||
if dbInfoAfterTouch.ModTime().Before(jsonlInfo.ModTime()) {
|
||||
t.Errorf("Database mtime should be >= JSONL mtime after export")
|
||||
t.Errorf("DB mtime: %v, JSONL mtime: %v",
|
||||
dbInfoAfterTouch.ModTime(), jsonlInfo.ModTime())
|
||||
}
|
||||
|
||||
// VERIFY: validatePreExport should now pass (not block on next export)
|
||||
if err := validatePreExport(ctx, store, jsonlPath); err != nil {
|
||||
t.Errorf("validatePreExport should pass after TouchDatabaseFile, but got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDaemonExportScenario simulates the full daemon auto-export workflow
|
||||
// that was causing issue #278 (daemon shutting down after export)
|
||||
func TestDaemonExportScenario(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow test in short mode")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
// Create and populate database
|
||||
store, err := sqlite.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Initialize database with issue_prefix
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
|
||||
t.Fatalf("Failed to set issue_prefix: %v", err)
|
||||
}
|
||||
|
||||
// Step 1: User creates an issue (e.g., bd close bd-123)
|
||||
now := time.Now()
|
||||
issue := &types.Issue{
|
||||
ID: "bd-123",
|
||||
Title: "User created issue",
|
||||
Status: types.StatusClosed,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
ClosedAt: &now,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Database is now newer than JSONL (JSONL doesn't exist yet)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Step 2: Daemon auto-exports after delay (30s-4min in real scenario)
|
||||
// This simulates the daemon's export cycle
|
||||
if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
|
||||
t.Fatalf("Daemon export failed: %v", err)
|
||||
}
|
||||
|
||||
// THIS IS THE FIX: daemon now calls TouchDatabaseFile after export
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
t.Fatalf("TouchDatabaseFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: User runs bd sync shortly after
|
||||
// WITHOUT the fix, this would fail with "JSONL is newer than database"
|
||||
// WITH the fix, this should succeed
|
||||
if err := validatePreExport(ctx, store, jsonlPath); err != nil {
|
||||
t.Errorf("Daemon export scenario failed: validatePreExport blocked after daemon export")
|
||||
t.Errorf("This is the bug from issue #278/#301/#321: %v", err)
|
||||
}
|
||||
|
||||
// Verify we can export again (simulates bd sync)
|
||||
jsonlPathTemp := jsonlPath + ".sync"
|
||||
if err := exportToJSONLWithStore(ctx, store, jsonlPathTemp); err != nil {
|
||||
t.Errorf("Second export (bd sync) failed: %v", err)
|
||||
}
|
||||
os.Remove(jsonlPathTemp)
|
||||
}
|
||||
|
||||
// TestMultipleExportCycles verifies repeated export cycles don't cause issues
|
||||
func TestMultipleExportCycles(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow test in short mode")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
// Create and populate database
|
||||
store, err := sqlite.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Initialize database with issue_prefix
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set issue_prefix: %v", err)
|
||||
}
|
||||
|
||||
// Run multiple export cycles
|
||||
for i := 0; i < 5; i++ {
|
||||
// Add an issue
|
||||
issue := &types.Issue{
|
||||
ID: "test-" + string(rune('a'+i)),
|
||||
Title: "Test Issue " + string(rune('A'+i)),
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
|
||||
t.Fatalf("Cycle %d: Failed to create issue: %v", i, err)
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Export (with fix)
|
||||
if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
|
||||
t.Fatalf("Cycle %d: Export failed: %v", i, err)
|
||||
}
|
||||
|
||||
// Apply fix
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
t.Fatalf("Cycle %d: TouchDatabaseFile failed: %v", i, err)
|
||||
}
|
||||
|
||||
// Verify validation passes
|
||||
if err := validatePreExport(ctx, store, jsonlPath); err != nil {
|
||||
t.Errorf("Cycle %d: validatePreExport failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +314,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
||||
// 2. Without mtime update, bd sync refuses to export (thinks JSONL is newer)
|
||||
// 3. This can happen after git pull updates JSONL mtime but content is identical
|
||||
// Fix for: refusing to export: JSONL is newer than database (import first to avoid data loss)
|
||||
if err := touchDatabaseFile(dbPath, input); err != nil {
|
||||
if err := TouchDatabaseFile(dbPath, input); err != nil {
|
||||
debug.Logf("Warning: failed to update database mtime: %v", err)
|
||||
}
|
||||
|
||||
@@ -381,17 +381,19 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
||||
},
|
||||
}
|
||||
|
||||
// touchDatabaseFile updates the modification time of the database file.
|
||||
// This is used after import to ensure the database appears "in sync" with JSONL,
|
||||
// preventing bd doctor from incorrectly warning that JSONL is newer.
|
||||
// TouchDatabaseFile updates the modification time of the database file.
|
||||
// This is used after import AND export to ensure the database appears "in sync" with JSONL,
|
||||
// preventing bd doctor and validatePreExport from incorrectly warning that JSONL is newer.
|
||||
//
|
||||
// In SQLite WAL mode, writes go to beads.db-wal and beads.db mtime may not update
|
||||
// until a checkpoint. Since bd doctor compares JSONL mtime to beads.db mtime only,
|
||||
// we need to explicitly touch the DB file after import.
|
||||
// until a checkpoint. Since validation compares JSONL mtime to beads.db mtime only,
|
||||
// we need to explicitly touch the DB file after both import and export operations.
|
||||
//
|
||||
// The function sets DB mtime to max(JSONL mtime, now) + 1ns to handle clock skew.
|
||||
// If jsonlPath is empty or can't be read, falls back to time.Now().
|
||||
func touchDatabaseFile(dbPath, jsonlPath string) error {
|
||||
//
|
||||
// Fixes issues #278, #301, #321: daemon export leaving JSONL newer than DB.
|
||||
func TouchDatabaseFile(dbPath, jsonlPath string) error {
|
||||
targetTime := time.Now()
|
||||
|
||||
// If we have the JSONL path, use max(JSONL mtime, now) to handle clock skew
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestTouchDatabaseFile verifies the touchDatabaseFile helper function
|
||||
// TestTouchDatabaseFile verifies the TouchDatabaseFile helper function
|
||||
func TestTouchDatabaseFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.db")
|
||||
@@ -27,8 +27,8 @@ func TestTouchDatabaseFile(t *testing.T) {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Touch the file
|
||||
if err := touchDatabaseFile(testFile, ""); err != nil {
|
||||
t.Fatalf("touchDatabaseFile failed: %v", err)
|
||||
if err := TouchDatabaseFile(testFile, ""); err != nil {
|
||||
t.Fatalf("TouchDatabaseFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Get new mtime
|
||||
@@ -64,8 +64,8 @@ func TestTouchDatabaseFileWithClockSkew(t *testing.T) {
|
||||
}
|
||||
|
||||
// Touch the DB file with JSONL path
|
||||
if err := touchDatabaseFile(dbFile, jsonlFile); err != nil {
|
||||
t.Fatalf("touchDatabaseFile failed: %v", err)
|
||||
if err := TouchDatabaseFile(dbFile, jsonlFile); err != nil {
|
||||
t.Fatalf("TouchDatabaseFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Get DB mtime
|
||||
|
||||
@@ -590,6 +590,15 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error {
|
||||
// Clear auto-flush state
|
||||
clearAutoFlushState()
|
||||
|
||||
// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
beadsDir := filepath.Dir(jsonlPath)
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
// Non-fatal warning
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to update database mtime: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
@@ -14,6 +17,9 @@ var (
|
||||
Version = "0.23.1"
|
||||
// Build can be set via ldflags at compile time
|
||||
Build = "dev"
|
||||
// Commit and branch the git revision the binary was built from (optional ldflag)
|
||||
Commit = ""
|
||||
Branch = ""
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
@@ -27,13 +33,29 @@ var versionCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
commit := resolveCommitHash()
|
||||
branch := resolveBranch()
|
||||
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]string{
|
||||
result := map[string]string{
|
||||
"version": Version,
|
||||
"build": Build,
|
||||
})
|
||||
}
|
||||
if commit != "" {
|
||||
result["commit"] = commit
|
||||
}
|
||||
if branch != "" {
|
||||
result["branch"] = branch
|
||||
}
|
||||
outputJSON(result)
|
||||
} else {
|
||||
fmt.Printf("bd version %s (%s)\n", Version, Build)
|
||||
if commit != "" && branch != "" {
|
||||
fmt.Printf("bd version %s (%s: %s@%s)\n", Version, Build, branch, shortCommit(commit))
|
||||
} else if commit != "" {
|
||||
fmt.Printf("bd version %s (%s: %s)\n", Version, Build, shortCommit(commit))
|
||||
} else {
|
||||
fmt.Printf("bd version %s (%s)\n", Version, Build)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -90,3 +112,52 @@ func init() {
|
||||
versionCmd.Flags().Bool("daemon", false, "Check daemon version and compatibility")
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
func resolveCommitHash() string {
|
||||
if Commit != "" {
|
||||
return Commit
|
||||
}
|
||||
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" && setting.Value != "" {
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func shortCommit(hash string) string {
|
||||
if len(hash) > 12 {
|
||||
return hash[:12]
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
func resolveBranch() string {
|
||||
if Branch != "" {
|
||||
return Branch
|
||||
}
|
||||
|
||||
// Try to get branch from build info (build-time VCS detection)
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.branch" && setting.Value != "" {
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to get branch from git at runtime
|
||||
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
cmd.Dir = "."
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
if branch := strings.TrimSpace(string(output)); branch != "" && branch != "HEAD" {
|
||||
return branch
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.16.0 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v1.17.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/ncruces/go-sqlite3 v0.29.1 // indirect
|
||||
github.com/ncruces/go-sqlite3 v0.30.1 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
@@ -21,15 +21,15 @@ require (
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.10.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/steveyegge/beads => ../..
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/anthropics/anthropic-sdk-go v1.16.0 h1:nRkOFDqYXsHteoIhjdJr/5dsiKbFF3rflSv8ax50y8o=
|
||||
github.com/anthropics/anthropic-sdk-go v1.16.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||
github.com/anthropics/anthropic-sdk-go v1.17.0 h1:BwK8ApcmaAUkvZTiQE0yi3R9XneEFskDIjLTmOAFZxQ=
|
||||
github.com/anthropics/anthropic-sdk-go v1.17.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -8,16 +8,16 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ncruces/go-sqlite3 v0.29.1 h1:NIi8AISWBToRHyoz01FXiTNvU147Tqdibgj2tFzJCqM=
|
||||
github.com/ncruces/go-sqlite3 v0.29.1/go.mod h1:PpccBNNhvjwUOwDQEn2gXQPFPTWdlromj0+fSkd5KSg=
|
||||
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
||||
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
@@ -42,8 +42,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
|
||||
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -56,12 +56,12 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -48,6 +48,13 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "PANIC in main: %v\n", r)
|
||||
}
|
||||
fmt.Println("Main function exiting")
|
||||
}()
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Find database path if not specified
|
||||
@@ -111,11 +118,9 @@ func main() {
|
||||
|
||||
// getSocketPath returns the Unix socket path for the daemon
|
||||
func getSocketPath(dbPath string) string {
|
||||
// Use the database directory to determine socket path
|
||||
// The daemon always creates the socket as "bd.sock" in the same directory as the database
|
||||
dbDir := filepath.Dir(dbPath)
|
||||
dbName := filepath.Base(dbPath)
|
||||
socketName := dbName + ".sock"
|
||||
return filepath.Join(dbDir, ".beads", socketName)
|
||||
return filepath.Join(dbDir, "bd.sock")
|
||||
}
|
||||
|
||||
// connectToDaemon establishes connection to the daemon
|
||||
@@ -321,6 +326,11 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleWebSocketBroadcast sends messages to all connected WebSocket clients
|
||||
func handleWebSocketBroadcast() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "PANIC in handleWebSocketBroadcast: %v\n", r)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
// Wait for message to broadcast
|
||||
message := <-wsBroadcast
|
||||
@@ -342,6 +352,11 @@ func handleWebSocketBroadcast() {
|
||||
|
||||
// pollMutations polls the daemon for mutations and broadcasts them to WebSocket clients
|
||||
func pollMutations() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "PANIC in pollMutations: %v\n", r)
|
||||
}
|
||||
}()
|
||||
lastPollTime := int64(0) // Start from beginning
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second) // Poll every 2 seconds
|
||||
|
||||
Binary file not shown.
@@ -181,6 +181,15 @@ main() {
|
||||
"\"version\": \"$CURRENT_VERSION\"" \
|
||||
"\"version\": \"$NEW_VERSION\""
|
||||
|
||||
# 9. Update hook templates
|
||||
echo " • cmd/bd/templates/hooks/*"
|
||||
HOOK_FILES=("pre-commit" "post-merge" "pre-push" "post-checkout")
|
||||
for hook in "${HOOK_FILES[@]}"; do
|
||||
update_file "cmd/bd/templates/hooks/$hook" \
|
||||
"# bd-hooks-version: $CURRENT_VERSION" \
|
||||
"# bd-hooks-version: $NEW_VERSION"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ Version updated to $NEW_VERSION${NC}"
|
||||
echo ""
|
||||
@@ -199,6 +208,7 @@ main() {
|
||||
"$(grep 'version = ' integrations/beads-mcp/pyproject.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')"
|
||||
"$(grep '__version__ = ' integrations/beads-mcp/src/beads_mcp/__init__.py | sed 's/.*"\(.*\)".*/\1/')"
|
||||
"$(jq -r '.version' npm-package/package.json)"
|
||||
"$(grep '# bd-hooks-version: ' cmd/bd/templates/hooks/pre-commit | sed 's/.*: \(.*\)/\1/')"
|
||||
)
|
||||
|
||||
ALL_MATCH=true
|
||||
@@ -228,7 +238,8 @@ main() {
|
||||
integrations/beads-mcp/pyproject.toml \
|
||||
integrations/beads-mcp/src/beads_mcp/__init__.py \
|
||||
npm-package/package.json \
|
||||
README.md
|
||||
README.md \
|
||||
cmd/bd/templates/hooks/*
|
||||
|
||||
# Add PLUGIN.md if it exists
|
||||
if [ -f "PLUGIN.md" ]; then
|
||||
|
||||
Reference in New Issue
Block a user