Skip to content

Commit 0bfd4ba

Browse files
niikCopilot
andcommitted
Print full help on command misuse for invoking agents
When an agent misuses a command, the terse usage string does not carry the examples, JSON fields or environment variables it needs to correct itself, forcing a second `--help` invocation. Extract the help renderer out of rootHelpFunc so it can target any writer, then use it from printError when an agent is detected. Rendering directly to stderr avoids cmd.Help(), which writes to stdout and would otherwise split a single failure across two streams. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 61ff572 commit 0bfd4ba

3 files changed

Lines changed: 118 additions & 17 deletions

File tree

‎internal/ghcmd/cmd.go‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func Main() exitCode {
223223
return exitCode(extError.ExitCode())
224224
}
225225

226-
printError(stderr, err, cmd, hasDebug)
226+
printError(stderr, ioStreams.ColorScheme(), err, cmd, hasDebug, invokingAgent != "")
227227

228228
if strings.Contains(err.Error(), "Incorrect function") {
229229
fmt.Fprintln(stderr, "You appear to be running in MinTTY without pseudo terminal support.")
@@ -279,7 +279,12 @@ func isExtensionCommand(rootCmd *cobra.Command, args []string) bool {
279279
return err == nil && c != nil && c.GroupID == "extension"
280280
}
281281

282-
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
282+
// printError writes err to out, followed by usage information when the error
283+
// is the result of command misuse. When fullHelp is set the complete help text
284+
// is written instead of the terse usage string, giving AI agents the examples,
285+
// JSON fields and environment variables they need to correct themselves without
286+
// a second round trip.
287+
func printError(out io.Writer, cs *iostreams.ColorScheme, err error, cmd *cobra.Command, debug, fullHelp bool) {
283288
var dnsError *net.DNSError
284289
if errors.As(err, &dnsError) {
285290
fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)
@@ -297,6 +302,13 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
297302
if !strings.HasSuffix(err.Error(), "\n") {
298303
fmt.Fprintln(out)
299304
}
305+
if fullHelp {
306+
// Render into out rather than calling cmd.Help(), which would send
307+
// the help text to stdout and split a single failure across two
308+
// streams.
309+
root.WriteHelp(out, cs, cmd)
310+
return
311+
}
300312
fmt.Fprintln(out, cmd.UsageString())
301313
}
302314
}

‎internal/ghcmd/cmd_test.go‎

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,37 @@ import (
99
"os"
1010
"testing"
1111

12+
"github.com/MakeNowJust/heredoc"
1213
"github.com/cli/cli/v2/api"
1314
"github.com/cli/cli/v2/internal/agents"
1415
"github.com/cli/cli/v2/internal/config"
1516
"github.com/cli/cli/v2/internal/gh"
1617
ghmock "github.com/cli/cli/v2/internal/gh/mock"
1718
"github.com/cli/cli/v2/pkg/cmdutil"
19+
"github.com/cli/cli/v2/pkg/iostreams"
1820
ghAPI "github.com/cli/go-gh/v2/pkg/api"
1921
"github.com/spf13/cobra"
2022
"github.com/stretchr/testify/assert"
2123
"github.com/stretchr/testify/require"
2224
)
2325

