fix(install): allow --wrappers in existing town without recreating HQ (#366)

When running `gt install --wrappers` in an existing Gas Town HQ,
the command now installs wrappers directly without requiring --force
or recreating the entire HQ structure.

Previously, `gt install --wrappers` would fail with "directory is
already a Gas Town HQ" unless --force was used, which would then
unnecessarily reinitialize the entire workspace.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik LaBianca
2026-01-11 21:45:24 -05:00
committed by GitHub
parent d126c967a0
commit 7ef4ddab6c
2 changed files with 63 additions and 0 deletions

View File

@@ -109,6 +109,14 @@ func runInstall(cmd *cobra.Command, args []string) error {
// Check if already a workspace
if isWS, _ := workspace.IsWorkspace(absPath); isWS && !installForce {
// If only --wrappers is requested in existing town, just install wrappers and exit
if installWrappers {
if err := wrappers.Install(); err != nil {
return fmt.Errorf("installing wrapper scripts: %w", err)
}
fmt.Printf("✓ Installed gt-codex and gt-opencode to %s\n", wrappers.BinDir())
return nil
}
return fmt.Errorf("directory is already a Gas Town HQ (use --force to reinitialize)")
}

View File

@@ -250,6 +250,61 @@ func TestInstallFormulasProvisioned(t *testing.T) {
}
}
// TestInstallWrappersInExistingTown validates that --wrappers works in an
// existing town without requiring --force or recreating HQ structure.
func TestInstallWrappersInExistingTown(t *testing.T) {
tmpDir := t.TempDir()
hqPath := filepath.Join(tmpDir, "test-hq")
binDir := filepath.Join(tmpDir, "bin")
// Create bin directory for wrappers
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatalf("failed to create bin dir: %v", err)
}
gtBinary := buildGT(t)
// First: create HQ without wrappers
cmd := exec.Command(gtBinary, "install", hqPath, "--no-beads")
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("first install failed: %v\nOutput: %s", err, output)
}
// Verify town.json exists (proves HQ was created)
townPath := filepath.Join(hqPath, "mayor", "town.json")
assertFileExists(t, townPath, "mayor/town.json")
// Get modification time of town.json before wrapper install
townInfo, err := os.Stat(townPath)
if err != nil {
t.Fatalf("failed to stat town.json: %v", err)
}
townModBefore := townInfo.ModTime()
// Second: install --wrappers in same directory (should not recreate HQ)
cmd = exec.Command(gtBinary, "install", hqPath, "--wrappers")
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install --wrappers in existing town failed: %v\nOutput: %s", err, output)
}
// Verify town.json was NOT modified (HQ was not recreated)
townInfo, err = os.Stat(townPath)
if err != nil {
t.Fatalf("failed to stat town.json after wrapper install: %v", err)
}
if townInfo.ModTime() != townModBefore {
t.Errorf("town.json was modified during --wrappers install, HQ should not be recreated")
}
// Verify output mentions wrapper installation
if !strings.Contains(string(output), "gt-codex") && !strings.Contains(string(output), "gt-opencode") {
t.Errorf("expected output to mention wrappers, got: %s", output)
}
}
// TestInstallNoBeadsFlag validates that --no-beads skips beads initialization.
func TestInstallNoBeadsFlag(t *testing.T) {
tmpDir := t.TempDir()