Files
beads/cmd/bd/nodb.go
Steve Yegge d240439868 fix: Resolve Windows test failures and lint warnings
- Fix Windows binary path issues (bd.exe vs bd)
- Skip scripttest on Windows (requires Unix shell)
- Skip file lock tests on Windows (platform locking differences)
- Fix registry tests to use USERPROFILE on Windows
- Fix 8 unparam lint warnings by marking unused params with _

All changes are platform-aware and maintain functionality.

Amp-Thread-ID: https://ampcode.com/threads/T-bc27021a-65db-4b64-a3f3-4e8d7bc8aa0d
Co-authored-by: Amp <amp@ampcode.com>
2025-11-02 08:30:31 -08:00

202 lines
5.0 KiB
Go

package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/storage/memory"
"github.com/steveyegge/beads/internal/types"
)
// initializeNoDbMode sets up in-memory storage from JSONL file
// This is called when --no-db flag is set
func initializeNoDbMode() error {
// Find .beads directory
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
beadsDir := filepath.Join(cwd, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
return fmt.Errorf("no .beads directory found (hint: run 'bd init' first)")
}
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
// Create memory storage
memStore := memory.New(jsonlPath)
// Try to load from JSONL if it exists
if _, err := os.Stat(jsonlPath); err == nil {
issues, err := loadIssuesFromJSONL(jsonlPath)
if err != nil {
return fmt.Errorf("failed to load issues from %s: %w", jsonlPath, err)
}
if err := memStore.LoadFromIssues(issues); err != nil {
return fmt.Errorf("failed to load issues into memory: %w", err)
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: loaded %d issues from %s\n", len(issues), jsonlPath)
}
} else {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: no existing %s, starting with empty database\n", jsonlPath)
}
}
// Detect and set prefix
prefix, err := detectPrefix(beadsDir, memStore)
if err != nil {
return fmt.Errorf("failed to detect prefix: %w", err)
}
ctx := context.Background()
if err := memStore.SetConfig(ctx, "issue_prefix", prefix); err != nil {
return fmt.Errorf("failed to set prefix: %w", err)
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: using prefix '%s'\n", prefix)
}
// Set global store
store = memStore
return nil
}
// loadIssuesFromJSONL reads all issues from a JSONL file
func loadIssuesFromJSONL(path string) ([]*types.Issue, error) {
// nolint:gosec // G304: path is validated JSONL file from findJSONLPath
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var issues []*types.Issue
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
// Skip empty lines
if strings.TrimSpace(line) == "" {
continue
}
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
return nil, fmt.Errorf("line %d: %w", lineNum, err)
}
issues = append(issues, &issue)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return issues, nil
}
// detectPrefix detects the issue prefix to use in --no-db mode
// Priority:
// 1. issue-prefix from config.yaml (if set)
// 2. Common prefix from existing issues (if all share same prefix)
// 3. Current directory name (fallback)
func detectPrefix(_ string, memStore *memory.MemoryStorage) (string, error) {
// Check config.yaml for issue-prefix
configPrefix := config.GetString("issue-prefix")
if configPrefix != "" {
return configPrefix, nil
}
// Check existing issues for common prefix
issues := memStore.GetAllIssues()
if len(issues) > 0 {
// Extract prefix from first issue
firstPrefix := extractIssuePrefix(issues[0].ID)
// Check if all issues share the same prefix
allSame := true
for _, issue := range issues {
if extractIssuePrefix(issue.ID) != firstPrefix {
allSame = false
break
}
}
if allSame && firstPrefix != "" {
return firstPrefix, nil
}
// If issues have mixed prefixes, we can't auto-detect
if !allSame {
return "", fmt.Errorf("issues have mixed prefixes, please set issue-prefix in .beads/config.yaml")
}
}
// Fallback to directory name
cwd, err := os.Getwd()
if err != nil {
return "bd", nil // Ultimate fallback
}
prefix := filepath.Base(cwd)
// Sanitize prefix (remove special characters, use only alphanumeric and hyphens)
prefix = strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
return r
}
if r >= 'A' && r <= 'Z' {
return r + ('a' - 'A') // Convert to lowercase
}
return -1 // Remove character
}, prefix)
if prefix == "" {
prefix = "bd"
}
return prefix, nil
}
// extractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
func extractIssuePrefix(issueID string) string {
idx := strings.LastIndex(issueID, "-")
if idx <= 0 {
return ""
}
return issueID[:idx]
}
// writeIssuesToJSONL writes all issues from memory storage to JSONL file atomically
func writeIssuesToJSONL(memStore *memory.MemoryStorage, beadsDir string) error {
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
// Get all issues from memory storage
issues := memStore.GetAllIssues()
// Write atomically using common helper (handles temp file + rename + permissions)
if _, err := writeJSONLAtomic(jsonlPath, issues); err != nil {
return err
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: wrote %d issues to %s\n", len(issues), jsonlPath)
}
return nil
}