Merge gt-cik.2-furiosa: add gt crew add command
This commit is contained in:
156
internal/cmd/crew.go
Normal file
156
internal/cmd/crew.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
"github.com/steveyegge/gastown/internal/crew"
|
||||||
|
"github.com/steveyegge/gastown/internal/git"
|
||||||
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Crew command flags
|
||||||
|
var (
|
||||||
|
crewRig string
|
||||||
|
crewBranch bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var crewCmd = &cobra.Command{
|
||||||
|
Use: "crew",
|
||||||
|
Short: "Manage crew workspaces (user-managed persistent workspaces)",
|
||||||
|
Long: `Crew workers are user-managed persistent workspaces within a rig.
|
||||||
|
|
||||||
|
Unlike polecats which are witness-managed and ephemeral, crew workers are:
|
||||||
|
- Persistent: Not auto-garbage-collected
|
||||||
|
- User-managed: Overseer controls lifecycle
|
||||||
|
- Long-lived identities: recognizable names like dave, emma, fred
|
||||||
|
- Gas Town integrated: Mail, handoff mechanics work
|
||||||
|
- Tmux optional: Can work in terminal directly
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
gt crew add <name> Create a new crew workspace
|
||||||
|
gt crew list List crew workspaces
|
||||||
|
gt crew remove <name> Remove a crew workspace`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var crewAddCmd = &cobra.Command{
|
||||||
|
Use: "add <name>",
|
||||||
|
Short: "Create a new crew workspace",
|
||||||
|
Long: `Create a new crew workspace with a clone of the rig repository.
|
||||||
|
|
||||||
|
The workspace is created at <rig>/crew/<name>/ with:
|
||||||
|
- A full git clone of the project repository
|
||||||
|
- Mail directory for message delivery
|
||||||
|
- CLAUDE.md with crew worker prompting
|
||||||
|
- Optional feature branch (crew/<name>)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt crew add dave # Create in current rig
|
||||||
|
gt crew add emma --rig gastown # Create in specific rig
|
||||||
|
gt crew add fred --branch # Create with feature branch`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runCrewAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
crewAddCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to create crew workspace in")
|
||||||
|
crewAddCmd.Flags().BoolVar(&crewBranch, "branch", false, "Create a feature branch (crew/<name>)")
|
||||||
|
|
||||||
|
crewCmd.AddCommand(crewAddCmd)
|
||||||
|
rootCmd.AddCommand(crewCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCrewAdd(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
// Find workspace
|
||||||
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load rigs config
|
||||||
|
rigsConfigPath := filepath.Join(townRoot, "config", "rigs.json")
|
||||||
|
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine rig
|
||||||
|
rigName := crewRig
|
||||||
|
if rigName == "" {
|
||||||
|
// Try to infer from cwd
|
||||||
|
rigName, err = inferRigFromCwd(townRoot)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not determine rig (use --rig flag): %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rig
|
||||||
|
g := git.NewGit(townRoot)
|
||||||
|
rigMgr := rig.NewManager(townRoot, rigsConfig, g)
|
||||||
|
r, err := rigMgr.GetRig(rigName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rig '%s' not found", rigName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create crew manager
|
||||||
|
crewGit := git.NewGit(r.Path)
|
||||||
|
crewMgr := crew.NewManager(r, crewGit)
|
||||||
|
|
||||||
|
// Create crew workspace
|
||||||
|
fmt.Printf("Creating crew workspace %s in %s...\n", name, rigName)
|
||||||
|
|
||||||
|
worker, err := crewMgr.Add(name, crewBranch)
|
||||||
|
if err != nil {
|
||||||
|
if err == crew.ErrCrewExists {
|
||||||
|
return fmt.Errorf("crew workspace '%s' already exists", name)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("creating crew workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Created crew workspace: %s/%s\n",
|
||||||
|
style.Bold.Render("✓"), rigName, name)
|
||||||
|
fmt.Printf(" Path: %s\n", worker.ClonePath)
|
||||||
|
fmt.Printf(" Branch: %s\n", worker.Branch)
|
||||||
|
fmt.Printf(" Mail: %s/mail/\n", worker.ClonePath)
|
||||||
|
|
||||||
|
fmt.Printf("\n%s\n", style.Dim.Render("Start working with: cd "+worker.ClonePath))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inferRigFromCwd tries to determine the rig from the current directory.
|
||||||
|
func inferRigFromCwd(townRoot string) (string, error) {
|
||||||
|
cwd, err := filepath.Abs(".")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cwd is within a rig
|
||||||
|
rel, err := filepath.Rel(townRoot, cwd)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("not in workspace")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First component should be the rig name
|
||||||
|
parts := filepath.SplitList(rel)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
// Split on path separator instead
|
||||||
|
for i := 0; i < len(rel); i++ {
|
||||||
|
if rel[i] == filepath.Separator {
|
||||||
|
return rel[:i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No separator found, entire rel is the rig name
|
||||||
|
if rel != "" && rel != "." {
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not infer rig from current directory")
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
|
|
||||||
// Common errors
|
// Common errors
|
||||||
var (
|
var (
|
||||||
ErrWorkerExists = errors.New("crew worker already exists")
|
ErrCrewExists = errors.New("crew worker already exists")
|
||||||
ErrWorkerNotFound = errors.New("crew worker not found")
|
ErrCrewNotFound = errors.New("crew worker not found")
|
||||||
ErrHasChanges = errors.New("crew worker has uncommitted changes")
|
ErrHasChanges = errors.New("crew worker has uncommitted changes")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,121 +33,176 @@ func NewManager(r *rig.Rig, g *git.Git) *Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// workerDir returns the directory for a crew worker.
|
// crewDir returns the directory for a crew worker.
|
||||||
func (m *Manager) workerDir(name string) string {
|
func (m *Manager) crewDir(name string) string {
|
||||||
return filepath.Join(m.rig.Path, "crew", name)
|
return filepath.Join(m.rig.Path, "crew", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stateFile returns the state file path for a crew worker.
|
// stateFile returns the state file path for a crew worker.
|
||||||
func (m *Manager) stateFile(name string) string {
|
func (m *Manager) stateFile(name string) string {
|
||||||
return filepath.Join(m.workerDir(name), "state.json")
|
return filepath.Join(m.crewDir(name), "state.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// mailDir returns the mail directory path for a crew worker.
|
||||||
|
func (m *Manager) mailDir(name string) string {
|
||||||
|
return filepath.Join(m.crewDir(name), "mail")
|
||||||
}
|
}
|
||||||
|
|
||||||
// exists checks if a crew worker exists.
|
// exists checks if a crew worker exists.
|
||||||
func (m *Manager) exists(name string) bool {
|
func (m *Manager) exists(name string) bool {
|
||||||
_, err := os.Stat(m.workerDir(name))
|
_, err := os.Stat(m.crewDir(name))
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add creates a new crew worker with a clone of the rig.
|
// Add creates a new crew worker with a clone of the rig.
|
||||||
func (m *Manager) Add(name string) (*Worker, error) {
|
func (m *Manager) Add(name string, createBranch bool) (*CrewWorker, error) {
|
||||||
if m.exists(name) {
|
if m.exists(name) {
|
||||||
return nil, ErrWorkerExists
|
return nil, ErrCrewExists
|
||||||
}
|
}
|
||||||
|
|
||||||
workerPath := m.workerDir(name)
|
crewPath := m.crewDir(name)
|
||||||
|
|
||||||
// Create crew directory if needed
|
// Create crew directory if needed
|
||||||
crewDir := filepath.Join(m.rig.Path, "crew")
|
crewBaseDir := filepath.Join(m.rig.Path, "crew")
|
||||||
if err := os.MkdirAll(crewDir, 0755); err != nil {
|
if err := os.MkdirAll(crewBaseDir, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("creating crew dir: %w", err)
|
return nil, fmt.Errorf("creating crew dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone the rig repo
|
// Clone the rig repo
|
||||||
if err := m.git.Clone(m.rig.GitURL, workerPath); err != nil {
|
if err := m.git.Clone(m.rig.GitURL, crewPath); err != nil {
|
||||||
return nil, fmt.Errorf("cloning rig: %w", err)
|
return nil, fmt.Errorf("cloning rig: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crewGit := git.NewGit(crewPath)
|
||||||
|
branchName := "main"
|
||||||
|
|
||||||
|
// Optionally create a working branch
|
||||||
|
if createBranch {
|
||||||
|
branchName = fmt.Sprintf("crew/%s", name)
|
||||||
|
if err := crewGit.CreateBranch(branchName); err != nil {
|
||||||
|
os.RemoveAll(crewPath)
|
||||||
|
return nil, fmt.Errorf("creating branch: %w", err)
|
||||||
|
}
|
||||||
|
if err := crewGit.Checkout(branchName); err != nil {
|
||||||
|
os.RemoveAll(crewPath)
|
||||||
|
return nil, fmt.Errorf("checking out branch: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mail directory for mail delivery
|
||||||
|
mailPath := m.mailDir(name)
|
||||||
|
if err := os.MkdirAll(mailPath, 0755); err != nil {
|
||||||
|
os.RemoveAll(crewPath)
|
||||||
|
return nil, fmt.Errorf("creating mail dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CLAUDE.md with crew worker prompting
|
||||||
|
if err := m.createClaudeMD(name, crewPath); err != nil {
|
||||||
|
os.RemoveAll(crewPath)
|
||||||
|
return nil, fmt.Errorf("creating CLAUDE.md: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create crew worker state
|
// Create crew worker state
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
worker := &Worker{
|
crew := &CrewWorker{
|
||||||
Name: name,
|
Name: name,
|
||||||
Rig: m.rig.Name,
|
Rig: m.rig.Name,
|
||||||
State: StateActive,
|
ClonePath: crewPath,
|
||||||
ClonePath: workerPath,
|
Branch: branchName,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save state
|
// Save state
|
||||||
if err := m.saveState(worker); err != nil {
|
if err := m.saveState(crew); err != nil {
|
||||||
os.RemoveAll(workerPath)
|
os.RemoveAll(crewPath)
|
||||||
return nil, fmt.Errorf("saving state: %w", err)
|
return nil, fmt.Errorf("saving state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return worker, nil
|
return crew, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddWithConfig creates a new crew worker with custom configuration.
|
// createClaudeMD creates the CLAUDE.md file for crew worker prompting.
|
||||||
func (m *Manager) AddWithConfig(name string, beadsDir string) (*Worker, error) {
|
func (m *Manager) createClaudeMD(name, crewPath string) error {
|
||||||
worker, err := m.Add(name)
|
content := fmt.Sprintf(`# Claude: Crew Worker - %s
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update with custom config
|
You are a **crew worker** in the %s rig. Crew workers are user-managed persistent workspaces.
|
||||||
if beadsDir != "" {
|
|
||||||
worker.BeadsDir = beadsDir
|
|
||||||
if err := m.saveState(worker); err != nil {
|
|
||||||
return nil, fmt.Errorf("saving config: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return worker, nil
|
## Key Differences from Polecats
|
||||||
|
|
||||||
|
- **User-managed**: You are NOT managed by the Witness daemon
|
||||||
|
- **Persistent**: Your workspace is not automatically cleaned up
|
||||||
|
- **Optional issue assignment**: You can work without a beads issue
|
||||||
|
- **Mail enabled**: You can send and receive mail
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Check mail:
|
||||||
|
`+"```bash"+`
|
||||||
|
town mail inbox --as %s/%s
|
||||||
|
`+"```"+`
|
||||||
|
|
||||||
|
Send mail:
|
||||||
|
`+"```bash"+`
|
||||||
|
town mail send <recipient> -s "Subject" -m "Message" --as %s/%s
|
||||||
|
`+"```"+`
|
||||||
|
|
||||||
|
## Session Cycling (Handoff)
|
||||||
|
|
||||||
|
When your context fills up, use mail-to-self for handoff:
|
||||||
|
|
||||||
|
1. Compose a handoff note with current state
|
||||||
|
2. Send to yourself: `+"```"+`town mail send %s/%s -s "Handoff" -m "..."--as %s/%s`+"```"+`
|
||||||
|
3. Exit cleanly
|
||||||
|
4. New session reads handoff from inbox
|
||||||
|
|
||||||
|
## Beads
|
||||||
|
|
||||||
|
If using beads for task tracking:
|
||||||
|
`+"```bash"+`
|
||||||
|
bd ready # Find available work
|
||||||
|
bd show <id> # Review issue details
|
||||||
|
bd update <id> --status=in_progress # Claim it
|
||||||
|
bd close <id> # Mark complete
|
||||||
|
`+"```"+`
|
||||||
|
`, name, m.rig.Name,
|
||||||
|
m.rig.Name, name,
|
||||||
|
m.rig.Name, name,
|
||||||
|
m.rig.Name, name, m.rig.Name, name)
|
||||||
|
|
||||||
|
claudePath := filepath.Join(crewPath, "CLAUDE.md")
|
||||||
|
return os.WriteFile(claudePath, []byte(content), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove deletes a crew worker.
|
// Remove deletes a crew worker.
|
||||||
func (m *Manager) Remove(name string) error {
|
func (m *Manager) Remove(name string, force bool) error {
|
||||||
if !m.exists(name) {
|
if !m.exists(name) {
|
||||||
return ErrWorkerNotFound
|
return ErrCrewNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
workerPath := m.workerDir(name)
|
crewPath := m.crewDir(name)
|
||||||
workerGit := git.NewGit(workerPath)
|
|
||||||
|
|
||||||
// Check for uncommitted changes
|
if !force {
|
||||||
hasChanges, err := workerGit.HasUncommittedChanges()
|
crewGit := git.NewGit(crewPath)
|
||||||
|
hasChanges, err := crewGit.HasUncommittedChanges()
|
||||||
if err == nil && hasChanges {
|
if err == nil && hasChanges {
|
||||||
return ErrHasChanges
|
return ErrHasChanges
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove directory
|
// Remove directory
|
||||||
if err := os.RemoveAll(workerPath); err != nil {
|
if err := os.RemoveAll(crewPath); err != nil {
|
||||||
return fmt.Errorf("removing crew worker dir: %w", err)
|
return fmt.Errorf("removing crew dir: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveForce deletes a crew worker even with uncommitted changes.
|
|
||||||
func (m *Manager) RemoveForce(name string) error {
|
|
||||||
if !m.exists(name) {
|
|
||||||
return ErrWorkerNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
workerPath := m.workerDir(name)
|
|
||||||
if err := os.RemoveAll(workerPath); err != nil {
|
|
||||||
return fmt.Errorf("removing crew worker dir: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all crew workers in the rig.
|
// List returns all crew workers in the rig.
|
||||||
func (m *Manager) List() ([]*Worker, error) {
|
func (m *Manager) List() ([]*CrewWorker, error) {
|
||||||
crewDir := filepath.Join(m.rig.Path, "crew")
|
crewBaseDir := filepath.Join(m.rig.Path, "crew")
|
||||||
|
|
||||||
entries, err := os.ReadDir(crewDir)
|
entries, err := os.ReadDir(crewBaseDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -155,7 +210,7 @@ func (m *Manager) List() ([]*Worker, error) {
|
|||||||
return nil, fmt.Errorf("reading crew dir: %w", err)
|
return nil, fmt.Errorf("reading crew dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var workers []*Worker
|
var workers []*CrewWorker
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
@@ -172,48 +227,22 @@ func (m *Manager) List() ([]*Worker, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get returns a specific crew worker by name.
|
// Get returns a specific crew worker by name.
|
||||||
func (m *Manager) Get(name string) (*Worker, error) {
|
func (m *Manager) Get(name string) (*CrewWorker, error) {
|
||||||
if !m.exists(name) {
|
if !m.exists(name) {
|
||||||
return nil, ErrWorkerNotFound
|
return nil, ErrCrewNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.loadState(name)
|
return m.loadState(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetState updates a crew worker's state.
|
|
||||||
func (m *Manager) SetState(name string, state State) error {
|
|
||||||
worker, err := m.Get(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
worker.State = state
|
|
||||||
worker.UpdatedAt = time.Now()
|
|
||||||
|
|
||||||
return m.saveState(worker)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBeadsDir updates the custom beads directory for a crew worker.
|
|
||||||
func (m *Manager) SetBeadsDir(name, beadsDir string) error {
|
|
||||||
worker, err := m.Get(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
worker.BeadsDir = beadsDir
|
|
||||||
worker.UpdatedAt = time.Now()
|
|
||||||
|
|
||||||
return m.saveState(worker)
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveState persists crew worker state to disk.
|
// saveState persists crew worker state to disk.
|
||||||
func (m *Manager) saveState(worker *Worker) error {
|
func (m *Manager) saveState(crew *CrewWorker) error {
|
||||||
data, err := json.MarshalIndent(worker, "", " ")
|
data, err := json.MarshalIndent(crew, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshaling state: %w", err)
|
return fmt.Errorf("marshaling state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stateFile := m.stateFile(worker.Name)
|
stateFile := m.stateFile(crew.Name)
|
||||||
if err := os.WriteFile(stateFile, data, 0644); err != nil {
|
if err := os.WriteFile(stateFile, data, 0644); err != nil {
|
||||||
return fmt.Errorf("writing state: %w", err)
|
return fmt.Errorf("writing state: %w", err)
|
||||||
}
|
}
|
||||||
@@ -222,49 +251,26 @@ func (m *Manager) saveState(worker *Worker) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadState reads crew worker state from disk.
|
// loadState reads crew worker state from disk.
|
||||||
func (m *Manager) loadState(name string) (*Worker, error) {
|
func (m *Manager) loadState(name string) (*CrewWorker, error) {
|
||||||
stateFile := m.stateFile(name)
|
stateFile := m.stateFile(name)
|
||||||
|
|
||||||
data, err := os.ReadFile(stateFile)
|
data, err := os.ReadFile(stateFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// Return minimal worker if state file missing
|
// Return minimal crew worker if state file missing
|
||||||
return &Worker{
|
return &CrewWorker{
|
||||||
Name: name,
|
Name: name,
|
||||||
Rig: m.rig.Name,
|
Rig: m.rig.Name,
|
||||||
State: StateActive,
|
ClonePath: m.crewDir(name),
|
||||||
ClonePath: m.workerDir(name),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("reading state: %w", err)
|
return nil, fmt.Errorf("reading state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var worker Worker
|
var crew CrewWorker
|
||||||
if err := json.Unmarshal(data, &worker); err != nil {
|
if err := json.Unmarshal(data, &crew); err != nil {
|
||||||
return nil, fmt.Errorf("parsing state: %w", err)
|
return nil, fmt.Errorf("parsing state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &worker, nil
|
return &crew, nil
|
||||||
}
|
|
||||||
|
|
||||||
// Names returns just the names of all crew workers.
|
|
||||||
func (m *Manager) Names() ([]string, error) {
|
|
||||||
crewDir := filepath.Join(m.rig.Path, "crew")
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(crewDir)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("reading crew dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var names []string
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
names = append(names, entry.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return names, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package crew
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -9,194 +10,281 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestManager_workerDir(t *testing.T) {
|
func TestManagerAddAndGet(t *testing.T) {
|
||||||
r := &rig.Rig{
|
// Create temp directory for test
|
||||||
Name: "test-rig",
|
tmpDir, err := os.MkdirTemp("", "crew-test-*")
|
||||||
Path: "/tmp/test-rig",
|
|
||||||
}
|
|
||||||
m := NewManager(r, nil)
|
|
||||||
|
|
||||||
got := m.workerDir("alice")
|
|
||||||
want := "/tmp/test-rig/crew/alice"
|
|
||||||
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("workerDir() = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_stateFile(t *testing.T) {
|
|
||||||
r := &rig.Rig{
|
|
||||||
Name: "test-rig",
|
|
||||||
Path: "/tmp/test-rig",
|
|
||||||
}
|
|
||||||
m := NewManager(r, nil)
|
|
||||||
|
|
||||||
got := m.stateFile("bob")
|
|
||||||
want := "/tmp/test-rig/crew/bob/state.json"
|
|
||||||
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("stateFile() = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_exists(t *testing.T) {
|
|
||||||
// Create temp directory structure
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
crewDir := filepath.Join(tmpDir, "crew", "existing-worker")
|
|
||||||
if err := os.MkdirAll(crewDir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &rig.Rig{
|
|
||||||
Name: "test-rig",
|
|
||||||
Path: tmpDir,
|
|
||||||
}
|
|
||||||
m := NewManager(r, nil)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
worker string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"existing worker", "existing-worker", true},
|
|
||||||
{"non-existing worker", "non-existing", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := m.exists(tt.worker)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("exists(%q) = %v, want %v", tt.worker, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_List_Empty(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
r := &rig.Rig{
|
|
||||||
Name: "test-rig",
|
|
||||||
Path: tmpDir,
|
|
||||||
}
|
|
||||||
m := NewManager(r, git.NewGit(tmpDir))
|
|
||||||
|
|
||||||
workers, err := m.List()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("List() error = %v", err)
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Create a mock rig
|
||||||
|
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||||
|
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create rig dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize git repo for the rig
|
||||||
|
g := git.NewGit(rigPath)
|
||||||
|
|
||||||
|
// For testing, we need a git URL - use a local bare repo
|
||||||
|
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
|
||||||
|
cmd := []string{"git", "init", "--bare", bareRepoPath}
|
||||||
|
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
|
||||||
|
t.Fatalf("failed to create bare repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &rig.Rig{
|
||||||
|
Name: "test-rig",
|
||||||
|
Path: rigPath,
|
||||||
|
GitURL: bareRepoPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := NewManager(r, g)
|
||||||
|
|
||||||
|
// Test Add
|
||||||
|
worker, err := mgr.Add("dave", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if worker.Name != "dave" {
|
||||||
|
t.Errorf("expected name 'dave', got '%s'", worker.Name)
|
||||||
|
}
|
||||||
|
if worker.Rig != "test-rig" {
|
||||||
|
t.Errorf("expected rig 'test-rig', got '%s'", worker.Rig)
|
||||||
|
}
|
||||||
|
if worker.Branch != "main" {
|
||||||
|
t.Errorf("expected branch 'main', got '%s'", worker.Branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify directory structure
|
||||||
|
crewDir := filepath.Join(rigPath, "crew", "dave")
|
||||||
|
if _, err := os.Stat(crewDir); os.IsNotExist(err) {
|
||||||
|
t.Error("crew directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
mailDir := filepath.Join(crewDir, "mail")
|
||||||
|
if _, err := os.Stat(mailDir); os.IsNotExist(err) {
|
||||||
|
t.Error("mail directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
claudeMD := filepath.Join(crewDir, "CLAUDE.md")
|
||||||
|
if _, err := os.Stat(claudeMD); os.IsNotExist(err) {
|
||||||
|
t.Error("CLAUDE.md was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFile := filepath.Join(crewDir, "state.json")
|
||||||
|
if _, err := os.Stat(stateFile); os.IsNotExist(err) {
|
||||||
|
t.Error("state.json was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Get
|
||||||
|
retrieved, err := mgr.Get("dave")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if retrieved.Name != "dave" {
|
||||||
|
t.Errorf("expected name 'dave', got '%s'", retrieved.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test duplicate Add
|
||||||
|
_, err = mgr.Add("dave", false)
|
||||||
|
if err != ErrCrewExists {
|
||||||
|
t.Errorf("expected ErrCrewExists, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Get non-existent
|
||||||
|
_, err = mgr.Get("nonexistent")
|
||||||
|
if err != ErrCrewNotFound {
|
||||||
|
t.Errorf("expected ErrCrewNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagerAddWithBranch(t *testing.T) {
|
||||||
|
// Create temp directory for test
|
||||||
|
tmpDir, err := os.MkdirTemp("", "crew-test-branch-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Create a mock rig
|
||||||
|
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||||
|
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create rig dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := git.NewGit(rigPath)
|
||||||
|
|
||||||
|
// Create a local repo with initial commit for branch testing
|
||||||
|
sourceRepoPath := filepath.Join(tmpDir, "source-repo")
|
||||||
|
if err := os.MkdirAll(sourceRepoPath, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create source repo dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize source repo with a commit
|
||||||
|
cmds := [][]string{
|
||||||
|
{"git", "-C", sourceRepoPath, "init"},
|
||||||
|
{"git", "-C", sourceRepoPath, "config", "user.email", "test@test.com"},
|
||||||
|
{"git", "-C", sourceRepoPath, "config", "user.name", "Test"},
|
||||||
|
}
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
|
||||||
|
t.Fatalf("failed to run %v: %v", cmd, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create initial file and commit
|
||||||
|
testFile := filepath.Join(sourceRepoPath, "README.md")
|
||||||
|
if err := os.WriteFile(testFile, []byte("# Test"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds = [][]string{
|
||||||
|
{"git", "-C", sourceRepoPath, "add", "."},
|
||||||
|
{"git", "-C", sourceRepoPath, "commit", "-m", "Initial commit"},
|
||||||
|
}
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
|
||||||
|
t.Fatalf("failed to run %v: %v", cmd, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &rig.Rig{
|
||||||
|
Name: "test-rig",
|
||||||
|
Path: rigPath,
|
||||||
|
GitURL: sourceRepoPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := NewManager(r, g)
|
||||||
|
|
||||||
|
// Test Add with branch
|
||||||
|
worker, err := mgr.Add("emma", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add with branch failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if worker.Branch != "crew/emma" {
|
||||||
|
t.Errorf("expected branch 'crew/emma', got '%s'", worker.Branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagerList(t *testing.T) {
|
||||||
|
// Create temp directory for test
|
||||||
|
tmpDir, err := os.MkdirTemp("", "crew-test-list-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Create a mock rig
|
||||||
|
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||||
|
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create rig dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := git.NewGit(rigPath)
|
||||||
|
|
||||||
|
// Create a bare repo for cloning
|
||||||
|
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
|
||||||
|
if err := runCmd("git", "init", "--bare", bareRepoPath); err != nil {
|
||||||
|
t.Fatalf("failed to create bare repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &rig.Rig{
|
||||||
|
Name: "test-rig",
|
||||||
|
Path: rigPath,
|
||||||
|
GitURL: bareRepoPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := NewManager(r, g)
|
||||||
|
|
||||||
|
// Initially empty
|
||||||
|
workers, err := mgr.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List failed: %v", err)
|
||||||
|
}
|
||||||
if len(workers) != 0 {
|
if len(workers) != 0 {
|
||||||
t.Errorf("List() returned %d workers, want 0", len(workers))
|
t.Errorf("expected 0 workers, got %d", len(workers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some workers
|
||||||
|
_, err = mgr.Add("alice", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add alice failed: %v", err)
|
||||||
|
}
|
||||||
|
_, err = mgr.Add("bob", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add bob failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
workers, err = mgr.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(workers) != 2 {
|
||||||
|
t.Errorf("expected 2 workers, got %d", len(workers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_List_WithWorkers(t *testing.T) {
|
func TestManagerRemove(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
// Create temp directory for test
|
||||||
|
tmpDir, err := os.MkdirTemp("", "crew-test-remove-*")
|
||||||
// Create some fake worker directories
|
if err != nil {
|
||||||
workers := []string{"alice", "bob", "charlie"}
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
for _, name := range workers {
|
|
||||||
workerDir := filepath.Join(tmpDir, "crew", name)
|
|
||||||
if err := os.MkdirAll(workerDir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Create a mock rig
|
||||||
|
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||||
|
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create rig dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := git.NewGit(rigPath)
|
||||||
|
|
||||||
|
// Create a bare repo for cloning
|
||||||
|
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
|
||||||
|
if err := runCmd("git", "init", "--bare", bareRepoPath); err != nil {
|
||||||
|
t.Fatalf("failed to create bare repo: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &rig.Rig{
|
r := &rig.Rig{
|
||||||
Name: "test-rig",
|
Name: "test-rig",
|
||||||
Path: tmpDir,
|
Path: rigPath,
|
||||||
|
GitURL: bareRepoPath,
|
||||||
}
|
}
|
||||||
m := NewManager(r, git.NewGit(tmpDir))
|
|
||||||
|
|
||||||
gotWorkers, err := m.List()
|
mgr := NewManager(r, g)
|
||||||
|
|
||||||
|
// Add a worker
|
||||||
|
_, err = mgr.Add("charlie", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("List() error = %v", err)
|
t.Fatalf("Add failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(gotWorkers) != len(workers) {
|
// Remove it (with force since CLAUDE.md is uncommitted)
|
||||||
t.Errorf("List() returned %d workers, want %d", len(gotWorkers), len(workers))
|
err = mgr.Remove("charlie", true)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_Names(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
// Create some fake worker directories
|
|
||||||
expected := []string{"alice", "bob"}
|
|
||||||
for _, name := range expected {
|
|
||||||
workerDir := filepath.Join(tmpDir, "crew", name)
|
|
||||||
if err := os.MkdirAll(workerDir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &rig.Rig{
|
|
||||||
Name: "test-rig",
|
|
||||||
Path: tmpDir,
|
|
||||||
}
|
|
||||||
m := NewManager(r, git.NewGit(tmpDir))
|
|
||||||
|
|
||||||
names, err := m.Names()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Names() error = %v", err)
|
t.Fatalf("Remove failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(names) != len(expected) {
|
// Verify it's gone
|
||||||
t.Errorf("Names() returned %d names, want %d", len(names), len(expected))
|
_, err = mgr.Get("charlie")
|
||||||
|
if err != ErrCrewNotFound {
|
||||||
|
t.Errorf("expected ErrCrewNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove non-existent
|
||||||
|
err = mgr.Remove("nonexistent", false)
|
||||||
|
if err != ErrCrewNotFound {
|
||||||
|
t.Errorf("expected ErrCrewNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorker_EffectiveBeadsDir(t *testing.T) {
|
// Helper to run commands
|
||||||
tests := []struct {
|
func runCmd(name string, args ...string) error {
|
||||||
name string
|
cmd := exec.Command(name, args...)
|
||||||
beadsDir string
|
return cmd.Run()
|
||||||
rigPath string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "custom beads dir",
|
|
||||||
beadsDir: "/custom/beads",
|
|
||||||
rigPath: "/tmp/rig",
|
|
||||||
want: "/custom/beads",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "default beads dir",
|
|
||||||
beadsDir: "",
|
|
||||||
rigPath: "/tmp/rig",
|
|
||||||
want: "/tmp/rig/.beads",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
w := &Worker{
|
|
||||||
BeadsDir: tt.beadsDir,
|
|
||||||
}
|
|
||||||
got := w.EffectiveBeadsDir(tt.rigPath)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("EffectiveBeadsDir() = %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorker_Summary(t *testing.T) {
|
|
||||||
w := &Worker{
|
|
||||||
Name: "alice",
|
|
||||||
State: StateActive,
|
|
||||||
Branch: "feature/test",
|
|
||||||
}
|
|
||||||
|
|
||||||
got := w.Summary()
|
|
||||||
|
|
||||||
if got.Name != w.Name {
|
|
||||||
t.Errorf("Summary().Name = %q, want %q", got.Name, w.Name)
|
|
||||||
}
|
|
||||||
if got.State != w.State {
|
|
||||||
t.Errorf("Summary().State = %q, want %q", got.State, w.State)
|
|
||||||
}
|
|
||||||
if got.Branch != w.Branch {
|
|
||||||
t.Errorf("Summary().Branch = %q, want %q", got.Branch, w.Branch)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +1,39 @@
|
|||||||
// Package crew provides crew worker management.
|
// Package crew provides crew workspace management for overseer workspaces.
|
||||||
// Crew workers are user-managed persistent workspaces within a rig,
|
|
||||||
// as opposed to polecats which are AI-managed workers.
|
|
||||||
package crew
|
package crew
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// State represents the current state of a crew worker.
|
// CrewWorker represents a user-managed workspace in a rig.
|
||||||
type State string
|
type CrewWorker struct {
|
||||||
|
|
||||||
const (
|
|
||||||
// StateActive means the crew workspace is active.
|
|
||||||
StateActive State = "active"
|
|
||||||
|
|
||||||
// StateInactive means the crew workspace is inactive.
|
|
||||||
StateInactive State = "inactive"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Worker represents a crew member's workspace in a rig.
|
|
||||||
// Unlike polecats, crew workers are managed by the Overseer (human),
|
|
||||||
// not by AI agents.
|
|
||||||
type Worker struct {
|
|
||||||
// Name is the crew worker identifier.
|
// Name is the crew worker identifier.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// Rig is the rig this worker belongs to.
|
// Rig is the rig this crew worker belongs to.
|
||||||
Rig string `json:"rig"`
|
Rig string `json:"rig"`
|
||||||
|
|
||||||
// State is the current state.
|
// ClonePath is the path to the crew worker's clone of the rig.
|
||||||
State State `json:"state"`
|
|
||||||
|
|
||||||
// ClonePath is the path to the worker's clone.
|
|
||||||
ClonePath string `json:"clone_path"`
|
ClonePath string `json:"clone_path"`
|
||||||
|
|
||||||
// Branch is the current git branch (if any).
|
// Branch is the current git branch.
|
||||||
Branch string `json:"branch,omitempty"`
|
Branch string `json:"branch"`
|
||||||
|
|
||||||
// BeadsDir is an optional custom beads directory.
|
// CreatedAt is when the crew worker was created.
|
||||||
// If empty, defaults to the rig's .beads/ directory.
|
|
||||||
BeadsDir string `json:"beads_dir,omitempty"`
|
|
||||||
|
|
||||||
// CreatedAt is when the worker was created.
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|
||||||
// UpdatedAt is when the worker was last updated.
|
// UpdatedAt is when the crew worker was last updated.
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary provides a concise view of crew worker status.
|
// Summary provides a concise view of crew worker status.
|
||||||
type Summary struct {
|
type Summary struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
State State `json:"state"`
|
Branch string `json:"branch"`
|
||||||
Branch string `json:"branch,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary returns a Summary for this worker.
|
// Summary returns a Summary for this crew worker.
|
||||||
func (w *Worker) Summary() Summary {
|
func (c *CrewWorker) Summary() Summary {
|
||||||
return Summary{
|
return Summary{
|
||||||
Name: w.Name,
|
Name: c.Name,
|
||||||
State: w.State,
|
Branch: c.Branch,
|
||||||
Branch: w.Branch,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EffectiveBeadsDir returns the beads directory to use.
|
|
||||||
// Returns the custom BeadsDir if set, otherwise returns the rig default path.
|
|
||||||
func (w *Worker) EffectiveBeadsDir(rigPath string) string {
|
|
||||||
if w.BeadsDir != "" {
|
|
||||||
return w.BeadsDir
|
|
||||||
}
|
|
||||||
return rigPath + "/.beads"
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user