Files
beads/internal/utils/path.go
Steve Yegge 9480041605 fix: skip interactions.jsonl in FindJSONLInDir (GH#709)
FindJSONLInDir() was returning interactions.jsonl when issues.jsonl
didn't exist. This caused bd sync to write issue data to the wrong
file after fresh init.

Add interactions.jsonl to the skip list alongside deletions.jsonl
and merge artifacts, so the function correctly defaults to issues.jsonl.
2025-12-22 23:37:45 -08:00

117 lines
3.6 KiB
Go

// Package utils provides utility functions for issue ID parsing and path handling.
package utils
import (
"os"
"path/filepath"
)
// FindJSONLInDir finds the JSONL file in the given .beads directory.
// It prefers issues.jsonl over other .jsonl files to prevent accidentally
// reading/writing to deletions.jsonl or merge artifacts (bd-tqo fix).
// Always returns a path (defaults to issues.jsonl if nothing suitable found).
//
// Search order:
// 1. issues.jsonl (canonical name)
// 2. beads.jsonl (legacy support)
// 3. Any other .jsonl file except deletions/merge artifacts
// 4. Default to issues.jsonl
func FindJSONLInDir(dbDir string) string {
pattern := filepath.Join(dbDir, "*.jsonl")
matches, err := filepath.Glob(pattern)
if err != nil || len(matches) == 0 {
// Default to issues.jsonl if glob fails or no matches
return filepath.Join(dbDir, "issues.jsonl")
}
// Prefer issues.jsonl over other .jsonl files (bd-tqo fix)
// This prevents accidentally using deletions.jsonl or merge artifacts
for _, match := range matches {
if filepath.Base(match) == "issues.jsonl" {
return match
}
}
// Fall back to beads.jsonl for legacy support
for _, match := range matches {
if filepath.Base(match) == "beads.jsonl" {
return match
}
}
// Last resort: use first match (but skip deletions.jsonl, interactions.jsonl, and merge artifacts)
for _, match := range matches {
base := filepath.Base(match)
// Skip deletions manifest, interactions (audit trail), and merge artifacts
if base == "deletions.jsonl" ||
base == "interactions.jsonl" ||
base == "beads.base.jsonl" ||
base == "beads.left.jsonl" ||
base == "beads.right.jsonl" {
continue
}
return match
}
// If only deletions/merge files exist, default to issues.jsonl
return filepath.Join(dbDir, "issues.jsonl")
}
// FindMoleculesJSONLInDir finds the molecules.jsonl file in the given .beads directory.
// Returns the path to molecules.jsonl if it exists, empty string otherwise.
// Molecules are template issues used for instantiation (beads-1ra).
func FindMoleculesJSONLInDir(dbDir string) string {
moleculesPath := filepath.Join(dbDir, "molecules.jsonl")
// Check if file exists - we don't fall back to any other file
// because molecules.jsonl is optional and specific
if _, err := os.Stat(moleculesPath); err == nil {
return moleculesPath
}
return ""
}
// ResolveForWrite returns the path to write to, resolving symlinks.
// If path is a symlink, returns the resolved target path.
// If path doesn't exist, returns path unchanged (new file).
func ResolveForWrite(path string) (string, error) {
info, err := os.Lstat(path)
if err != nil {
if os.IsNotExist(err) {
return path, nil
}
return "", err
}
if info.Mode()&os.ModeSymlink != 0 {
return filepath.EvalSymlinks(path)
}
return path, nil
}
// CanonicalizePath converts a path to its canonical form by:
// 1. Converting to absolute path
// 2. Resolving symlinks
//
// If either step fails, it falls back to the best available form:
// - If symlink resolution fails, returns absolute path
// - If absolute path conversion fails, returns original path
//
// This function is used to ensure consistent path handling across the codebase,
// particularly for BEADS_DIR environment variable processing.
func CanonicalizePath(path string) string {
// Try to get absolute path
absPath, err := filepath.Abs(path)
if err != nil {
// If we can't get absolute path, return original
return path
}
// Try to resolve symlinks
canonical, err := filepath.EvalSymlinks(absPath)
if err != nil {
// If we can't resolve symlinks, return absolute path
return absPath
}
return canonical
}