feat: add RoleEnvVars and improve gt role CLI
Introduces config.RoleEnvVars() as the single source of truth for role identity environment variables (GT_ROLE, GT_RIG, BD_ACTOR, etc.). CLI improvements: - Fix getRoleHome paths (witness has no /rig suffix, polecat/crew do) - Make gt role env read-only (displays current role from env/cwd) - Add EnvIncomplete handling: fill missing env vars from cwd with warning - Add cwd mismatch warnings when not in role home directory - gt role home now validates --polecat requires --rig Includes comprehensive e2e tests for all role detection scenarios. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
491b635cbc
commit
2343e6b0ef
@@ -4,9 +4,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
@@ -21,16 +23,17 @@ const (
|
||||
// This is the canonical struct for role detection - used by both GetRole()
|
||||
// and detectRole() functions.
|
||||
type RoleInfo struct {
|
||||
Role Role `json:"role"`
|
||||
Source string `json:"source"` // "env", "cwd", or "explicit"
|
||||
Home string `json:"home"`
|
||||
Rig string `json:"rig,omitempty"`
|
||||
Polecat string `json:"polecat,omitempty"`
|
||||
EnvRole string `json:"env_role,omitempty"` // Value of GT_ROLE if set
|
||||
CwdRole Role `json:"cwd_role,omitempty"` // Role detected from cwd
|
||||
Mismatch bool `json:"mismatch,omitempty"` // True if env != cwd detection
|
||||
TownRoot string `json:"town_root,omitempty"`
|
||||
WorkDir string `json:"work_dir,omitempty"` // Current working directory
|
||||
Role Role `json:"role"`
|
||||
Source string `json:"source"` // "env", "cwd", or "explicit"
|
||||
Home string `json:"home"`
|
||||
Rig string `json:"rig,omitempty"`
|
||||
Polecat string `json:"polecat,omitempty"`
|
||||
EnvRole string `json:"env_role,omitempty"` // Value of GT_ROLE if set
|
||||
CwdRole Role `json:"cwd_role,omitempty"` // Role detected from cwd
|
||||
Mismatch bool `json:"mismatch,omitempty"` // True if env != cwd detection
|
||||
EnvIncomplete bool `json:"env_incomplete,omitempty"` // True if env was set but missing rig/polecat, filled from cwd
|
||||
TownRoot string `json:"town_root,omitempty"`
|
||||
WorkDir string `json:"work_dir,omitempty"` // Current working directory
|
||||
}
|
||||
|
||||
var roleCmd = &cobra.Command{
|
||||
@@ -86,14 +89,18 @@ var roleListCmd = &cobra.Command{
|
||||
var roleEnvCmd = &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Print export statements for current role",
|
||||
Long: `Print shell export statements to set GT_ROLE and GT_ROLE_HOME.
|
||||
Long: `Print shell export statements for the current role.
|
||||
|
||||
Usage:
|
||||
eval $(gt role env) # Set role env vars in current shell`,
|
||||
Role is determined from GT_ROLE environment variable or current working directory.
|
||||
This is a read-only command that displays the current role's env vars.
|
||||
|
||||
Examples:
|
||||
eval $(gt role env) # Export current role's env vars
|
||||
gt role env # View what would be exported`,
|
||||
RunE: runRoleEnv,
|
||||
}
|
||||
|
||||
// Flags
|
||||
// Flags for role home command
|
||||
var (
|
||||
roleRig string
|
||||
rolePolecat string
|
||||
@@ -107,7 +114,7 @@ func init() {
|
||||
roleCmd.AddCommand(roleListCmd)
|
||||
roleCmd.AddCommand(roleEnvCmd)
|
||||
|
||||
// Add --rig flag to home command for witness/refinery/polecat
|
||||
// Add --rig and --polecat flags to home command for overrides
|
||||
roleHomeCmd.Flags().StringVar(&roleRig, "rig", "", "Rig name (required for rig-specific roles)")
|
||||
roleHomeCmd.Flags().StringVar(&rolePolecat, "polecat", "", "Polecat/crew member name")
|
||||
}
|
||||
@@ -170,6 +177,20 @@ func GetRoleWithContext(cwd, townRoot string) (RoleInfo, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// If env is incomplete (missing rig/polecat for roles that need them),
|
||||
// fill gaps from cwd detection and mark as incomplete
|
||||
needsRig := parsedRole == RoleWitness || parsedRole == RoleRefinery || parsedRole == RolePolecat || parsedRole == RoleCrew
|
||||
needsPolecat := parsedRole == RolePolecat || parsedRole == RoleCrew
|
||||
|
||||
if needsRig && info.Rig == "" && cwdCtx.Rig != "" {
|
||||
info.Rig = cwdCtx.Rig
|
||||
info.EnvIncomplete = true
|
||||
}
|
||||
if needsPolecat && info.Polecat == "" && cwdCtx.Polecat != "" {
|
||||
info.Polecat = cwdCtx.Polecat
|
||||
info.EnvIncomplete = true
|
||||
}
|
||||
|
||||
// Check for mismatch with cwd detection
|
||||
if cwdCtx.Role != RoleUnknown && cwdCtx.Role != parsedRole {
|
||||
info.Mismatch = true
|
||||
@@ -277,7 +298,7 @@ func getRoleHome(role Role, rig, polecat, townRoot string) string {
|
||||
if rig == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(townRoot, rig, "witness", "rig")
|
||||
return filepath.Join(townRoot, rig, "witness")
|
||||
case RoleRefinery:
|
||||
if rig == "" {
|
||||
return ""
|
||||
@@ -287,12 +308,12 @@ func getRoleHome(role Role, rig, polecat, townRoot string) string {
|
||||
if rig == "" || polecat == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(townRoot, rig, "polecats", polecat)
|
||||
return filepath.Join(townRoot, rig, "polecats", polecat, "rig")
|
||||
case RoleCrew:
|
||||
if rig == "" || polecat == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(townRoot, rig, "crew", polecat)
|
||||
return filepath.Join(townRoot, rig, "crew", polecat, "rig")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -335,6 +356,11 @@ func runRoleShow(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func runRoleHome(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
@@ -343,29 +369,29 @@ func runRoleHome(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
var role Role
|
||||
var rig, polecat string
|
||||
// Validate flag combinations: --polecat requires --rig to prevent strange merges
|
||||
if rolePolecat != "" && roleRig == "" {
|
||||
return fmt.Errorf("--polecat requires --rig to be specified")
|
||||
}
|
||||
|
||||
// Start with current role detection (from env vars or cwd)
|
||||
info, err := GetRole()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role := info.Role
|
||||
rig := info.Rig
|
||||
polecat := info.Polecat
|
||||
|
||||
// Apply overrides from arguments/flags
|
||||
if len(args) > 0 {
|
||||
// Explicit role provided
|
||||
role, rig, polecat = parseRoleString(args[0])
|
||||
|
||||
// Override with flags if provided
|
||||
if roleRig != "" {
|
||||
rig = roleRig
|
||||
}
|
||||
if rolePolecat != "" {
|
||||
polecat = rolePolecat
|
||||
}
|
||||
} else {
|
||||
// Use current role
|
||||
info, err := GetRole()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role = info.Role
|
||||
rig = info.Rig
|
||||
polecat = info.Polecat
|
||||
role, _, _ = parseRoleString(args[0])
|
||||
}
|
||||
if roleRig != "" {
|
||||
rig = roleRig
|
||||
}
|
||||
if rolePolecat != "" {
|
||||
polecat = rolePolecat
|
||||
}
|
||||
|
||||
home := getRoleHome(role, rig, polecat, townRoot)
|
||||
@@ -373,6 +399,11 @@ func runRoleHome(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("cannot determine home for role %s (rig=%q, polecat=%q)", role, rig, polecat)
|
||||
}
|
||||
|
||||
// Warn if computed home doesn't match cwd
|
||||
if home != cwd && !strings.HasPrefix(cwd, home) {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Warning: cwd (%s) is not within role home (%s)\n", cwd, home)
|
||||
}
|
||||
|
||||
fmt.Println(home)
|
||||
return nil
|
||||
}
|
||||
@@ -440,33 +471,52 @@ func runRoleList(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func runRoleEnv(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
}
|
||||
if townRoot == "" {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
// Get current role (read-only - from env vars or cwd)
|
||||
info, err := GetRole()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build the role string for GT_ROLE
|
||||
var roleStr string
|
||||
switch info.Role {
|
||||
case RoleMayor:
|
||||
roleStr = "mayor"
|
||||
case RoleDeacon:
|
||||
roleStr = "deacon"
|
||||
case RoleWitness:
|
||||
roleStr = fmt.Sprintf("%s/witness", info.Rig)
|
||||
case RoleRefinery:
|
||||
roleStr = fmt.Sprintf("%s/refinery", info.Rig)
|
||||
case RolePolecat:
|
||||
roleStr = fmt.Sprintf("%s/polecats/%s", info.Rig, info.Polecat)
|
||||
case RoleCrew:
|
||||
roleStr = fmt.Sprintf("%s/crew/%s", info.Rig, info.Polecat)
|
||||
default:
|
||||
roleStr = string(info.Role)
|
||||
home := getRoleHome(info.Role, info.Rig, info.Polecat, townRoot)
|
||||
if home == "" {
|
||||
return fmt.Errorf("cannot determine home for role %s (rig=%q, polecat=%q)", info.Role, info.Rig, info.Polecat)
|
||||
}
|
||||
|
||||
fmt.Printf("export %s=%s\n", EnvGTRole, roleStr)
|
||||
if info.Home != "" {
|
||||
fmt.Printf("export %s=%s\n", EnvGTRoleHome, info.Home)
|
||||
// Warn if env was incomplete and we filled from cwd
|
||||
if info.EnvIncomplete {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Warning: env vars incomplete, filled from cwd\n")
|
||||
}
|
||||
|
||||
// Warn if computed home doesn't match cwd
|
||||
if home != cwd && !strings.HasPrefix(cwd, home) {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Warning: cwd (%s) is not within role home (%s)\n", cwd, home)
|
||||
}
|
||||
|
||||
// Get canonical env vars from shared source of truth
|
||||
envVars := config.RoleEnvVars(string(info.Role), info.Rig, info.Polecat)
|
||||
envVars[EnvGTRoleHome] = home
|
||||
|
||||
// Output in sorted order for consistent output
|
||||
keys := make([]string, 0, len(envVars))
|
||||
for k := range envVars {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Printf("export %s=%s\n", k, envVars[k])
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
1056
internal/cmd/role_e2e_test.go
Normal file
1056
internal/cmd/role_e2e_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1170,6 +1170,46 @@ func BuildStartupCommandWithAgentOverride(envVars map[string]string, rigPath, pr
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// RoleEnvVars returns the canonical environment variables for a role.
|
||||
// This is the single source of truth for role identity env vars.
|
||||
// The role parameter should be one of: "mayor", "deacon", "witness", "refinery", "polecat", "crew".
|
||||
// For rig-specific roles, rig must be provided.
|
||||
// For polecat/crew, polecatOrCrew must be the polecat or crew member name.
|
||||
func RoleEnvVars(role, rig, polecatOrCrew string) map[string]string {
|
||||
envVars := map[string]string{
|
||||
"GT_ROLE": role,
|
||||
}
|
||||
|
||||
switch role {
|
||||
case "mayor":
|
||||
envVars["BD_ACTOR"] = "mayor"
|
||||
envVars["GIT_AUTHOR_NAME"] = "mayor"
|
||||
case "deacon":
|
||||
envVars["BD_ACTOR"] = "deacon"
|
||||
envVars["GIT_AUTHOR_NAME"] = "deacon"
|
||||
case "witness":
|
||||
envVars["GT_RIG"] = rig
|
||||
envVars["BD_ACTOR"] = fmt.Sprintf("%s/witness", rig)
|
||||
envVars["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/witness", rig)
|
||||
case "refinery":
|
||||
envVars["GT_RIG"] = rig
|
||||
envVars["BD_ACTOR"] = fmt.Sprintf("%s/refinery", rig)
|
||||
envVars["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/refinery", rig)
|
||||
case "polecat":
|
||||
envVars["GT_RIG"] = rig
|
||||
envVars["GT_POLECAT"] = polecatOrCrew
|
||||
envVars["BD_ACTOR"] = fmt.Sprintf("%s/polecats/%s", rig, polecatOrCrew)
|
||||
envVars["GIT_AUTHOR_NAME"] = polecatOrCrew
|
||||
case "crew":
|
||||
envVars["GT_RIG"] = rig
|
||||
envVars["GT_CREW"] = polecatOrCrew
|
||||
envVars["BD_ACTOR"] = fmt.Sprintf("%s/crew/%s", rig, polecatOrCrew)
|
||||
envVars["GIT_AUTHOR_NAME"] = polecatOrCrew
|
||||
}
|
||||
|
||||
return envVars
|
||||
}
|
||||
|
||||
// BuildAgentStartupCommand is a convenience function for starting agent sessions.
|
||||
// It sets standard environment variables (GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME)
|
||||
// and builds the full startup command.
|
||||
|
||||
Reference in New Issue
Block a user