diff --git a/cmd/bd/mol_bond.go b/cmd/bd/mol_bond.go index 58d822c1..02dc03a1 100644 --- a/cmd/bd/mol_bond.go +++ b/cmd/bd/mol_bond.go @@ -32,9 +32,9 @@ Bond types: parallel - B runs alongside A conditional - B runs only if A fails -Ephemeral storage (wisps): - Use --ephemeral to create molecules in .beads-ephemeral/ instead of .beads/. - Ephemeral molecules (wisps) are local-only, gitignored, and not synced. +Wisp storage (ephemeral molecules): + Use --wisp to create molecules in .beads-wisps/ instead of .beads/. + Wisps are local-only, gitignored, and not synced - the "steam" of Gas Town. Use bd mol squash to convert a wisp to a digest in permanent storage. Use bd mol burn to delete a wisp without creating a digest. @@ -43,7 +43,7 @@ Examples: bd mol bond mol-feature mol-deploy --type parallel # Run in parallel bd mol bond mol-feature bd-abc123 # Attach proto to molecule bd mol bond bd-abc123 bd-def456 # Join two molecules - bd mol bond mol-patrol --ephemeral # Create wisp for patrol cycle`, + bd mol bond mol-patrol --wisp # Create wisp for patrol cycle`, Args: cobra.ExactArgs(2), Run: runMolBond, } @@ -78,22 +78,22 @@ func runMolBond(cmd *cobra.Command, args []string) { customTitle, _ := cmd.Flags().GetString("as") dryRun, _ := cmd.Flags().GetBool("dry-run") varFlags, _ := cmd.Flags().GetStringSlice("var") - ephemeral, _ := cmd.Flags().GetBool("ephemeral") + wisp, _ := cmd.Flags().GetBool("wisp") // Determine which store to use for spawning targetStore := store - if ephemeral { - // Open ephemeral storage for wisp creation - ephStore, err := beads.NewEphemeralStorage(ctx) + if wisp { + // Open wisp storage for ephemeral molecule creation + wispStore, err := beads.NewWispStorage(ctx) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to open ephemeral storage: %v\n", err) + fmt.Fprintf(os.Stderr, "Error: failed to open wisp storage: %v\n", err) os.Exit(1) } - defer ephStore.Close() - targetStore = ephStore + defer wispStore.Close() + targetStore = wispStore - // Ensure ephemeral directory is gitignored - if err := beads.EnsureEphemeralGitignore(); err != nil { + // Ensure wisp directory is gitignored + if err := beads.EnsureWispGitignore(); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not update .gitignore: %v\n", err) } } @@ -148,16 +148,16 @@ func runMolBond(cmd *cobra.Command, args []string) { fmt.Printf(" A: %s (%s)\n", issueA.Title, operandType(aIsProto)) fmt.Printf(" B: %s (%s)\n", issueB.Title, operandType(bIsProto)) fmt.Printf(" Bond type: %s\n", bondType) - if ephemeral { - fmt.Printf(" Storage: ephemeral (.beads-ephemeral/)\n") + if wisp { + fmt.Printf(" Storage: wisp (.beads-wisps/)\n") } if aIsProto && bIsProto { fmt.Printf(" Result: compound proto\n") if customTitle != "" { fmt.Printf(" Custom title: %s\n", customTitle) } - if ephemeral { - fmt.Printf(" Note: --ephemeral ignored for proto+proto (templates stay in permanent storage)\n") + if wisp { + fmt.Printf(" Note: --wisp ignored for proto+proto (templates stay in permanent storage)\n") } } else if aIsProto || bIsProto { fmt.Printf(" Result: spawn proto, attach to molecule\n") @@ -187,8 +187,8 @@ func runMolBond(cmd *cobra.Command, args []string) { os.Exit(1) } - // Schedule auto-flush (only for non-ephemeral, ephemeral doesn't sync) - if !ephemeral { + // Schedule auto-flush (only for non-wisp, wisps don't sync) + if !wisp { markDirtyAndScheduleFlush() } @@ -202,8 +202,8 @@ func runMolBond(cmd *cobra.Command, args []string) { if result.Spawned > 0 { fmt.Printf(" Spawned: %d issues\n", result.Spawned) } - if ephemeral { - fmt.Printf(" Storage: ephemeral (wisp)\n") + if wisp { + fmt.Printf(" Storage: wisp (.beads-wisps/)\n") } } @@ -418,7 +418,7 @@ func init() { molBondCmd.Flags().String("as", "", "Custom title for compound proto (proto+proto only)") molBondCmd.Flags().Bool("dry-run", false, "Preview what would be created") molBondCmd.Flags().StringSlice("var", []string{}, "Variable substitution for spawned protos (key=value)") - molBondCmd.Flags().Bool("ephemeral", false, "Create molecule in ephemeral storage (wisp)") + molBondCmd.Flags().Bool("wisp", false, "Create molecule in wisp storage (.beads-wisps/)") molCmd.AddCommand(molBondCmd) } diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 01b7c0a4..f5158dff 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -622,49 +622,50 @@ func FindAllDatabases() []DatabaseInfo { return databases } -// EphemeralDirName is the default name for the ephemeral storage directory. +// WispDirName is the default name for the wisp storage directory. // This directory is a sibling to .beads/ and should be gitignored. -const EphemeralDirName = ".beads-ephemeral" +// Wisps are ephemeral molecules - the "steam" in Gas Town's engine metaphor. +const WispDirName = ".beads-wisps" -// FindEphemeralDir locates or determines the ephemeral storage directory. -// The ephemeral directory is a sibling to the .beads directory. +// FindWispDir locates or determines the wisp storage directory. +// The wisp directory is a sibling to the .beads directory. // -// Returns the path to the ephemeral directory (which may not exist yet). +// Returns the path to the wisp directory (which may not exist yet). // Returns empty string if no .beads directory can be found. -func FindEphemeralDir() string { +func FindWispDir() string { beadsDir := FindBeadsDir() if beadsDir == "" { return "" } - // Ephemeral dir is a sibling to .beads - // e.g., /project/.beads -> /project/.beads-ephemeral + // Wisp dir is a sibling to .beads + // e.g., /project/.beads -> /project/.beads-wisps projectRoot := filepath.Dir(beadsDir) - return filepath.Join(projectRoot, EphemeralDirName) + return filepath.Join(projectRoot, WispDirName) } -// FindEphemeralDatabasePath returns the path to the ephemeral database file. -// Creates the ephemeral directory if it doesn't exist. +// 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. -func FindEphemeralDatabasePath() (string, error) { - ephemeralDir := FindEphemeralDir() - if ephemeralDir == "" { +func FindWispDatabasePath() (string, error) { + wispDir := FindWispDir() + if wispDir == "" { return "", fmt.Errorf("no .beads directory found") } - // Create ephemeral directory if it doesn't exist - if err := os.MkdirAll(ephemeralDir, 0755); err != nil { - return "", fmt.Errorf("creating ephemeral directory: %w", err) + // 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(ephemeralDir, CanonicalDatabaseName), nil + return filepath.Join(wispDir, CanonicalDatabaseName), nil } -// NewEphemeralStorage opens the ephemeral database for wisp storage. +// NewWispStorage opens the wisp database for ephemeral molecule storage. // Creates the database and directory if they don't exist. -// The ephemeral database uses the same schema as the main database. -func NewEphemeralStorage(ctx context.Context) (Storage, error) { - dbPath, err := FindEphemeralDatabasePath() +// The wisp database uses the same schema as the main database. +func NewWispStorage(ctx context.Context) (Storage, error) { + dbPath, err := FindWispDatabasePath() if err != nil { return nil, err } @@ -672,9 +673,9 @@ func NewEphemeralStorage(ctx context.Context) (Storage, error) { return sqlite.New(ctx, dbPath) } -// EnsureEphemeralGitignore ensures the ephemeral directory is gitignored. -// This should be called after creating the ephemeral directory. -func EnsureEphemeralGitignore() error { +// EnsureWispGitignore ensures the wisp directory is gitignored. +// This should be called after creating the wisp directory. +func EnsureWispGitignore() error { beadsDir := FindBeadsDir() if beadsDir == "" { return fmt.Errorf("no .beads directory found") @@ -683,14 +684,14 @@ func EnsureEphemeralGitignore() error { projectRoot := filepath.Dir(beadsDir) gitignorePath := filepath.Join(projectRoot, ".gitignore") - // Check if .gitignore exists and already contains the ephemeral dir + // 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 == EphemeralDirName || line == EphemeralDirName+"/" { + if line == WispDirName || line == WispDirName+"/" { return nil // Already gitignored } } @@ -710,20 +711,20 @@ func EnsureEphemeralGitignore() error { } } - // Add the ephemeral directory - if _, err := f.WriteString(EphemeralDirName + "/\n"); err != nil { + // Add the wisp directory + if _, err := f.WriteString(WispDirName + "/\n"); err != nil { return fmt.Errorf("writing to .gitignore: %w", err) } return nil } -// IsEphemeralDatabase checks if a database path is an ephemeral database. -// Returns true if the database is in a .beads-ephemeral directory. -func IsEphemeralDatabase(dbPath string) bool { +// IsWispDatabase checks if a database path is a wisp database. +// Returns true if the database is in a .beads-wisps directory. +func IsWispDatabase(dbPath string) bool { if dbPath == "" { return false } dir := filepath.Dir(dbPath) - return filepath.Base(dir) == EphemeralDirName + return filepath.Base(dir) == WispDirName } diff --git a/internal/beads/beads_test.go b/internal/beads/beads_test.go index 22b4d531..7d1b4134 100644 --- a/internal/beads/beads_test.go +++ b/internal/beads/beads_test.go @@ -1255,8 +1255,8 @@ func TestFindDatabasePath_WorktreeNoLocalDB(t *testing.T) { } } -// TestFindEphemeralDir tests that FindEphemeralDir returns the correct path -func TestFindEphemeralDir(t *testing.T) { +// TestFindWispDir tests that FindWispDir returns the correct path +func TestFindWispDir(t *testing.T) { // Save original state originalEnv := os.Getenv("BEADS_DIR") defer func() { @@ -1268,7 +1268,7 @@ func TestFindEphemeralDir(t *testing.T) { }() // Create temporary directory with .beads - tmpDir, err := os.MkdirTemp("", "beads-ephemeral-test-*") + tmpDir, err := os.MkdirTemp("", "beads-wisp-test-*") if err != nil { t.Fatal(err) } @@ -1286,22 +1286,22 @@ func TestFindEphemeralDir(t *testing.T) { // Set BEADS_DIR os.Setenv("BEADS_DIR", beadsDir) - // FindEphemeralDir should return sibling directory - result := FindEphemeralDir() - expected := filepath.Join(tmpDir, EphemeralDirName) + // 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("FindEphemeralDir() = %q, want %q", result, expected) + t.Errorf("FindWispDir() = %q, want %q", result, expected) } } -// TestFindEphemeralDir_NoBeadsDir tests that FindEphemeralDir returns empty string +// TestFindWispDir_NoBeadsDir tests that FindWispDir returns empty string // when no .beads directory exists -func TestFindEphemeralDir_NoBeadsDir(t *testing.T) { +func TestFindWispDir_NoBeadsDir(t *testing.T) { // Save original state originalEnv := os.Getenv("BEADS_DIR") defer func() { @@ -1314,7 +1314,7 @@ func TestFindEphemeralDir_NoBeadsDir(t *testing.T) { os.Unsetenv("BEADS_DIR") // Create temporary directory without .beads - tmpDir, err := os.MkdirTemp("", "beads-no-ephemeral-test-*") + tmpDir, err := os.MkdirTemp("", "beads-no-wisp-test-*") if err != nil { t.Fatal(err) } @@ -1322,16 +1322,16 @@ func TestFindEphemeralDir_NoBeadsDir(t *testing.T) { t.Chdir(tmpDir) - // FindEphemeralDir should return empty string - result := FindEphemeralDir() + // FindWispDir should return empty string + result := FindWispDir() if result != "" { - t.Errorf("FindEphemeralDir() = %q, want empty string", result) + t.Errorf("FindWispDir() = %q, want empty string", result) } } -// TestFindEphemeralDatabasePath tests that FindEphemeralDatabasePath creates -// the ephemeral directory and returns the correct database path -func TestFindEphemeralDatabasePath(t *testing.T) { +// 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() { @@ -1343,7 +1343,7 @@ func TestFindEphemeralDatabasePath(t *testing.T) { }() // Create temporary directory with .beads - tmpDir, err := os.MkdirTemp("", "beads-ephdb-test-*") + tmpDir, err := os.MkdirTemp("", "beads-wispdb-test-*") if err != nil { t.Fatal(err) } @@ -1359,32 +1359,32 @@ func TestFindEphemeralDatabasePath(t *testing.T) { os.Setenv("BEADS_DIR", beadsDir) - // FindEphemeralDatabasePath should create directory and return path - result, err := FindEphemeralDatabasePath() + // FindWispDatabasePath should create directory and return path + result, err := FindWispDatabasePath() if err != nil { - t.Fatalf("FindEphemeralDatabasePath() error = %v", err) + t.Fatalf("FindWispDatabasePath() error = %v", err) } - expected := filepath.Join(tmpDir, EphemeralDirName, CanonicalDatabaseName) + expected := filepath.Join(tmpDir, WispDirName, CanonicalDatabaseName) // Resolve symlinks for comparison resultResolved, _ := filepath.EvalSymlinks(result) expectedResolved, _ := filepath.EvalSymlinks(expected) if resultResolved != expectedResolved { - t.Errorf("FindEphemeralDatabasePath() = %q, want %q", result, expected) + t.Errorf("FindWispDatabasePath() = %q, want %q", result, expected) } // Verify the directory was created - ephemeralDir := filepath.Join(tmpDir, EphemeralDirName) - if _, err := os.Stat(ephemeralDir); os.IsNotExist(err) { - t.Errorf("Ephemeral directory was not created: %q", ephemeralDir) + wispDir := filepath.Join(tmpDir, WispDirName) + if _, err := os.Stat(wispDir); os.IsNotExist(err) { + t.Errorf("Wisp directory was not created: %q", wispDir) } } -// TestIsEphemeralDatabase tests that IsEphemeralDatabase correctly identifies -// ephemeral database paths -func TestIsEphemeralDatabase(t *testing.T) { +// TestIsWispDatabase tests that IsWispDatabase correctly identifies +// wisp database paths +func TestIsWispDatabase(t *testing.T) { tests := []struct { name string dbPath string @@ -1401,35 +1401,35 @@ func TestIsEphemeralDatabase(t *testing.T) { expected: false, }, { - name: "ephemeral database", - dbPath: "/project/.beads-ephemeral/beads.db", + name: "wisp database", + dbPath: "/project/.beads-wisps/beads.db", expected: true, }, { - name: "nested ephemeral", - dbPath: "/some/deep/path/.beads-ephemeral/beads.db", + name: "nested wisp", + dbPath: "/some/deep/path/.beads-wisps/beads.db", expected: true, }, { - name: "similar but not ephemeral", - dbPath: "/project/.beads-ephemeral-backup/beads.db", + name: "similar but not wisp", + dbPath: "/project/.beads-wisps-backup/beads.db", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := IsEphemeralDatabase(tt.dbPath) + result := IsWispDatabase(tt.dbPath) if result != tt.expected { - t.Errorf("IsEphemeralDatabase(%q) = %v, want %v", tt.dbPath, result, tt.expected) + t.Errorf("IsWispDatabase(%q) = %v, want %v", tt.dbPath, result, tt.expected) } }) } } -// TestEnsureEphemeralGitignore tests that EnsureEphemeralGitignore correctly -// adds the ephemeral directory to .gitignore -func TestEnsureEphemeralGitignore(t *testing.T) { +// 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() { @@ -1452,12 +1452,12 @@ func TestEnsureEphemeralGitignore(t *testing.T) { }, { name: "already gitignored", - existingContent: ".beads-ephemeral/\n", + existingContent: ".beads-wisps/\n", expectAppend: false, }, { name: "already gitignored without slash", - existingContent: ".beads-ephemeral\n", + existingContent: ".beads-wisps\n", expectAppend: false, }, { @@ -1499,9 +1499,9 @@ func TestEnsureEphemeralGitignore(t *testing.T) { } } - // Call EnsureEphemeralGitignore - if err := EnsureEphemeralGitignore(); err != nil { - t.Fatalf("EnsureEphemeralGitignore() error = %v", err) + // Call EnsureWispGitignore + if err := EnsureWispGitignore(); err != nil { + t.Fatalf("EnsureWispGitignore() error = %v", err) } // Read result @@ -1510,30 +1510,30 @@ func TestEnsureEphemeralGitignore(t *testing.T) { t.Fatalf("Failed to read .gitignore: %v", err) } - // Check if ephemeral dir is in gitignore + // 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 == EphemeralDirName || line == EphemeralDirName+"/" { + if line == WispDirName || line == WispDirName+"/" { hasEntry = true break } } if !hasEntry { - t.Errorf("EnsureEphemeralGitignore() did not add %s to .gitignore", EphemeralDirName) + t.Errorf("EnsureWispGitignore() did not add %s to .gitignore", WispDirName) } // Verify idempotent: calling again should not duplicate - if err := EnsureEphemeralGitignore(); err != nil { - t.Fatalf("EnsureEphemeralGitignore() second call error = %v", err) + if err := EnsureWispGitignore(); err != nil { + t.Fatalf("EnsureWispGitignore() second call error = %v", err) } content2, _ := os.ReadFile(gitignorePath) - count := strings.Count(string(content2), EphemeralDirName) + count := strings.Count(string(content2), WispDirName) if count > 1 { - t.Errorf("EnsureEphemeralGitignore() added duplicate entry (count=%d)", count) + t.Errorf("EnsureWispGitignore() added duplicate entry (count=%d)", count) } }) }