From 37a490c827ebfc2fe84d363353c88cdbaf41d035 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Fri, 9 Oct 2026 11:36:16 +0100 Subject: [PATCH 1/4] fix: stop forwarding --debug to the legacy CLI The root command's persistent --debug flag was passed through to the legacy PHP CLI, which rejected it ("The "--debug" option does not exist."). Strip it before the args are forwarded, in the root command and in both help paths, and pass debug mode to the legacy CLI as DEBUG=1 instead, matching the environment variable. The help command does not parse flags, so it now enables debug mode itself when --debug is given. Co-Authored-By: Claude Opus 5.5 --- commands/help.go | 5 +++++ commands/root.go | 29 +++++++++++++++++++++++- commands/root_test.go | 25 +++++++++++++++++++++ integration-tests/debug_test.go | 39 +++++++++++++++++++++++++++++++++ internal/legacy/legacy.go | 3 +++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 integration-tests/debug_test.go diff --git a/commands/help.go b/commands/help.go index c7c433cdc..c1108c135 100644 --- a/commands/help.go +++ b/commands/help.go @@ -2,6 +2,7 @@ package commands import ( "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/upsun/cli/internal/config" "github.com/upsun/cli/internal/legacy" @@ -13,6 +14,10 @@ func newHelpCommand(cnf *config.Config) *cobra.Command { // Disable flag parsing so flags like --format are preserved for the legacy CLI. DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { + args, debug := stripDebugFlag(args) + if debug { + viper.Set("debug", true) + } loadLegacyCmds := enabledLegacyCommands(cnf, legacy.Commands) if expanded, ok, err := expandAbbreviation(cmd.Root(), loadLegacyCmds, args); err != nil { debugLogf("Failed to load the legacy command index: %s", err) diff --git a/commands/root.go b/commands/root.go index 0db2a8240..b233f85b6 100644 --- a/commands/root.go +++ b/commands/root.go @@ -10,6 +10,7 @@ import ( "path/filepath" "regexp" "slices" + "strconv" "strings" "github.com/fatih/color" @@ -107,7 +108,8 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob }, Run: func(cmd *cobra.Command, _ []string) { c := makeLegacyCLIWrapper(cnf, cmd.OutOrStdout(), cmd.ErrOrStderr(), cmd.InOrStdin()) - if err := c.Exec(cmd.Context(), os.Args[1:]...); err != nil { + args, _ := stripDebugFlag(os.Args[1:]) + if err := c.Exec(cmd.Context(), args...); err != nil { exitWithError(err) } }, @@ -127,6 +129,7 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob } // Others will be passed to the legacy CLI's help command. + args, _ = stripDebugFlag(args) if !slices.Contains(args, "--help") && !slices.Contains(args, "-h") { args = append([]string{"help"}, args...) } @@ -294,6 +297,29 @@ func debugLogf(format string, v ...any) { fmt.Fprintf(color.Error, prefix+" "+strings.TrimSpace(format)+"\n", v...) } +// stripDebugFlag removes the --debug flag, which the legacy CLI does not accept, from args before any "--" +// separator. It reports whether the flag enabled debug mode. +func stripDebugFlag(args []string) (stripped []string, debug bool) { + stripped = make([]string, 0, len(args)) + for i, arg := range args { + if arg == "--" { + return append(stripped, args[i:]...), debug + } + if arg == "--debug" { + debug = true + continue + } + if v, ok := strings.CutPrefix(arg, "--debug="); ok { + if b, err := strconv.ParseBool(v); err == nil { + debug = b + continue + } + } + stripped = append(stripped, arg) + } + return stripped, debug +} + func exitWithError(err error) { var execErr *exec.ExitError if errors.As(err, &execErr) { @@ -311,6 +337,7 @@ func makeLegacyCLIWrapper(cnf *config.Config, stdout, stderr io.Writer, stdin io return &legacy.CLIWrapper{ Config: cnf, Version: config.Version, + Debug: viper.GetBool("debug"), DebugLogFunc: debugLogf, DisableInteraction: viper.GetBool("no-interaction"), Stdout: stdout, diff --git a/commands/root_test.go b/commands/root_test.go index fddd8066a..be3d02600 100644 --- a/commands/root_test.go +++ b/commands/root_test.go @@ -1,6 +1,7 @@ package commands import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -52,3 +53,27 @@ func TestShellQuote(t *testing.T) { assert.Equal(t, c.want, shellQuote(c.in)) } } + +func TestStripDebugFlag(t *testing.T) { + cases := []struct { + args []string + want []string + wantDebug bool + }{ + {[]string{"cc"}, []string{"cc"}, false}, + {[]string{"cc", "--debug"}, []string{"cc"}, true}, + {[]string{"--debug", "cc", "-v"}, []string{"cc", "-v"}, true}, + {[]string{"cc", "--debug=1"}, []string{"cc"}, true}, + {[]string{"cc", "--debug=false"}, []string{"cc"}, false}, + {[]string{"cc", "--debug=foo"}, []string{"cc", "--debug=foo"}, false}, + {[]string{"ssh", "--", "cmd", "--debug"}, []string{"ssh", "--", "cmd", "--debug"}, false}, + {[]string{"ssh", "--debug", "--", "--debug"}, []string{"ssh", "--", "--debug"}, true}, + } + for _, c := range cases { + t.Run(strings.Join(c.args, " "), func(t *testing.T) { + got, debug := stripDebugFlag(c.args) + assert.Equal(t, c.want, got) + assert.Equal(t, c.wantDebug, debug) + }) + } +} diff --git a/integration-tests/debug_test.go b/integration-tests/debug_test.go new file mode 100644 index 000000000..6a03373c8 --- /dev/null +++ b/integration-tests/debug_test.go @@ -0,0 +1,39 @@ +package tests + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestDebugFlag checks that --debug enables debug logging and is not forwarded to the legacy CLI. +func TestDebugFlag(t *testing.T) { + f := newCommandFactory(t, "", "") + + cases := []struct { + args []string + expected string + }{ + {[]string{"cc", "--debug"}, ""}, + {[]string{"--debug", "cc"}, ""}, + {[]string{"cc", "--debug", "--help"}, "Command: clear-cache"}, + {[]string{"--help", "--debug", "cc"}, "Command: clear-cache"}, + {[]string{"help", "cc", "--debug"}, "Command: clear-cache"}, + {[]string{"help", "--debug", "cc"}, "Command: clear-cache"}, + {[]string{"--debug", "p:init", "--help"}, "Command: project:init"}, + } + for _, c := range cases { + t.Run(strings.Join(c.args, " "), func(t *testing.T) { + stdOut, stdErr, err := f.RunCombinedOutput(c.args...) + require.NoError(t, err, stdErr) + assert.NotContains(t, stdErr, `"--debug" option does not exist`) + assert.Contains(t, stdOut, c.expected) + if c.expected != "Command: project:init" { + // Logged by the Go wrapper when it starts the legacy CLI. + assert.Contains(t, stdErr, "Initialized PHP CLI") + } + }) + } +} diff --git a/internal/legacy/legacy.go b/internal/legacy/legacy.go index 1e01fb91c..1628a10bb 100644 --- a/internal/legacy/legacy.go +++ b/internal/legacy/legacy.go @@ -149,6 +149,9 @@ func (c *CLIWrapper) Exec(ctx context.Context, args ...string) error { if c.DisableInteraction { cmd.Env = append(cmd.Env, envPrefix+"NO_INTERACTION=1") } + if c.Debug { + cmd.Env = append(cmd.Env, envPrefix+"DEBUG=1") + } if c.ForceColor { cmd.Env = append(cmd.Env, "CLICOLOR_FORCE=1") } From d26ba64b8128597e1d27973a4e6bf94177a4955b Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Fri, 9 Oct 2026 11:36:21 +0100 Subject: [PATCH 2/4] docs: mention the --debug flag alongside UPSUN_CLI_DEBUG Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2eb9d1aa..d1df68511 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Environment variables include: - `UPSUN_CLI_TOKEN`: an API token, for non-interactive use such as CI. An API token can act as the account that created it, so use a separate machine account to limit its access. Interactively, prefer `upsun auth:api-token-login`. -- `UPSUN_CLI_DEBUG=1`: enable debug output. This can print HTTP request details, +- `UPSUN_CLI_DEBUG=1`: enable debug output, like the `--debug` flag. This can print HTTP request details, including access tokens. - `UPSUN_CLI_DEFAULT_TIMEOUT`: the timeout in seconds for most API requests (default 30). From 6902c9ce53e916fbe188d577a12f0803beb64080 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Fri, 9 Oct 2026 11:41:02 +0100 Subject: [PATCH 3/4] fix(legacy): pass a disabled debug mode to the legacy CLI too Otherwise an inherited DEBUG=1 still enables PHP debug output when --debug=false is given. Co-Authored-By: Claude Opus 5.5 --- internal/legacy/legacy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/legacy/legacy.go b/internal/legacy/legacy.go index 1628a10bb..609083be9 100644 --- a/internal/legacy/legacy.go +++ b/internal/legacy/legacy.go @@ -149,8 +149,11 @@ func (c *CLIWrapper) Exec(ctx context.Context, args ...string) error { if c.DisableInteraction { cmd.Env = append(cmd.Env, envPrefix+"NO_INTERACTION=1") } + // Override any inherited value, so that --debug=false also applies to the legacy CLI. if c.Debug { cmd.Env = append(cmd.Env, envPrefix+"DEBUG=1") + } else { + cmd.Env = append(cmd.Env, envPrefix+"DEBUG=0") } if c.ForceColor { cmd.Env = append(cmd.Env, "CLICOLOR_FORCE=1") From 91e4b621d66389c9d7278b8ea0bd5522808966e2 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Fri, 9 Oct 2026 11:43:30 +0100 Subject: [PATCH 4/4] fix(help): honor --debug=false in the help command The help command does not parse flags, so an explicit false value must also override DEBUG. Co-Authored-By: Claude Opus 5.5 --- commands/help.go | 4 ++-- commands/root.go | 8 ++++---- commands/root_test.go | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/commands/help.go b/commands/help.go index c1108c135..e6df1d0ac 100644 --- a/commands/help.go +++ b/commands/help.go @@ -15,8 +15,8 @@ func newHelpCommand(cnf *config.Config) *cobra.Command { DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { args, debug := stripDebugFlag(args) - if debug { - viper.Set("debug", true) + if debug != nil { + viper.Set("debug", *debug) } loadLegacyCmds := enabledLegacyCommands(cnf, legacy.Commands) if expanded, ok, err := expandAbbreviation(cmd.Root(), loadLegacyCmds, args); err != nil { diff --git a/commands/root.go b/commands/root.go index b233f85b6..f2b56c11e 100644 --- a/commands/root.go +++ b/commands/root.go @@ -298,20 +298,20 @@ func debugLogf(format string, v ...any) { } // stripDebugFlag removes the --debug flag, which the legacy CLI does not accept, from args before any "--" -// separator. It reports whether the flag enabled debug mode. -func stripDebugFlag(args []string) (stripped []string, debug bool) { +// separator. It returns the flag's value, or nil if it was not given. +func stripDebugFlag(args []string) (stripped []string, debug *bool) { stripped = make([]string, 0, len(args)) for i, arg := range args { if arg == "--" { return append(stripped, args[i:]...), debug } if arg == "--debug" { - debug = true + debug = new(true) continue } if v, ok := strings.CutPrefix(arg, "--debug="); ok { if b, err := strconv.ParseBool(v); err == nil { - debug = b + debug = new(b) continue } } diff --git a/commands/root_test.go b/commands/root_test.go index be3d02600..0a577f1e2 100644 --- a/commands/root_test.go +++ b/commands/root_test.go @@ -58,16 +58,16 @@ func TestStripDebugFlag(t *testing.T) { cases := []struct { args []string want []string - wantDebug bool + wantDebug *bool }{ - {[]string{"cc"}, []string{"cc"}, false}, - {[]string{"cc", "--debug"}, []string{"cc"}, true}, - {[]string{"--debug", "cc", "-v"}, []string{"cc", "-v"}, true}, - {[]string{"cc", "--debug=1"}, []string{"cc"}, true}, - {[]string{"cc", "--debug=false"}, []string{"cc"}, false}, - {[]string{"cc", "--debug=foo"}, []string{"cc", "--debug=foo"}, false}, - {[]string{"ssh", "--", "cmd", "--debug"}, []string{"ssh", "--", "cmd", "--debug"}, false}, - {[]string{"ssh", "--debug", "--", "--debug"}, []string{"ssh", "--", "--debug"}, true}, + {[]string{"cc"}, []string{"cc"}, nil}, + {[]string{"cc", "--debug"}, []string{"cc"}, new(true)}, + {[]string{"--debug", "cc", "-v"}, []string{"cc", "-v"}, new(true)}, + {[]string{"cc", "--debug=1"}, []string{"cc"}, new(true)}, + {[]string{"cc", "--debug=false"}, []string{"cc"}, new(false)}, + {[]string{"cc", "--debug=foo"}, []string{"cc", "--debug=foo"}, nil}, + {[]string{"ssh", "--", "cmd", "--debug"}, []string{"ssh", "--", "cmd", "--debug"}, nil}, + {[]string{"ssh", "--debug", "--", "--debug"}, []string{"ssh", "--", "--debug"}, new(true)}, } for _, c := range cases { t.Run(strings.Join(c.args, " "), func(t *testing.T) {