Fixes gt-e5o: When a rig has its own mayor/ directory, workspace detection now continues searching upward for primary markers. Changes: - Add AlternativePrimaryMarker (mayor/config.json) to distinguish town-level mayor from rig-level mayor clones - Continue searching after finding secondary marker (mayor/) to prefer primary matches higher in the tree - Return the first secondary match only if no primary is found This fixes role detection for polecats/refinery/witness when running from within a rig that has its own mayor/ clone. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
146 lines
4.2 KiB
Go
146 lines
4.2 KiB
Go
// Package workspace provides workspace detection and management.
|
|
package workspace
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// ErrNotFound indicates no workspace was found.
|
|
var ErrNotFound = errors.New("not in a Gas Town workspace")
|
|
|
|
// Markers used to detect a Gas Town workspace.
|
|
const (
|
|
// PrimaryMarker is the main config file that identifies a workspace.
|
|
PrimaryMarker = "config/town.json"
|
|
|
|
// AlternativePrimaryMarker is the town-level mayor config file.
|
|
// This distinguishes a town mayor from a rig-level mayor clone.
|
|
AlternativePrimaryMarker = "mayor/config.json"
|
|
|
|
// SecondaryMarker is an alternative indicator at the town level.
|
|
// Note: This can match rig-level mayors too, so we continue searching
|
|
// upward after finding this to look for primary markers.
|
|
SecondaryMarker = "mayor"
|
|
)
|
|
|
|
// Find locates the town root by walking up from the given directory.
|
|
// It looks for config/town.json or mayor/config.json (primary markers)
|
|
// or mayor/ directory (secondary marker).
|
|
//
|
|
// To avoid matching rig-level mayor directories, we continue searching
|
|
// upward after finding a secondary marker, preferring primary matches.
|
|
func Find(startDir string) (string, error) {
|
|
// Resolve to absolute path and follow symlinks
|
|
absDir, err := filepath.Abs(startDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolving path: %w", err)
|
|
}
|
|
|
|
absDir, err = filepath.EvalSymlinks(absDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("evaluating symlinks: %w", err)
|
|
}
|
|
|
|
// Track the first secondary match in case no primary is found
|
|
var secondaryMatch string
|
|
|
|
// Walk up the directory tree
|
|
current := absDir
|
|
for {
|
|
// Check for primary marker (config/town.json)
|
|
primaryPath := filepath.Join(current, PrimaryMarker)
|
|
if _, err := os.Stat(primaryPath); err == nil {
|
|
return current, nil
|
|
}
|
|
|
|
// Check for alternative primary marker (mayor/config.json)
|
|
// This distinguishes a town-level mayor from a rig-level mayor clone
|
|
altPrimaryPath := filepath.Join(current, AlternativePrimaryMarker)
|
|
if _, err := os.Stat(altPrimaryPath); err == nil {
|
|
return current, nil
|
|
}
|
|
|
|
// Check for secondary marker (mayor/ directory)
|
|
// Don't return immediately - continue searching for primary markers
|
|
if secondaryMatch == "" {
|
|
secondaryPath := filepath.Join(current, SecondaryMarker)
|
|
info, err := os.Stat(secondaryPath)
|
|
if err == nil && info.IsDir() {
|
|
secondaryMatch = current
|
|
}
|
|
}
|
|
|
|
// Move to parent directory
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
// Reached filesystem root - return secondary match if found
|
|
return secondaryMatch, nil
|
|
}
|
|
current = parent
|
|
}
|
|
}
|
|
|
|
// FindOrError is like Find but returns a user-friendly error if not found.
|
|
func FindOrError(startDir string) (string, error) {
|
|
root, err := Find(startDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if root == "" {
|
|
return "", ErrNotFound
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
// FindFromCwd locates the town root from the current working directory.
|
|
func FindFromCwd() (string, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting current directory: %w", err)
|
|
}
|
|
return Find(cwd)
|
|
}
|
|
|
|
// FindFromCwdOrError is like FindFromCwd but returns an error if not found.
|
|
func FindFromCwdOrError() (string, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting current directory: %w", err)
|
|
}
|
|
return FindOrError(cwd)
|
|
}
|
|
|
|
// IsWorkspace checks if the given directory is a Gas Town workspace root.
|
|
// A directory is a workspace if it has primary markers (config/town.json
|
|
// or mayor/config.json) or a secondary marker (mayor/ directory).
|
|
func IsWorkspace(dir string) (bool, error) {
|
|
absDir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return false, fmt.Errorf("resolving path: %w", err)
|
|
}
|
|
|
|
// Check for primary marker
|
|
primaryPath := filepath.Join(absDir, PrimaryMarker)
|
|
if _, err := os.Stat(primaryPath); err == nil {
|
|
return true, nil
|
|
}
|
|
|
|
// Check for alternative primary marker
|
|
altPrimaryPath := filepath.Join(absDir, AlternativePrimaryMarker)
|
|
if _, err := os.Stat(altPrimaryPath); err == nil {
|
|
return true, nil
|
|
}
|
|
|
|
// Check for secondary marker
|
|
secondaryPath := filepath.Join(absDir, SecondaryMarker)
|
|
info, err := os.Stat(secondaryPath)
|
|
if err == nil && info.IsDir() {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|