diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index 669747c1..b720a4d7 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -110,12 +110,13 @@ Use --merge to merge the sync branch back to main branch.`, } // If import-only mode, just import and exit + // Use inline import to avoid subprocess path resolution issues with .beads/redirect (bd-ysal) if importOnly { if dryRun { fmt.Println("→ [DRY RUN] Would import from JSONL") } else { fmt.Println("→ Importing from JSONL...") - if err := importFromJSONL(ctx, jsonlPath, renameOnImport, noGitHistory); err != nil { + if err := importFromJSONLInline(ctx, jsonlPath, renameOnImport, noGitHistory); err != nil { FatalError("importing: %v", err) } fmt.Println("✓ Import complete") diff --git a/cmd/bd/sync_import.go b/cmd/bd/sync_import.go index 6bf87fe2..e76690e0 100644 --- a/cmd/bd/sync_import.go +++ b/cmd/bd/sync_import.go @@ -1,10 +1,16 @@ package main import ( + "bufio" "context" + "encoding/json" "fmt" "os" "os/exec" + "time" + + "github.com/steveyegge/beads/internal/debug" + "github.com/steveyegge/beads/internal/types" ) // importFromJSONL imports the JSONL file by running the import command @@ -55,6 +61,91 @@ func importFromJSONL(ctx context.Context, jsonlPath string, renameOnImport bool, return nil } +// importFromJSONLInline imports the JSONL file directly without spawning a subprocess. +// This avoids path resolution issues when running from directories with .beads/redirect. +// The parent process's store and dbPath are used, ensuring consistent path resolution. +// (bd-ysal fix) +func importFromJSONLInline(ctx context.Context, jsonlPath string, renameOnImport bool, noGitHistory bool) error { + // Verify we have an active store + if store == nil { + return fmt.Errorf("no database store available for inline import") + } + + // Read and parse the JSONL file + // #nosec G304 - jsonlPath is from findJSONLPath() which uses trusted paths + f, err := os.Open(jsonlPath) + if err != nil { + return fmt.Errorf("failed to open JSONL file: %w", err) + } + defer func() { _ = f.Close() }() + + var allIssues []*types.Issue + scanner := bufio.NewScanner(f) + lineNum := 0 + + for scanner.Scan() { + lineNum++ + line := scanner.Text() + if line == "" { + continue + } + + var issue types.Issue + if err := json.Unmarshal([]byte(line), &issue); err != nil { + return fmt.Errorf("error parsing line %d: %w", lineNum, err) + } + issue.SetDefaults() + allIssues = append(allIssues, &issue) + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error reading JSONL: %w", err) + } + + // Import using shared logic + opts := ImportOptions{ + RenameOnImport: renameOnImport, + } + result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts) + if err != nil { + return fmt.Errorf("import failed: %w", err) + } + + // Update staleness metadata (same as import.go lines 386-411) + // This is critical: without this, CheckStaleness will still report stale + if currentHash, hashErr := computeJSONLHash(jsonlPath); hashErr == nil { + if err := store.SetMetadata(ctx, "jsonl_content_hash", currentHash); err != nil { + debug.Logf("Warning: failed to update jsonl_content_hash: %v", err) + } + if err := store.SetJSONLFileHash(ctx, currentHash); err != nil { + debug.Logf("Warning: failed to update jsonl_file_hash: %v", err) + } + importTime := time.Now().Format(time.RFC3339Nano) + if err := store.SetMetadata(ctx, "last_import_time", importTime); err != nil { + debug.Logf("Warning: failed to update last_import_time: %v", err) + } + } else { + debug.Logf("Warning: failed to compute JSONL hash: %v", hashErr) + } + + // Update database mtime + if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil { + debug.Logf("Warning: failed to update database mtime: %v", err) + } + + // Print summary + fmt.Fprintf(os.Stderr, "Import complete: %d created, %d updated", result.Created, result.Updated) + if result.Unchanged > 0 { + fmt.Fprintf(os.Stderr, ", %d unchanged", result.Unchanged) + } + if result.Skipped > 0 { + fmt.Fprintf(os.Stderr, ", %d skipped", result.Skipped) + } + fmt.Fprintf(os.Stderr, "\n") + + return nil +} + // resolveNoGitHistoryForFromMain returns the resolved noGitHistory value for sync operations. // When syncing from main (--from-main), noGitHistory is forced to true to prevent creating // incorrect deletion records for locally-created beads that don't exist on main.