feat: add .beads-ephemeral/ creation to gt rig add
During rig initialization, now creates a .beads-ephemeral/ directory: - Initialized as a git repo (for local versioning) - Contains config.yaml with ephemeral: true - Automatically added to rig .gitignore This provides a local-only beads database for runtime tracking of wisps and molecules, separate from the synced .beads/ database. Closes gt-3x0z.1 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ var rigAddCmd = &cobra.Command{
|
||||
This creates a rig container with:
|
||||
- config.json Rig configuration
|
||||
- .beads/ Rig-level issue tracking (initialized)
|
||||
- .beads-ephemeral/ Local runtime tracking (gitignored)
|
||||
- refinery/rig/ Canonical main clone
|
||||
- mayor/rig/ Mayor's working clone
|
||||
- crew/main/ Default human workspace
|
||||
@@ -192,6 +193,7 @@ func runRigAdd(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf(" %s/\n", name)
|
||||
fmt.Printf(" ├── config.json\n")
|
||||
fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix)
|
||||
fmt.Printf(" ├── .beads-ephemeral/ (local runtime tracking)\n")
|
||||
fmt.Printf(" ├── refinery/rig/ (canonical main)\n")
|
||||
fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n")
|
||||
fmt.Printf(" ├── crew/%s/ (your workspace)\n", rigAddCrew)
|
||||
|
||||
@@ -278,6 +278,11 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
|
||||
return nil, fmt.Errorf("initializing beads: %w", err)
|
||||
}
|
||||
|
||||
// Initialize ephemeral beads for wisp/molecule tracking
|
||||
if err := m.initEphemeralBeads(rigPath); err != nil {
|
||||
return nil, fmt.Errorf("initializing ephemeral beads: %w", err)
|
||||
}
|
||||
|
||||
// Register in town config
|
||||
m.config.Rigs[opts.Name] = config.RigEntry{
|
||||
GitURL: opts.GitURL,
|
||||
@@ -351,6 +356,67 @@ func (m *Manager) initBeads(rigPath, prefix string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initEphemeralBeads initializes the ephemeral beads database at rig level.
|
||||
// Ephemeral beads are local-only (no sync-branch) and used for runtime tracking
|
||||
// of wisps and molecules.
|
||||
func (m *Manager) initEphemeralBeads(rigPath string) error {
|
||||
beadsDir := filepath.Join(rigPath, ".beads-ephemeral")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize as a git repo (for local versioning, not for sync)
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = beadsDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("git init: %w", err)
|
||||
}
|
||||
|
||||
// Create ephemeral config (no sync-branch needed)
|
||||
configPath := filepath.Join(beadsDir, "config.yaml")
|
||||
configContent := "ephemeral: true\n# No sync-branch - ephemeral is local only\n"
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add .beads-ephemeral/ to .gitignore if not already present
|
||||
gitignorePath := filepath.Join(rigPath, ".gitignore")
|
||||
return m.ensureGitignoreEntry(gitignorePath, ".beads-ephemeral/")
|
||||
}
|
||||
|
||||
// ensureGitignoreEntry adds an entry to .gitignore if it doesn't already exist.
|
||||
func (m *Manager) ensureGitignoreEntry(gitignorePath, entry string) error {
|
||||
// Read existing content
|
||||
content, err := os.ReadFile(gitignorePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if entry already exists
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == entry {
|
||||
return nil // Already present
|
||||
}
|
||||
}
|
||||
|
||||
// Append entry
|
||||
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return 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 err
|
||||
}
|
||||
}
|
||||
_, err = f.WriteString(entry + "\n")
|
||||
return err
|
||||
}
|
||||
|
||||
// deriveBeadsPrefix generates a beads prefix from a rig name.
|
||||
// Examples: "gastown" -> "gt", "my-project" -> "mp", "foo" -> "foo"
|
||||
func deriveBeadsPrefix(name string) string {
|
||||
|
||||
@@ -192,3 +192,108 @@ func TestRigSummary(t *testing.T) {
|
||||
t.Error("expected HasRefinery = false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitEphemeralBeads(t *testing.T) {
|
||||
root, rigsConfig := setupTestTown(t)
|
||||
manager := NewManager(root, rigsConfig, git.NewGit(root))
|
||||
|
||||
rigPath := filepath.Join(root, "test-rig")
|
||||
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
|
||||
if err := manager.initEphemeralBeads(rigPath); err != nil {
|
||||
t.Fatalf("initEphemeralBeads: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory was created
|
||||
ephemeralPath := filepath.Join(rigPath, ".beads-ephemeral")
|
||||
if _, err := os.Stat(ephemeralPath); os.IsNotExist(err) {
|
||||
t.Error(".beads-ephemeral/ was not created")
|
||||
}
|
||||
|
||||
// Verify it's a git repo
|
||||
gitPath := filepath.Join(ephemeralPath, ".git")
|
||||
if _, err := os.Stat(gitPath); os.IsNotExist(err) {
|
||||
t.Error(".beads-ephemeral/ was not initialized as git repo")
|
||||
}
|
||||
|
||||
// Verify config.yaml was created with ephemeral: true
|
||||
configPath := filepath.Join(ephemeralPath, "config.yaml")
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("reading config.yaml: %v", err)
|
||||
}
|
||||
if string(content) != "ephemeral: true\n# No sync-branch - ephemeral is local only\n" {
|
||||
t.Errorf("config.yaml content = %q, want ephemeral: true with comment", string(content))
|
||||
}
|
||||
|
||||
// Verify .gitignore was updated
|
||||
gitignorePath := filepath.Join(rigPath, ".gitignore")
|
||||
ignoreContent, err := os.ReadFile(gitignorePath)
|
||||
if err != nil {
|
||||
t.Fatalf("reading .gitignore: %v", err)
|
||||
}
|
||||
if string(ignoreContent) != ".beads-ephemeral/\n" {
|
||||
t.Errorf(".gitignore content = %q, want .beads-ephemeral/", string(ignoreContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureGitignoreEntry_AddsEntry(t *testing.T) {
|
||||
root, rigsConfig := setupTestTown(t)
|
||||
manager := NewManager(root, rigsConfig, git.NewGit(root))
|
||||
|
||||
gitignorePath := filepath.Join(root, ".gitignore")
|
||||
|
||||
if err := manager.ensureGitignoreEntry(gitignorePath, ".test-entry/"); err != nil {
|
||||
t.Fatalf("ensureGitignoreEntry: %v", err)
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(gitignorePath)
|
||||
if string(content) != ".test-entry/\n" {
|
||||
t.Errorf("content = %q, want .test-entry/", string(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureGitignoreEntry_DoesNotDuplicate(t *testing.T) {
|
||||
root, rigsConfig := setupTestTown(t)
|
||||
manager := NewManager(root, rigsConfig, git.NewGit(root))
|
||||
|
||||
gitignorePath := filepath.Join(root, ".gitignore")
|
||||
|
||||
// Pre-populate with the entry
|
||||
if err := os.WriteFile(gitignorePath, []byte(".test-entry/\n"), 0644); err != nil {
|
||||
t.Fatalf("writing .gitignore: %v", err)
|
||||
}
|
||||
|
||||
if err := manager.ensureGitignoreEntry(gitignorePath, ".test-entry/"); err != nil {
|
||||
t.Fatalf("ensureGitignoreEntry: %v", err)
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(gitignorePath)
|
||||
if string(content) != ".test-entry/\n" {
|
||||
t.Errorf("content = %q, want single .test-entry/", string(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureGitignoreEntry_AppendsToExisting(t *testing.T) {
|
||||
root, rigsConfig := setupTestTown(t)
|
||||
manager := NewManager(root, rigsConfig, git.NewGit(root))
|
||||
|
||||
gitignorePath := filepath.Join(root, ".gitignore")
|
||||
|
||||
// Pre-populate with existing entries
|
||||
if err := os.WriteFile(gitignorePath, []byte("node_modules/\n*.log\n"), 0644); err != nil {
|
||||
t.Fatalf("writing .gitignore: %v", err)
|
||||
}
|
||||
|
||||
if err := manager.ensureGitignoreEntry(gitignorePath, ".test-entry/"); err != nil {
|
||||
t.Fatalf("ensureGitignoreEntry: %v", err)
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(gitignorePath)
|
||||
expected := "node_modules/\n*.log\n.test-entry/\n"
|
||||
if string(content) != expected {
|
||||
t.Errorf("content = %q, want %q", string(content), expected)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user