fix: crew start rig inference + refactor overlay to shared utility
Two improvements: 1. gt crew start now infers rig from cwd when first arg is not a valid rig name (gt-czltv). Previously, running `gt crew start bob` from within a rig directory would fail because "bob" was treated as the rig name. Now it checks if the arg is a valid rig first. 2. Refactored copyOverlay to shared rig.CopyOverlay utility: - Eliminates code duplication between crew and polecat managers - Preserves source file permissions instead of hardcoding 0644 - Follows PR #278 improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -253,8 +253,9 @@ func runCrewRefresh(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// runCrewStart starts crew workers in a rig.
|
||||
// args[0] is the rig name (optional if inferrable from cwd)
|
||||
// args[1:] are crew member names (optional - defaults to all if not specified)
|
||||
// If first arg is a valid rig name, it's used as the rig; otherwise rig is inferred from cwd.
|
||||
// Remaining args (or all args if rig is inferred) are crew member names.
|
||||
// Defaults to all crew members if no names specified.
|
||||
func runCrewStart(cmd *cobra.Command, args []string) error {
|
||||
var rigName string
|
||||
var crewNames []string
|
||||
@@ -263,8 +264,16 @@ func runCrewStart(cmd *cobra.Command, args []string) error {
|
||||
// No args - infer rig from cwd
|
||||
rigName = "" // getCrewManager will infer from cwd
|
||||
} else {
|
||||
rigName = args[0]
|
||||
crewNames = args[1:]
|
||||
// Check if first arg is a valid rig name
|
||||
if _, _, err := getRig(args[0]); err == nil {
|
||||
// First arg is a rig name
|
||||
rigName = args[0]
|
||||
crewNames = args[1:]
|
||||
} else {
|
||||
// First arg is not a rig - infer rig from cwd and treat all args as crew names
|
||||
rigName = "" // getCrewManager will infer from cwd
|
||||
crewNames = args
|
||||
}
|
||||
}
|
||||
|
||||
// Get the rig manager and rig (infers from cwd if rigName is empty)
|
||||
|
||||
@@ -176,7 +176,7 @@ func (m *Manager) Add(name string, createBranch bool) (*CrewWorker, error) {
|
||||
|
||||
// Copy overlay files from .runtime/overlay/ to crew root.
|
||||
// This allows services to have .env and other config files at their root.
|
||||
if err := m.copyOverlay(crewPath); err != nil {
|
||||
if err := rig.CopyOverlay(m.rig.Path, crewPath); err != nil {
|
||||
// Non-fatal - log warning but continue
|
||||
fmt.Printf("Warning: could not copy overlay files: %v\n", err)
|
||||
}
|
||||
@@ -577,59 +577,3 @@ func (m *Manager) IsRunning(name string) (bool, error) {
|
||||
sessionID := m.SessionName(name)
|
||||
return t.HasSession(sessionID)
|
||||
}
|
||||
|
||||
// copyOverlay copies files from <rig>/.runtime/overlay/ to the crew worker root.
|
||||
// This allows storing gitignored files (like .env) that services need at their root.
|
||||
// The overlay is copied non-recursively - only files, not subdirectories.
|
||||
//
|
||||
// Structure:
|
||||
//
|
||||
// rig/
|
||||
// .runtime/
|
||||
// overlay/
|
||||
// .env <- Copied to crew root
|
||||
// config.json <- Copied to crew root
|
||||
// crew/
|
||||
// <name>/
|
||||
// .env <- Copied from overlay
|
||||
// config.json <- Copied from overlay
|
||||
func (m *Manager) copyOverlay(crewPath string) error {
|
||||
overlayDir := filepath.Join(m.rig.Path, ".runtime", "overlay")
|
||||
|
||||
// Check if overlay directory exists
|
||||
entries, err := os.ReadDir(overlayDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No overlay directory - not an error, just nothing to copy
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading overlay dir: %w", err)
|
||||
}
|
||||
|
||||
// Copy each file (not directories) from overlay to crew root
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// Skip subdirectories - only copy files at overlay root
|
||||
continue
|
||||
}
|
||||
|
||||
srcPath := filepath.Join(overlayDir, entry.Name())
|
||||
dstPath := filepath.Join(crewPath, entry.Name())
|
||||
|
||||
// Read source file
|
||||
data, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
// Log warning but continue - don't fail spawn for overlay issues
|
||||
fmt.Printf("Warning: could not read overlay file %s: %v\n", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Write to destination
|
||||
if err := os.WriteFile(dstPath, data, 0644); err != nil {
|
||||
fmt.Printf("Warning: could not write overlay file %s: %v\n", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
||||
|
||||
// Copy overlay files from .runtime/overlay/ to polecat root.
|
||||
// This allows services to have .env and other config files at their root.
|
||||
if err := m.copyOverlay(polecatPath); err != nil {
|
||||
if err := rig.CopyOverlay(m.rig.Path, polecatPath); err != nil {
|
||||
// Non-fatal - log warning but continue
|
||||
fmt.Printf("Warning: could not copy overlay files: %v\n", err)
|
||||
}
|
||||
@@ -489,7 +489,7 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
|
||||
}
|
||||
|
||||
// Copy overlay files from .runtime/overlay/ to polecat root.
|
||||
if err := m.copyOverlay(polecatPath); err != nil {
|
||||
if err := rig.CopyOverlay(m.rig.Path, polecatPath); err != nil {
|
||||
fmt.Printf("Warning: could not copy overlay files: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -741,62 +741,6 @@ func (m *Manager) setupSharedBeads(polecatPath string) error {
|
||||
return beads.SetupRedirect(townRoot, polecatPath)
|
||||
}
|
||||
|
||||
// copyOverlay copies files from <rig>/.runtime/overlay/ to the worktree root.
|
||||
// This allows storing gitignored files (like .env) that services need at their root.
|
||||
// The overlay is copied non-recursively - only files, not subdirectories.
|
||||
//
|
||||
// Structure:
|
||||
//
|
||||
// rig/
|
||||
// .runtime/
|
||||
// overlay/
|
||||
// .env <- Copied to polecat root
|
||||
// config.json <- Copied to polecat root
|
||||
// polecats/
|
||||
// <name>/
|
||||
// .env <- Copied from overlay
|
||||
// config.json <- Copied from overlay
|
||||
func (m *Manager) copyOverlay(polecatPath string) error {
|
||||
overlayDir := filepath.Join(m.rig.Path, ".runtime", "overlay")
|
||||
|
||||
// Check if overlay directory exists
|
||||
entries, err := os.ReadDir(overlayDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No overlay directory - not an error, just nothing to copy
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading overlay dir: %w", err)
|
||||
}
|
||||
|
||||
// Copy each file (not directories) from overlay to polecat root
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// Skip subdirectories - only copy files at overlay root
|
||||
continue
|
||||
}
|
||||
|
||||
srcPath := filepath.Join(overlayDir, entry.Name())
|
||||
dstPath := filepath.Join(polecatPath, entry.Name())
|
||||
|
||||
// Read source file
|
||||
data, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
// Log warning but continue - don't fail spawn for overlay issues
|
||||
fmt.Printf("Warning: could not read overlay file %s: %v\n", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Write to destination
|
||||
if err := os.WriteFile(dstPath, data, 0644); err != nil {
|
||||
fmt.Printf("Warning: could not write overlay file %s: %v\n", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupStaleBranches removes orphaned polecat branches that are no longer in use.
|
||||
// This includes:
|
||||
// - Branches for polecats that no longer exist
|
||||
|
||||
86
internal/rig/overlay.go
Normal file
86
internal/rig/overlay.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package rig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// CopyOverlay copies files from <rigPath>/.runtime/overlay/ to the destination path.
|
||||
// This allows storing gitignored files (like .env) that services need at their root.
|
||||
// The overlay is copied non-recursively - only files, not subdirectories.
|
||||
// File permissions from the source are preserved.
|
||||
//
|
||||
// Structure:
|
||||
//
|
||||
// rig/
|
||||
// .runtime/
|
||||
// overlay/
|
||||
// .env <- Copied to destPath
|
||||
// config.json <- Copied to destPath
|
||||
//
|
||||
// Returns nil if the overlay directory doesn't exist (nothing to copy).
|
||||
// Individual file copy failures are logged as warnings but don't stop the process.
|
||||
func CopyOverlay(rigPath, destPath string) error {
|
||||
overlayDir := filepath.Join(rigPath, ".runtime", "overlay")
|
||||
|
||||
// Check if overlay directory exists
|
||||
entries, err := os.ReadDir(overlayDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No overlay directory - not an error, just nothing to copy
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading overlay dir: %w", err)
|
||||
}
|
||||
|
||||
// Copy each file (not directories) from overlay to destination
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// Skip subdirectories - only copy files at overlay root
|
||||
continue
|
||||
}
|
||||
|
||||
srcPath := filepath.Join(overlayDir, entry.Name())
|
||||
dstPath := filepath.Join(destPath, entry.Name())
|
||||
|
||||
if err := copyFilePreserveMode(srcPath, dstPath); err != nil {
|
||||
// Log warning but continue - don't fail spawn for overlay issues
|
||||
fmt.Printf("Warning: could not copy overlay file %s: %v\n", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFilePreserveMode copies a file from src to dst, preserving the source file's permissions.
|
||||
func copyFilePreserveMode(src, dst string) error {
|
||||
// Get source file info for permissions
|
||||
srcInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat source: %w", err)
|
||||
}
|
||||
|
||||
// Open source file
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Create destination file with same permissions
|
||||
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode().Perm())
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination: %w", err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
// Copy contents
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return fmt.Errorf("copy contents: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user