diff --git a/cmd/bd/init.go b/cmd/bd/init.go index 1025fa28..43a39013 100644 --- a/cmd/bd/init.go +++ b/cmd/bd/init.go @@ -1,7 +1,9 @@ package main import ( + "bufio" "context" + "encoding/json" "fmt" "os" "os/exec" @@ -16,6 +18,8 @@ import ( "github.com/steveyegge/beads/internal/configfile" "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/syncbranch" + "github.com/steveyegge/beads/internal/types" + "github.com/steveyegge/beads/internal/utils" ) var initCmd = &cobra.Command{ @@ -47,12 +51,24 @@ With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite } } - // Determine prefix with precedence: flag > config > auto-detect + // Determine prefix with precedence: flag > config > auto-detect from git > auto-detect from directory name if prefix == "" { // Try to get from config file prefix = config.GetString("issue-prefix") } + // auto-detect prefix from first issue in JSONL file + if prefix == "" { + issueCount, jsonlPath := checkGitForIssues() + if issueCount > 0 { + firstIssue, err := readFirstIssueFromJSONL(jsonlPath) + if firstIssue != nil && err == nil { + prefix = utils.ExtractIssuePrefix(firstIssue.ID) + } + } + } + + // auto-detect prefix from directory name if prefix == "" { // Auto-detect from directory name cwd, err := os.Getwd() @@ -969,3 +985,39 @@ func createConfigYaml(beadsDir string, noDbMode bool) error { return nil } + +// readFirstIssueFromJSONL reads the first issue from a JSONL file +func readFirstIssueFromJSONL(path string) (*types.Issue, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open JSONL file: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + lineNum := 0 + for scanner.Scan() { + lineNum++ + line := scanner.Text() + + // skip empty lines + if line == "" { + continue + } + + var issue types.Issue + if err := json.Unmarshal([]byte(line), &issue); err == nil { + return &issue, nil + } else { + // Skip malformed lines with warning + fmt.Fprintf(os.Stderr, "Warning: skipping malformed JSONL line %d: %v\n", lineNum, err) + continue + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading JSONL file: %w", err) + } + + return nil, nil +} diff --git a/cmd/bd/init_test.go b/cmd/bd/init_test.go index 2e203846..da2622dd 100644 --- a/cmd/bd/init_test.go +++ b/cmd/bd/init_test.go @@ -788,3 +788,90 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) { } }) } + +// TestReadFirstIssueFromJSONL_ValidFile verifies reading first issue from valid JSONL +func TestReadFirstIssueFromJSONL_ValidFile(t *testing.T) { + tempDir := t.TempDir() + jsonlPath := filepath.Join(tempDir, "test.jsonl") + + // Create test JSONL file with multiple issues + content := `{"id":"bd-1","title":"First Issue","description":"First test"} +{"id":"bd-2","title":"Second Issue","description":"Second test"} +{"id":"bd-3","title":"Third Issue","description":"Third test"} +` + if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + issue, err := readFirstIssueFromJSONL(jsonlPath) + if err != nil { + t.Fatalf("readFirstIssueFromJSONL failed: %v", err) + } + + if issue == nil { + t.Fatal("Expected non-nil issue, got nil") + } + + // Verify we got the FIRST issue + if issue.ID != "bd-1" { + t.Errorf("Expected ID 'bd-1', got '%s'", issue.ID) + } + if issue.Title != "First Issue" { + t.Errorf("Expected title 'First Issue', got '%s'", issue.Title) + } + if issue.Description != "First test" { + t.Errorf("Expected description 'First test', got '%s'", issue.Description) + } +} + +// TestReadFirstIssueFromJSONL_EmptyLines verifies skipping empty lines +func TestReadFirstIssueFromJSONL_EmptyLines(t *testing.T) { + tempDir := t.TempDir() + jsonlPath := filepath.Join(tempDir, "test.jsonl") + + // Create JSONL with empty lines before first valid issue + content := ` + +{"id":"bd-1","title":"First Valid Issue"} +{"id":"bd-2","title":"Second Issue"} +` + if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + issue, err := readFirstIssueFromJSONL(jsonlPath) + if err != nil { + t.Fatalf("readFirstIssueFromJSONL failed: %v", err) + } + + if issue == nil { + t.Fatal("Expected non-nil issue, got nil") + } + + if issue.ID != "bd-1" { + t.Errorf("Expected ID 'bd-1', got '%s'", issue.ID) + } + if issue.Title != "First Valid Issue" { + t.Errorf("Expected title 'First Valid Issue', got '%s'", issue.Title) + } +} + +// TestReadFirstIssueFromJSONL_EmptyFile verifies handling of empty file +func TestReadFirstIssueFromJSONL_EmptyFile(t *testing.T) { + tempDir := t.TempDir() + jsonlPath := filepath.Join(tempDir, "empty.jsonl") + + // Create empty file + if err := os.WriteFile(jsonlPath, []byte(""), 0o600); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + issue, err := readFirstIssueFromJSONL(jsonlPath) + if err != nil { + t.Fatalf("readFirstIssueFromJSONL should not error on empty file: %v", err) + } + + if issue != nil { + t.Errorf("Expected nil issue for empty file, got %+v", issue) + } +}