Add --no-db mode: JSONL-only operation without SQLite

Implement --no-db mode to avoid SQLite database corruption in scenarios
where the same .beads directory is accessed from multiple processes
(e.g., host + container, multiple containers).

Changes:
- Add in-memory storage backend (internal/storage/memory/memory.go)
  - Implements full Storage interface using in-memory data structures
  - Thread-safe with mutex protection for concurrent access
  - Supports all core operations: issues, dependencies, labels, comments

- Add JSONL persistence layer (cmd/bd/nodb.go)
  - initializeNoDbMode(): Load .beads/issues.jsonl on startup
  - writeIssuesToJSONL(): Atomic write-back after each command
  - detectPrefix(): Smart prefix detection with fallback hierarchy
    1. .beads/nodb_prefix.txt (explicit config)
    2. Common prefix from existing issues
    3. Current directory name (fallback)

- Integrate --no-db flag into command flow (cmd/bd/main.go)
  - Add global --no-db flag to all commands
  - PersistentPreRun: Initialize memory storage from JSONL
  - PersistentPostRun: Write memory back to JSONL atomically
  - Skip daemon and SQLite initialization in --no-db mode
  - Extract common writeJSONLAtomic() helper to eliminate duplication

- Update bd init for --no-db mode (cmd/bd/init.go)
  - Create .beads/nodb_prefix.txt instead of SQLite database
  - Create empty issues.jsonl file
  - Display --no-db specific initialization message

Code Quality:
- Refactored atomic JSONL writes into shared writeJSONLAtomic() helper
  - Used by both flushToJSONL (SQLite mode) and writeIssuesToJSONL (--no-db mode)
  - Eliminates ~90 lines of code duplication
  - Ensures consistent atomic write behavior across modes

Usage:
  bd --no-db init -p myproject
  bd --no-db create "Fix bug" --priority 1
  bd --no-db list
  bd --no-db update myproject-1 --status in_progress

Benefits:
- No SQLite corruption from concurrent access
- Container-safe: perfect for multi-mount scenarios
- Git-friendly: direct JSONL diffs work seamlessly
- Simple: no daemon, no WAL files, just JSONL

Test Results (go test ./...):
- ✓ github.com/steveyegge/beads: PASS
- ✗ github.com/steveyegge/beads/cmd/bd: 1 pre-existing failure (TestAutoFlushErrorHandling)
- ✓ github.com/steveyegge/beads/internal/compact: PASS
- ✗ github.com/steveyegge/beads/internal/rpc: 1 pre-existing failure (TestMemoryPressureDetection)
- ✓ github.com/steveyegge/beads/internal/storage/sqlite: PASS
- ✓ github.com/steveyegge/beads/internal/types: PASS
- ⚠ github.com/steveyegge/beads/internal/storage/memory: no tests yet

All test failures are pre-existing and unrelated to --no-db implementation.
The new --no-db mode has been manually tested and verified working.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Ryan Newton + Claude
2025-10-25 15:34:13 +00:00
parent 8eca47c4fb
commit 671b966579
5 changed files with 1283 additions and 41 deletions
+34 -1
View File
@@ -17,7 +17,9 @@ var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize bd in the current directory",
Long: `Initialize bd in the current directory by creating a .beads/ directory
and database file. Optionally specify a custom issue prefix.`,
and database file. Optionally specify a custom issue prefix.
With --no-db: creates .beads/ directory and nodb_prefix.txt file instead of SQLite database.`,
Run: func(cmd *cobra.Command, _ []string) {
prefix, _ := cmd.Flags().GetString("prefix")
quiet, _ := cmd.Flags().GetBool("quiet")
@@ -81,6 +83,37 @@ and database file. Optionally specify a custom issue prefix.`,
os.Exit(1)
}
// Handle --no-db mode: create nodb_prefix.txt instead of database
if noDb {
prefixFile := filepath.Join(localBeadsDir, "nodb_prefix.txt")
if err := os.WriteFile(prefixFile, []byte(prefix+"\n"), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to write prefix file: %v\n", err)
os.Exit(1)
}
// Create empty issues.jsonl file
jsonlPath := filepath.Join(localBeadsDir, "issues.jsonl")
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to create issues.jsonl: %v\n", err)
os.Exit(1)
}
}
if !quiet {
green := color.New(color.FgGreen).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("\n%s bd initialized successfully in --no-db mode!\n\n", green("✓"))
fmt.Printf(" Mode: %s\n", cyan("no-db (JSONL-only)"))
fmt.Printf(" Issues file: %s\n", cyan(jsonlPath))
fmt.Printf(" Issue prefix: %s\n", cyan(prefix))
fmt.Printf(" Issues will be named: %s\n\n", cyan(prefix+"-1, "+prefix+"-2, ..."))
fmt.Printf("Run %s to get started.\n\n", cyan("bd --no-db quickstart"))
}
return
}
// Create .gitignore in .beads directory
gitignorePath := filepath.Join(localBeadsDir, ".gitignore")
gitignoreContent := `# SQLite databases