2426
func Test_printError(t *testing.T) {
25-
cmd := &cobra.Command{}
27+
rootCmd := &cobra.Command{Use: "gh"}
28+
cmd := &cobra.Command{
29+
Use: "spend",
30+
Short: "Spend money",
31+
Example: heredoc.Doc(`
32+
$ gh spend --amount 1
33+
`),
34+
}
35+
cmd.Flags().Int("amount", 0, "How much to spend")
36+
rootCmd.AddCommand(cmd)
2637

2738
type args struct {
28-
err error
29-
cmd *cobra.Command
30-
debug bool
39+
err error
40+
cmd *cobra.Command
41+
debug bool
42+
fullHelp bool
3143
}
3244
tests := []struct {
3345
name string
@@ -63,7 +75,7 @@ check your internet connection or https://githubstatus.com
6375
cmd: cmd,
6476
debug: false,
6577
},
66-
wantOut: "unknown flag --foo\n\nUsage:\n\n",
78+
wantOut: "unknown flag --foo\n\n" + cmd.UsageString() + "\n",
6779
},
6880
{
6981
name: "unknown Cobra command error",
@@ -72,17 +84,86 @@ check your internet connection or https://githubstatus.com
7284
cmd: cmd,
7385
debug: false,
7486
},
75-
wantOut: "unknown command foo\n\nUsage:\n\n",
87+
wantOut: "unknown command foo\n\n" + cmd.UsageString() + "\n",
88+
},
89+
{
90+
name: "Cobra flag error with full help",
91+
args: args{
92+
err: cmdutil.FlagErrorf("unknown flag --foo"),
93+
cmd: cmd,
94+
debug: false,
95+
fullHelp: true,
96+
},
97+
wantOut: heredoc.Doc(`
98+
unknown flag --foo
99+
100+
Spend money
101+
102+
USAGE
103+
gh spend [flags]
104+
105+
FLAGS
106+
--amount int How much to spend
107+
108+
EXAMPLES
109+
$ gh spend --amount 1
110+
111+
LEARN MORE
112+
Use ` + "`gh <command> <subcommand> --help`" + ` for more information about a command.
113+
Read the manual at https://cli.github.com/manual
114+
Learn about exit codes using ` + "`gh help exit-codes`" + `
115+
Learn about accessibility experiences using ` + "`gh help accessibility`" + `
116+
117+
`),
118+
},
119+
{
120+
name: "unknown Cobra command error with full help",
121+
args: args{
122+
err: errors.New("unknown command foo"),
123+
cmd: cmd,
124+
debug: false,
125+
fullHelp: true,
126+
},
127+
wantOut: heredoc.Doc(`
128+
unknown command foo
129+
130+
Spend money
131+
132+
USAGE
133+
gh spend [flags]
134+
135+
FLAGS
136+
--amount int How much to spend
137+
138+
EXAMPLES
139+
$ gh spend --amount 1
140+
141+
LEARN MORE
142+
Use ` + "`gh <command> <subcommand> --help`" + ` for more information about a command.
143+
Read the manual at https://cli.github.com/manual
144+
Learn about exit codes using ` + "`gh help exit-codes`" + `
145+
Learn about accessibility experiences using ` + "`gh help accessibility`" + `
146+
147+
`),
148+
},
149+
{
150+
name: "generic error is unaffected by full help",
151+
args: args{
152+
err: errors.New("the app exploded"),
153+
cmd: cmd,
154+
debug: false,
155+
fullHelp: true,
156+
},
157+
wantOut: "the app exploded\n",
76158
},
77159
}
78160

79161
for _, tt := range tests {
80162
t.Run(tt.name, func(t *testing.T) {
163+
ios, _, _, _ := iostreams.Test()
81164
out := &bytes.Buffer{}
82-
printError(out, tt.args.err, tt.args.cmd, tt.args.debug)
83-
if gotOut := out.String(); gotOut != tt.wantOut {
84-
t.Errorf("printError() = %q, want %q", gotOut, tt.wantOut)
85-
}
165+
printError(out, ios.ColorScheme(), tt.args.err, tt.args.cmd, tt.args.debug, tt.args.fullHelp)
166+
assert.Equal(t, tt.wantOut, out.String())
86167
})
87168
}
88169
}

‎pkg/cmd/root/help.go‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/MakeNowJust/heredoc"
1212
"github.com/cli/cli/v2/internal/text"
1313
"github.com/cli/cli/v2/pkg/cmdutil"
14+
"github.com/cli/cli/v2/pkg/iostreams"
1415
"github.com/spf13/cobra"
1516
"github.com/spf13/pflag"
1617
)
@@ -109,6 +110,14 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
109110
return
110111
}
111112

113+
WriteHelp(f.IOStreams.Out, cs, command)
114+
}
115+
116+
// WriteHelp renders the full help text for command to w. This is the same
117+
// output produced by `gh <command> --help`, exposed separately so that callers
118+
// such as error reporting can render help to a stream of their choosing rather
119+
// than always writing to stdout.
120+
func WriteHelp(w io.Writer, cs *iostreams.ColorScheme, command *cobra.Command) {
112121
type helpEntry struct {
113122
Title string
114123
Body string
@@ -193,17 +202,16 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
193202
Learn about accessibility experiences using %[1]sgh help accessibility%[1]s
194203
`, "`")})
195204

196-
out := f.IOStreams.Out
197205
for _, e := range helpEntries {
198206
if e.Title != "" {
199207
// If there is a title, add indentation to each line in the body
200-
fmt.Fprintln(out, cs.Bold(e.Title))
201-
fmt.Fprintln(out, text.Indent(strings.Trim(e.Body, "\r\n"), " "))
208+
fmt.Fprintln(w, cs.Bold(e.Title))
209+
fmt.Fprintln(w, text.Indent(strings.Trim(e.Body, "\r\n"), " "))
202210
} else {
203211
// If there is no title print the body as is
204-
fmt.Fprintln(out, e.Body)
212+
fmt.Fprintln(w, e.Body)
205213
}
206-
fmt.Fprintln(out)
214+
fmt.Fprintln(w)
207215
}
208216
}
209217

0 commit comments

Comments
 (0)