refactor: remove deprecated .beads-wisp directory support (bd-bkul)
Wisps now use Wisp=true flag in the main database. The separate .beads-wisp/ directory is no longer needed. Removed: - WispDirName constant - FindWispDir() - FindWispDatabasePath() - NewWispStorage() - EnsureWispGitignore() - IsWispDatabase() - All related tests - .beads-wisp/ from .gitignore
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -117,7 +117,6 @@ history/
|
||||
# Gas Town agent state
|
||||
state.json
|
||||
.beads/redirect
|
||||
.beads-wisp/
|
||||
|
||||
# Claude Code runtime
|
||||
.runtime/
|
||||
|
||||
@@ -616,155 +616,3 @@ func FindAllDatabases() []DatabaseInfo {
|
||||
|
||||
return databases
|
||||
}
|
||||
|
||||
// WispDirName is the default name for the wisp storage directory.
|
||||
// This directory is a sibling to .beads/ and should be gitignored.
|
||||
// Wisps are ephemeral molecules - the "steam" in Gas Town's engine metaphor.
|
||||
//
|
||||
// Deprecated: Wisps are now stored in the main database with Wisp=true.
|
||||
// The separate .beads-wisp/ directory is no longer used ("bd-bkul").
|
||||
const WispDirName = ".beads-wisp"
|
||||
|
||||
// FindWispDir locates or determines the wisp storage directory.
|
||||
// The wisp directory is a sibling to the .beads directory.
|
||||
//
|
||||
// Returns the path to the wisp directory (which may not exist yet).
|
||||
// Returns empty string if no .beads directory can be found.
|
||||
//
|
||||
// Deprecated: Wisps are now stored in the main database with Wisp=true.
|
||||
// The separate .beads-wisp/ directory is no longer used ("bd-bkul").
|
||||
func FindWispDir() string {
|
||||
beadsDir := FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Wisp dir is a sibling to .beads
|
||||
// e.g., /project/.beads -> /project/.beads-wisp
|
||||
projectRoot := filepath.Dir(beadsDir)
|
||||
return filepath.Join(projectRoot, WispDirName)
|
||||
}
|
||||
|
||||
// FindWispDatabasePath returns the path to the wisp database file.
|
||||
// Creates the wisp directory if it doesn't exist.
|
||||
// Returns empty string if no .beads directory can be found.
|
||||
//
|
||||
// Deprecated: Wisps are now stored in the main database with Wisp=true.
|
||||
// The separate .beads-wisp/ directory is no longer used ("bd-bkul").
|
||||
func FindWispDatabasePath() (string, error) {
|
||||
wispDir := FindWispDir()
|
||||
if wispDir == "" {
|
||||
return "", fmt.Errorf("no .beads directory found")
|
||||
}
|
||||
|
||||
// Create wisp directory if it doesn't exist
|
||||
if err := os.MkdirAll(wispDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("creating wisp directory: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(wispDir, CanonicalDatabaseName), nil
|
||||
}
|
||||
|
||||
// NewWispStorage opens the wisp database for ephemeral molecule storage.
|
||||
// Creates the database and directory if they don't exist.
|
||||
// The wisp database uses the same schema as the main database.
|
||||
// Automatically copies issue_prefix from the main beads config if not set.
|
||||
//
|
||||
// Deprecated: Wisps are now stored in the main database with Wisp=true.
|
||||
// The separate .beads-wisp/ directory is no longer used ("bd-bkul").
|
||||
func NewWispStorage(ctx context.Context) (Storage, error) {
|
||||
dbPath, err := FindWispDatabasePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wispStore, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if wisp db has issue_prefix configured
|
||||
prefix, err := wispStore.GetConfig(ctx, "issue_prefix")
|
||||
if err != nil || prefix == "" {
|
||||
// Copy issue_prefix from main beads database
|
||||
mainDBPath := FindDatabasePath()
|
||||
if mainDBPath != "" {
|
||||
mainStore, mainErr := sqlite.New(ctx, mainDBPath)
|
||||
if mainErr == nil {
|
||||
defer func() { _ = mainStore.Close() }()
|
||||
mainPrefix, _ := mainStore.GetConfig(ctx, "issue_prefix")
|
||||
if mainPrefix != "" {
|
||||
if setErr := wispStore.SetConfig(ctx, "issue_prefix", mainPrefix); setErr != nil {
|
||||
_ = wispStore.Close()
|
||||
return nil, fmt.Errorf("setting wisp issue_prefix: %w", setErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wispStore, nil
|
||||
}
|
||||
|
||||
// EnsureWispGitignore ensures the wisp directory is gitignored.
|
||||
// This should be called after creating the wisp directory.
|
||||
//
|
||||
// Deprecated: Wisps are now stored in the main database with Wisp=true.
|
||||
// The separate .beads-wisp/ directory is no longer used ("bd-bkul").
|
||||
func EnsureWispGitignore() error {
|
||||
beadsDir := FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
return fmt.Errorf("no .beads directory found")
|
||||
}
|
||||
|
||||
projectRoot := filepath.Dir(beadsDir)
|
||||
gitignorePath := filepath.Join(projectRoot, ".gitignore")
|
||||
|
||||
// Check if .gitignore exists and already contains the wisp dir
|
||||
content, err := os.ReadFile(gitignorePath)
|
||||
if err == nil {
|
||||
// File exists, check if already gitignored
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == WispDirName || line == WispDirName+"/" {
|
||||
return nil // Already gitignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append to .gitignore (or create it)
|
||||
// #nosec G302 -- .gitignore is a public config file, 0644 is standard
|
||||
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening .gitignore: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Add newline before if file doesn't end with one
|
||||
if len(content) > 0 && content[len(content)-1] != '\n' {
|
||||
if _, err := f.WriteString("\n"); err != nil {
|
||||
return fmt.Errorf("writing to .gitignore: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the wisp directory
|
||||
if _, err := f.WriteString(WispDirName + "/\n"); err != nil {
|
||||
return fmt.Errorf("writing to .gitignore: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsWispDatabase checks if a database path is a wisp database.
|
||||
// Returns true if the database is in a .beads-wisp directory.
|
||||
//
|
||||
// Deprecated: Wisps are now stored in the main database with Wisp=true.
|
||||
// The separate .beads-wisp/ directory is no longer used ("bd-bkul").
|
||||
func IsWispDatabase(dbPath string) bool {
|
||||
if dbPath == "" {
|
||||
return false
|
||||
}
|
||||
dir := filepath.Dir(dbPath)
|
||||
return filepath.Base(dir) == WispDirName
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -1253,288 +1252,4 @@ func TestFindDatabasePath_WorktreeNoLocalDB(t *testing.T) {
|
||||
if resultResolved != mainDBPathResolved {
|
||||
t.Errorf("FindDatabasePath() = %q, want main repo shared db %q", result, mainDBPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindWispDir tests that FindWispDir returns the correct path
|
||||
func TestFindWispDir(t *testing.T) {
|
||||
// Save original state
|
||||
originalEnv := os.Getenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if originalEnv != "" {
|
||||
os.Setenv("BEADS_DIR", originalEnv)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
}()
|
||||
|
||||
// Create temporary directory with .beads
|
||||
tmpDir, err := os.MkdirTemp("", "beads-wisp-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add a project file so it's recognized
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "beads.db"), []byte{}, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set BEADS_DIR
|
||||
os.Setenv("BEADS_DIR", beadsDir)
|
||||
|
||||
// FindWispDir should return sibling directory
|
||||
result := FindWispDir()
|
||||
expected := filepath.Join(tmpDir, WispDirName)
|
||||
|
||||
// Resolve symlinks for comparison
|
||||
resultResolved, _ := filepath.EvalSymlinks(result)
|
||||
expectedResolved, _ := filepath.EvalSymlinks(expected)
|
||||
|
||||
if resultResolved != expectedResolved {
|
||||
t.Errorf("FindWispDir() = %q, want %q", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindWispDir_NoBeadsDir tests that FindWispDir returns empty string
|
||||
// when no .beads directory exists
|
||||
func TestFindWispDir_NoBeadsDir(t *testing.T) {
|
||||
// Save original state
|
||||
originalEnv := os.Getenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if originalEnv != "" {
|
||||
os.Setenv("BEADS_DIR", originalEnv)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
}()
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
|
||||
// Create temporary directory without .beads
|
||||
tmpDir, err := os.MkdirTemp("", "beads-no-wisp-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
// FindWispDir should return empty string
|
||||
result := FindWispDir()
|
||||
if result != "" {
|
||||
t.Errorf("FindWispDir() = %q, want empty string", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindWispDatabasePath tests that FindWispDatabasePath creates
|
||||
// the wisp directory and returns the correct database path
|
||||
func TestFindWispDatabasePath(t *testing.T) {
|
||||
// Save original state
|
||||
originalEnv := os.Getenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if originalEnv != "" {
|
||||
os.Setenv("BEADS_DIR", originalEnv)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
}()
|
||||
|
||||
// Create temporary directory with .beads
|
||||
tmpDir, err := os.MkdirTemp("", "beads-wispdb-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "beads.db"), []byte{}, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
os.Setenv("BEADS_DIR", beadsDir)
|
||||
|
||||
// FindWispDatabasePath should create directory and return path
|
||||
result, err := FindWispDatabasePath()
|
||||
if err != nil {
|
||||
t.Fatalf("FindWispDatabasePath() error = %v", err)
|
||||
}
|
||||
|
||||
expected := filepath.Join(tmpDir, WispDirName, CanonicalDatabaseName)
|
||||
|
||||
// Resolve symlinks for comparison
|
||||
resultResolved, _ := filepath.EvalSymlinks(result)
|
||||
expectedResolved, _ := filepath.EvalSymlinks(expected)
|
||||
|
||||
if resultResolved != expectedResolved {
|
||||
t.Errorf("FindWispDatabasePath() = %q, want %q", result, expected)
|
||||
}
|
||||
|
||||
// Verify the directory was created
|
||||
wispDir := filepath.Join(tmpDir, WispDirName)
|
||||
if _, err := os.Stat(wispDir); os.IsNotExist(err) {
|
||||
t.Errorf("Wisp directory was not created: %q", wispDir)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsWispDatabase tests that IsWispDatabase correctly identifies
|
||||
// wisp database paths
|
||||
func TestIsWispDatabase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dbPath string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "empty path",
|
||||
dbPath: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "regular database",
|
||||
dbPath: "/project/.beads/beads.db",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wisp database",
|
||||
dbPath: "/project/.beads-wisp/beads.db",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "nested wisp",
|
||||
dbPath: "/some/deep/path/.beads-wisp/beads.db",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "similar but not wisp",
|
||||
dbPath: "/project/.beads-wisp-backup/beads.db",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := IsWispDatabase(tt.dbPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsWispDatabase(%q) = %v, want %v", tt.dbPath, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureWispGitignore tests that EnsureWispGitignore correctly
|
||||
// adds the wisp directory to .gitignore
|
||||
func TestEnsureWispGitignore(t *testing.T) {
|
||||
// Save original state
|
||||
originalEnv := os.Getenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if originalEnv != "" {
|
||||
os.Setenv("BEADS_DIR", originalEnv)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
existingContent string
|
||||
expectAppend bool
|
||||
}{
|
||||
{
|
||||
name: "no existing gitignore",
|
||||
existingContent: "",
|
||||
expectAppend: true,
|
||||
},
|
||||
{
|
||||
name: "already gitignored",
|
||||
existingContent: ".beads-wisp/\n",
|
||||
expectAppend: false,
|
||||
},
|
||||
{
|
||||
name: "already gitignored without slash",
|
||||
existingContent: ".beads-wisp\n",
|
||||
expectAppend: false,
|
||||
},
|
||||
{
|
||||
name: "other entries only",
|
||||
existingContent: "node_modules/\n.env\n",
|
||||
expectAppend: true,
|
||||
},
|
||||
{
|
||||
name: "other entries no trailing newline",
|
||||
existingContent: "node_modules/\n.env",
|
||||
expectAppend: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create temporary directory with .beads
|
||||
tmpDir, err := os.MkdirTemp("", "beads-gitignore-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "beads.db"), []byte{}, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
os.Setenv("BEADS_DIR", beadsDir)
|
||||
|
||||
// Create .gitignore if needed
|
||||
gitignorePath := filepath.Join(tmpDir, ".gitignore")
|
||||
if tt.existingContent != "" {
|
||||
if err := os.WriteFile(gitignorePath, []byte(tt.existingContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Call EnsureWispGitignore
|
||||
if err := EnsureWispGitignore(); err != nil {
|
||||
t.Fatalf("EnsureWispGitignore() error = %v", err)
|
||||
}
|
||||
|
||||
// Read result
|
||||
content, err := os.ReadFile(gitignorePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read .gitignore: %v", err)
|
||||
}
|
||||
|
||||
// Check if wisp dir is in gitignore
|
||||
hasEntry := false
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == WispDirName || line == WispDirName+"/" {
|
||||
hasEntry = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEntry {
|
||||
t.Errorf("EnsureWispGitignore() did not add %s to .gitignore", WispDirName)
|
||||
}
|
||||
|
||||
// Verify idempotent: calling again should not duplicate
|
||||
if err := EnsureWispGitignore(); err != nil {
|
||||
t.Fatalf("EnsureWispGitignore() second call error = %v", err)
|
||||
}
|
||||
|
||||
content2, _ := os.ReadFile(gitignorePath)
|
||||
count := strings.Count(string(content2), WispDirName)
|
||||
if count > 1 {
|
||||
t.Errorf("EnsureWispGitignore() added duplicate entry (count=%d)", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user