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>
This commit is contained in:
Babak K. Shandiz 2026-05-11 14:32:15 +01:00
parent 9b505c3fb8
commit 601dd346b0
No known key found for this signature in database
GPG key ID: 9472CAEFF56C742E
2 changed files with 45 additions and 3 deletions

View file

@ -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

View file

@ -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{}