Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
655 changes: 655 additions & 0 deletions TEST_PLAN.md

Large diffs are not rendered by default.

124 changes: 114 additions & 10 deletions cmd/amux/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,48 @@ var (
date = "unknown"
)

// CLI subcommands that route to the headless CLI.
var cliCommands = map[string]bool{
"status": true, "doctor": true, "logs": true,
// legacyCLICommands route to the JSON-capable headless CLI.
var legacyCLICommands = map[string]bool{
"workspace": true, "agent": true, "session": true, "project": true,
"terminal": true,
"logs": true,
"capabilities": true,
"version": true, "help": true,
}

// cobraCLICommands route to the sandbox-oriented Cobra command tree.
var cobraCLICommands = map[string]bool{
"auth": true,
"sandbox": true,
"setup": true,
"snapshot": true,
"settings": true,
"ssh": true,
"exec": true,
"status": true,
"doctor": true,
"completion": true,
"explain": true,
"claude": true,
"codex": true,
"opencode": true,
"amp": true,
"gemini": true,
"droid": true,
"shell": true,
"ls": true,
"rm": true,
}

type dispatchTarget int

const (
dispatchTargetLegacy dispatchTarget = iota
dispatchTargetCobra
dispatchTargetTUI
dispatchTargetUnknown
)

func main() {
// Handle --version flag
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
Expand All @@ -55,13 +88,26 @@ func main() {
os.Exit(code)
}

// Route to CLI if a known subcommand is given (even with leading global flags).
// Route to the appropriate command surface if a known subcommand is given
// (even with leading global flags).
if sub != "" {
if cliCommands[sub] {
switch classifyDispatch(sub) {
case dispatchTargetLegacy:
code := cli.Run(os.Args[1:], version, commit, date)
os.Exit(code)
}
if sub == "tui" {
case dispatchTargetCobra:
if shouldRouteLegacyJSONContract(sub, os.Args[1:]) {
code := cli.Run(os.Args[1:], version, commit, date)
os.Exit(code)
}
gf, cobraArgs, err := prepareCobraDispatchArgs(os.Args[1:], sub)
if err != nil {
code := cli.Run(os.Args[1:], version, commit, date)
os.Exit(code)
}
code := cli.RunCobraWithGlobals(cobraArgs, gf, version)
os.Exit(code)
case dispatchTargetTUI:
// Launch TUI unconditionally.
runTUI()
return
Expand All @@ -87,9 +133,17 @@ func main() {
os.Exit(code)
}

func firstCLIArg(args []string) string {
sub, _ := classifyInvocation(args)
return sub
func classifyDispatch(sub string) dispatchTarget {
if legacyCLICommands[sub] {
return dispatchTargetLegacy
}
if cobraCLICommands[sub] {
return dispatchTargetCobra
}
if sub == "tui" {
return dispatchTargetTUI
}
return dispatchTargetUnknown
}

func classifyInvocation(args []string) (string, error) {
Expand All @@ -103,6 +157,56 @@ func classifyInvocation(args []string) (string, error) {
return rest[0], nil
}

func shouldRouteLegacyJSONContract(sub string, args []string) bool {
if sub != "status" && sub != "doctor" {
return false
}
gf, _, err := cli.ParseGlobalFlags(args)
if err != nil {
return false
}
return gf.JSON
}

func prepareCobraDispatchArgs(args []string, sub string) (cli.GlobalFlags, []string, error) {
parseGlobals := cli.ParseLeadingRunGlobals
if requiresCobraCompatPreprocess(sub) {
parseGlobals = cli.ParseGlobalFlags
}

gf, rest, err := parseGlobals(args)
if err != nil {
return gf, nil, err
}
if len(rest) == 0 {
return gf, []string{}, nil
}

cobraArgs := append([]string(nil), rest...)
// Preserve the documented leading-global form by remapping a consumed
// leading --json into the leaf Cobra command's argv.
if gf.JSON && !containsArg(cobraArgs, "--json") {
cobraArgs = cli.InsertFlagAfterCobraCommandPath(cobraArgs, "--json")
}
return gf, cobraArgs, nil
}

func requiresCobraCompatPreprocess(sub string) bool {
return sub == "status" || sub == "doctor"
}

func containsArg(args []string, target string) bool {
for _, arg := range args {
if arg == "--" {
return false
}
if arg == target {
return true
}
}
return false
}

func shouldLaunchTUI(stdinIsTTY, stdoutIsTTY, stderrIsTTY bool) bool {
return stdinIsTTY && stdoutIsTTY && stderrIsTTY
}
Expand Down
76 changes: 76 additions & 0 deletions cmd/amux/main_cobra_dispatch_mixed_globals_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//go:build !windows

package main

import (
"testing"

"github.com/andyrewlee/amux/internal/cli"
)

func TestPrepareCobraDispatchArgsConsumesRunGlobalsAfterPreservedLeadingGlobals(t *testing.T) {
tmp := t.TempDir()

gotGF, gotArgs, err := prepareCobraDispatchArgs([]string{"--json", "--cwd", tmp, "sandbox", "ls"}, "sandbox")
if err != nil {
t.Fatalf("prepareCobraDispatchArgs() error = %v", err)
}
if gotGF.Cwd != tmp {
t.Fatalf("cwd = %q, want %q", gotGF.Cwd, tmp)
}
if !gotGF.JSON {
t.Fatal("expected JSON global to be consumed into GlobalFlags")
}
wantArgs := []string{"sandbox", "ls", "--json"}
if len(gotArgs) != len(wantArgs) {
t.Fatalf("cobra args = %v, want %v", gotArgs, wantArgs)
}
for i := range wantArgs {
if gotArgs[i] != wantArgs[i] {
t.Fatalf("cobra args = %v, want %v", gotArgs, wantArgs)
}
}
}

func TestPrepareCobraDispatchArgsRemapsSandboxLsCommandLocalJSONFlag(t *testing.T) {
gotGF, gotArgs, err := prepareCobraDispatchArgs([]string{"sandbox", "ls", "--json"}, "sandbox")
if err != nil {
t.Fatalf("prepareCobraDispatchArgs() error = %v", err)
}
if gotGF != (cli.GlobalFlags{JSON: true}) {
t.Fatalf("GlobalFlags = %+v, want JSON global", gotGF)
}
wantArgs := []string{"sandbox", "ls", "--json"}
if len(gotArgs) != len(wantArgs) {
t.Fatalf("cobra args = %v, want %v", gotArgs, wantArgs)
}
for i := range wantArgs {
if gotArgs[i] != wantArgs[i] {
t.Fatalf("cobra args = %v, want %v", gotArgs, wantArgs)
}
}
}

func TestPrepareCobraDispatchArgsConsumesPostCommandGlobalsBeforeDoubleDash(t *testing.T) {
tmp := t.TempDir()

gotGF, gotArgs, err := prepareCobraDispatchArgs([]string{"sandbox", "ls", "--cwd", tmp, "--request-id", "req-post", "--", "--quiet"}, "sandbox")
if err != nil {
t.Fatalf("prepareCobraDispatchArgs() error = %v", err)
}
if gotGF.Cwd != tmp {
t.Fatalf("cwd = %q, want %q", gotGF.Cwd, tmp)
}
if gotGF.RequestID != "req-post" {
t.Fatalf("request-id = %q, want %q", gotGF.RequestID, "req-post")
}
wantArgs := []string{"sandbox", "ls", "--", "--quiet"}
if len(gotArgs) != len(wantArgs) {
t.Fatalf("cobra args = %v, want %v", gotArgs, wantArgs)
}
for i := range wantArgs {
if gotArgs[i] != wantArgs[i] {
t.Fatalf("cobra args = %v, want %v", gotArgs, wantArgs)
}
}
}
Loading