From 601dd346b00b357a0541239fb80b34c3795e7c33 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 11 May 2026 14:32:15 +0100 Subject: [PATCH] fix(copilot): hint to run copilot directly when exec fails When the copilot binary is found in PATH but exec fails (e.g., due to unusual characters like parentheses in the path on Windows), append a hint suggesting the user run `copilot` directly without `gh`. The hint is only shown when the binary was already present on the host, not when it was freshly downloaded and installed by `gh`. Closes cli/cli#13106 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/copilot/copilot.go | 21 ++++++++++++++++++--- pkg/cmd/copilot/copilot_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/copilot/copilot.go b/pkg/cmd/copilot/copilot.go index 50b00e9fe..d0cd2e8fc 100644 --- a/pkg/cmd/copilot/copilot.go +++ b/pkg/cmd/copilot/copilot.go @@ -142,8 +142,9 @@ func runCopilot(opts *CopilotOptions) error { return nil } - copilotPath := findCopilotBinary() - if copilotPath == "" { + copilotPath := findCopilotBinaryFunc() + foundInPath := copilotPath != "" + if !foundInPath { if opts.IO.CanPrompt() { confirmed, err := opts.Prompter.Confirm("GitHub Copilot CLI is not installed. Would you like to install it?", true) if err != nil { @@ -175,12 +176,18 @@ func runCopilot(opts *CopilotOptions) error { externalCmd.Stderr = opts.IO.ErrOut externalCmd.Env = append(os.Environ(), "COPILOT_GH=true") - if err := externalCmd.Run(); err != nil { + if err := runExternalCmdFunc(externalCmd); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { // We terminate with os.Exit here, preserving the exit code from Copilot CLI, // and also preventing stdio writes by callers up the stack. os.Exit(exitErr.ExitCode()) } + if foundInPath { + // The binary exists in PATH but exec failed, possibly due to + // unusual characters in the path (see https://github.com/cli/cli/issues/13106). + // Suggest running copilot directly as a workaround. + return fmt.Errorf("%w\nTry running `copilot` directly without `gh`.", err) + } return err } return nil @@ -200,6 +207,14 @@ func copilotBinaryPath() string { return filepath.Join(copilotInstallDir(), binaryName) } +var runExternalCmdFunc = runExternalCmd + +func runExternalCmd(cmd *exec.Cmd) error { + return cmd.Run() +} + +var findCopilotBinaryFunc = findCopilotBinary + // findCopilotBinary returns the path to the Copilot CLI binary, if installed, // with the following order of precedence: // 1. `copilot` in the PATH diff --git a/pkg/cmd/copilot/copilot_test.go b/pkg/cmd/copilot/copilot_test.go index 07e0191e6..16c0b1155 100644 --- a/pkg/cmd/copilot/copilot_test.go +++ b/pkg/cmd/copilot/copilot_test.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -589,6 +590,32 @@ func TestDownloadCopilot(t *testing.T) { }) } +func TestRunCopilot_execFailureHint(t *testing.T) { + ios, _, _, _ := iostreams.Test() + opts := &CopilotOptions{ + IO: ios, + CopilotArgs: []string{}, + } + + origFind := findCopilotBinaryFunc + findCopilotBinaryFunc = func() string { + return "/usr/bin/copilot" + } + t.Cleanup(func() { findCopilotBinaryFunc = origFind }) + + execErr := fmt.Errorf("exec failed: something went wrong") + origRun := runExternalCmdFunc + runExternalCmdFunc = func(_ *exec.Cmd) error { + return execErr + } + t.Cleanup(func() { runExternalCmdFunc = origRun }) + + err := runCopilot(opts) + require.Error(t, err) + require.ErrorIs(t, err, execErr) + require.Contains(t, err.Error(), "Try running `copilot` directly without `gh`.") +} + func TestCopilotCommandIsSampledAt100(t *testing.T) { spy := &telemetry.CommandRecorderSpy{} factory := &cmdutil.Factory{}