Add file input support to agent-task create command

The agent-task create command now accepts a task description from a file using the -F/--from-file flag, with mutual exclusivity enforced between inline and file input. Tests were updated to cover new input scenarios and error cases, and usage examples were added to the command help.
This commit is contained in:
Kynan Ware 2025-09-03 15:24:01 -06:00
parent 6a50ecb880
commit b94ffe90c4
2 changed files with 122 additions and 13 deletions

View file

@ -5,10 +5,13 @@ import (
"errors"
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
@ -31,18 +34,35 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
opts := &CreateOptions{
IO: f.IOStreams,
}
var fromFileName string
cmd := &cobra.Command{
Use: "create \"<task description>\"",
Use: "create [<task description>] [flags]",
Short: "Create an agent task (preview)",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// TODO: We'll support prompting for the problem statement if not provided
// and from file flags, later.
if len(args) == 0 {
return cmdutil.FlagErrorf("a task description is required")
if err := cmdutil.MutuallyExclusive("only one of -F or arg can be provided", len(args) > 0, fromFileName != ""); err != nil {
return err
}
opts.ProblemStatement = args[0]
// Populate ProblemStatement from either arg or file
if len(args) > 0 {
opts.ProblemStatement = args[0]
} else if fromFileName != "" {
fileContent, err := os.ReadFile(fromFileName)
if err != nil {
return cmdutil.FlagErrorf("could not read task description file: %v", err)
}
trimmed := strings.TrimSpace(string(fileContent))
if trimmed == "" {
return cmdutil.FlagErrorf("task description file is empty")
}
opts.ProblemStatement = trimmed
}
if opts.ProblemStatement == "" {
return cmdutil.FlagErrorf("a task description is required")
}
// Support -R/--repo override
if f != nil {
opts.BaseRepo = f.BaseRepo
@ -52,11 +72,20 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
}
return createRun(opts)
},
Example: heredoc.Doc(`
# Create a task from an inline description
$ gh agent-task create "build me a new app"
# Create a task from a file
$ gh agent-task create -F task-desc.md
`),
}
if f != nil {
cmdutil.EnableRepoOverride(cmd, f)
}
cmd.Flags().StringVarP(&fromFileName, "from-file", "F", "", "Read task description from file")
opts.CapiClient = func() (capi.CapiClient, error) {
cfg, err := f.Config()
if err != nil {

View file

@ -2,6 +2,7 @@ package create
import (
"net/http"
"os"
"testing"
"github.com/MakeNowJust/heredoc"
@ -17,13 +18,92 @@ import (
// Test basic option parsing & repository requirement
func TestNewCmdCreate_Args(t *testing.T) {
f := &cmdutil.Factory{}
cmd := NewCmdCreate(f, func(o *CreateOptions) error { return nil })
// no args should error via cobra MinimumNArgs before our runF
// TODO once we support more sources of problem statement input,
// this will change.
_, err := cmd.ExecuteC()
require.Error(t, err)
type tc struct {
name string
args []string
fileContent string // if non-empty, create temp file and substitute {{FILE}} token in args
wantOpts *CreateOptions // nil when expecting error
expectedErr string
}
tests := []tc{
{
name: "no args nor file",
args: []string{},
expectedErr: "a task description is required",
},
{
name: "arg only success",
args: []string{"task description from args"},
wantOpts: &CreateOptions{
ProblemStatement: "task description from args",
},
},
{
name: "from-file success",
args: []string{"-F", "{{FILE}}"},
fileContent: "task description from file",
wantOpts: &CreateOptions{
ProblemStatement: "task description from file",
},
},
{
name: "mutually exclusive arg and file",
args: []string{"Some task inline", "-F", "{{FILE}}"},
fileContent: "Some task",
expectedErr: "only one of -F or arg can be provided",
},
{
name: "missing file path",
args: []string{"-F", "does-not-exist.md"},
expectedErr: "could not read task description file: open does-not-exist.md: no such file or directory",
},
{
name: "empty file",
args: []string{"-F", "{{FILE}}"},
fileContent: " \n\n",
expectedErr: "task description file is empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test file creation
var filePath string
if tt.fileContent != "" {
dir := t.TempDir()
filePath = dir + "/task.md"
if err := os.WriteFile(filePath, []byte(tt.fileContent), 0o600); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
// substitute placeholder
for i, a := range tt.args {
if a == "{{FILE}}" {
tt.args[i] = filePath
}
}
}
f := &cmdutil.Factory{}
var gotOpts *CreateOptions
cmd := NewCmdCreate(f, func(o *CreateOptions) error {
gotOpts = o
return nil
})
cmd.SetArgs(tt.args)
_, err := cmd.ExecuteC()
if tt.expectedErr != "" {
require.Error(t, err)
require.Equal(t, tt.expectedErr, err.Error())
return
}
require.NoError(t, err)
if tt.wantOpts != nil {
require.Equal(t, tt.wantOpts.ProblemStatement, gotOpts.ProblemStatement)
}
})
}
}
func Test_createRun(t *testing.T) {