fix(autoimport): enable cold-start bootstrap for read-only commands (#977)
After devcontainer restart (cold-start), `bd --no-daemon show` failed to find beads because: 1. Read-only commands skipped auto-import 2. Newly created DB had no issue_prefix set, causing import to fail This fix enables seamless cold-start recovery by: - Allowing read-only commands (show, list, etc.) to auto-bootstrap when JSONL exists but DB doesn't - Setting needsBootstrap flag when falling back from read-only to read-write mode for missing DB - Auto-detecting and setting issue_prefix from JSONL during auto-import when DB is uninitialized Fixes: gt-b09 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,39 @@ func canonicalizeIfRelative(path string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
// detectPrefixFromJSONL extracts the issue prefix from JSONL data.
|
||||
// Returns empty string if prefix cannot be detected.
|
||||
// Used by cold-start bootstrap to initialize the database (GH#b09).
|
||||
func detectPrefixFromJSONL(jsonlData []byte) string {
|
||||
// Parse first issue to extract prefix from its ID
|
||||
scanner := bufio.NewScanner(bytes.NewReader(jsonlData))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var issue struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &issue); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if issue.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract prefix from ID (e.g., "gt-abc" -> "gt", "test-001" -> "test")
|
||||
if idx := strings.Index(issue.ID, "-"); idx > 0 {
|
||||
return issue.ID[:idx]
|
||||
}
|
||||
// No hyphen - use whole ID as prefix
|
||||
return issue.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// autoImportIfNewer checks if JSONL content changed (via hash) and imports if so
|
||||
// Hash-based comparison is git-proof (mtime comparison fails after git pull).
|
||||
// Uses collision detection to prevent silently overwriting local changes.
|
||||
@@ -152,6 +185,34 @@ func autoImportIfNewer() {
|
||||
|
||||
debug.Logf("auto-import triggered (hash changed)")
|
||||
|
||||
// Check if database needs initialization (GH#b09 - cold-start bootstrap)
|
||||
// If issue_prefix is not set, the DB is uninitialized and import will fail.
|
||||
// Auto-detect and set the prefix to enable seamless cold-start recovery.
|
||||
// Note: Use global store directly as cmdCtx.Store may not be synced yet (GH#b09)
|
||||
if store != nil {
|
||||
prefix, prefixErr := store.GetConfig(ctx, "issue_prefix")
|
||||
if prefixErr != nil || prefix == "" {
|
||||
// Database needs initialization - detect prefix from JSONL or directory
|
||||
detectedPrefix := detectPrefixFromJSONL(jsonlData)
|
||||
if detectedPrefix == "" {
|
||||
// Fallback: detect from directory name
|
||||
beadsDir := filepath.Dir(jsonlPath)
|
||||
parentDir := filepath.Dir(beadsDir)
|
||||
detectedPrefix = filepath.Base(parentDir)
|
||||
if detectedPrefix == "." || detectedPrefix == "/" {
|
||||
detectedPrefix = "bd"
|
||||
}
|
||||
}
|
||||
detectedPrefix = strings.TrimRight(detectedPrefix, "-")
|
||||
|
||||
if setErr := store.SetConfig(ctx, "issue_prefix", detectedPrefix); setErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Auto-import: failed to initialize database prefix: %v\n", setErr)
|
||||
return
|
||||
}
|
||||
debug.Logf("auto-import: initialized database with prefix '%s'", detectedPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Git merge conflict markers
|
||||
// Only match if they appear as standalone lines (not embedded in JSON strings)
|
||||
lines := bytes.Split(jsonlData, []byte("\n"))
|
||||
|
||||
@@ -442,7 +442,19 @@ var rootCmd = &cobra.Command{
|
||||
isYamlOnlyConfigOp = true
|
||||
}
|
||||
}
|
||||
if cmd.Name() != "import" && cmd.Name() != "setup" && !isYamlOnlyConfigOp {
|
||||
|
||||
// Allow read-only commands to auto-bootstrap from JSONL (GH#b09)
|
||||
// This enables `bd --no-daemon show` after cold-start when DB is missing
|
||||
canAutoBootstrap := false
|
||||
if isReadOnlyCommand(cmd.Name()) && beadsDir != "" {
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if _, err := os.Stat(jsonlPath); err == nil {
|
||||
canAutoBootstrap = true
|
||||
debug.Logf("cold-start bootstrap: JSONL exists, allowing auto-create for %s", cmd.Name())
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Name() != "import" && cmd.Name() != "setup" && !isYamlOnlyConfigOp && !canAutoBootstrap {
|
||||
// No database found - provide context-aware error message
|
||||
fmt.Fprintf(os.Stderr, "Error: no beads database found\n")
|
||||
|
||||
@@ -707,6 +719,7 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// Fall back to direct storage access
|
||||
var err error
|
||||
var needsBootstrap bool // Track if DB needs initial import (GH#b09)
|
||||
if useReadOnly {
|
||||
// Read-only mode: prevents file modifications (GH#804)
|
||||
store, err = sqlite.NewReadOnlyWithTimeout(rootCtx, dbPath, lockTimeout)
|
||||
@@ -715,6 +728,7 @@ var rootCmd = &cobra.Command{
|
||||
// This handles the case where user runs "bd list" before "bd init"
|
||||
debug.Logf("read-only open failed, falling back to read-write: %v", err)
|
||||
store, err = sqlite.NewWithTimeout(rootCtx, dbPath, lockTimeout)
|
||||
needsBootstrap = true // New DB needs auto-import (GH#b09)
|
||||
}
|
||||
} else {
|
||||
store, err = sqlite.NewWithTimeout(rootCtx, dbPath, lockTimeout)
|
||||
@@ -760,7 +774,9 @@ var rootCmd = &cobra.Command{
|
||||
// Skip for delete command to prevent resurrection of deleted issues
|
||||
// Skip if sync --dry-run to avoid modifying DB in dry-run mode
|
||||
// Skip for read-only commands - they can't write anyway (GH#804)
|
||||
if cmd.Name() != "import" && cmd.Name() != "delete" && autoImportEnabled && !useReadOnly {
|
||||
// Exception: allow auto-import for read-only commands that fell back to
|
||||
// read-write mode due to missing DB (needsBootstrap) - fixes GH#b09
|
||||
if cmd.Name() != "import" && cmd.Name() != "delete" && autoImportEnabled && (!useReadOnly || needsBootstrap) {
|
||||
// Check if this is sync command with --dry-run flag
|
||||
if cmd.Name() == "sync" {
|
||||
if dryRun, _ := cmd.Flags().GetBool("dry-run"); dryRun {
|
||||
|
||||
Reference in New Issue
Block a user