Skip to content

Commit a2a758d

Browse files
chore(cli): re-order CLI create command (coder#19658)
Relates to coder/internal#893 Instead of `coder task create <template> --input <input>`, it is now `coder task create <input> --template <template>`. If there is only one AI task template on the deployment, the `--template` parameter can be omitted.
1 parent 5198127 commit a2a758d

File tree

2 files changed

+147
-28
lines changed

2 files changed

+147
-28
lines changed
Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"io"
56
"strings"
67

78
"github.com/google/uuid"
@@ -20,43 +21,49 @@ func (r *RootCmd) taskCreate() *serpent.Command {
2021
templateName string
2122
templateVersionName string
2223
presetName string
23-
taskInput string
24+
stdin bool
2425
)
2526

2627
cmd := &serpent.Command{
27-
Use: "create [template]",
28+
Use: "create [input]",
2829
Short: "Create an experimental task",
2930
Middleware: serpent.Chain(
3031
serpent.RequireRangeArgs(0, 1),
3132
r.InitClient(client),
3233
),
3334
Options: serpent.OptionSet{
3435
{
35-
Flag: "input",
36-
Env: "CODER_TASK_INPUT",
37-
Value: serpent.StringOf(&taskInput),
38-
Required: true,
39-
},
40-
{
36+
Name: "template",
37+
Flag: "template",
4138
Env: "CODER_TASK_TEMPLATE_NAME",
4239
Value: serpent.StringOf(&templateName),
4340
},
4441
{
42+
Name: "template-version",
43+
Flag: "template-version",
4544
Env: "CODER_TASK_TEMPLATE_VERSION",
4645
Value: serpent.StringOf(&templateVersionName),
4746
},
4847
{
48+
Name: "preset",
4949
Flag: "preset",
5050
Env: "CODER_TASK_PRESET_NAME",
5151
Value: serpent.StringOf(&presetName),
5252
Default: PresetNone,
5353
},
54+
{
55+
Name: "stdin",
56+
Flag: "stdin",
57+
Description: "Reads from stdin for the task input.",
58+
Value: serpent.BoolOf(&stdin),
59+
},
5460
},
5561
Handler: func(inv *serpent.Invocation) error {
5662
var (
5763
ctx = inv.Context()
5864
expClient = codersdk.NewExperimentalClient(client)
5965

66+
taskInput string
6067
templateVersionID uuid.UUID
6168
templateVersionPresetID uuid.UUID
6269
)
@@ -66,22 +73,68 @@ func (r *RootCmd) taskCreate() *serpent.Command {
6673
return xerrors.Errorf("get current organization: %w", err)
6774
}
6875

69-
if len(inv.Args) > 0 {
70-
templateName, templateVersionName, _ = strings.Cut(inv.Args[0], "@")
76+
if stdin {
77+
bytes, err := io.ReadAll(inv.Stdin)
78+
if err != nil {
79+
return xerrors.Errorf("reading stdin: %w", err)
80+
}
81+
82+
taskInput = string(bytes)
83+
} else {
84+
if len(inv.Args) != 1 {
85+
return xerrors.Errorf("expected an input for task")
86+
}
87+
88+
taskInput = inv.Args[0]
7189
}
7290

73-
if templateName == "" {
74-
return xerrors.Errorf("template name not provided")
91+
if taskInput == "" {
92+
return xerrors.Errorf("a task cannot be started with an empty input")
7593
}
7694

77-
if templateVersionName != "" {
95+
switch {
96+
case templateName == "":
97+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{SearchQuery: "has-ai-task:true", OrganizationID: organization.ID})
98+
if err != nil {
99+
return xerrors.Errorf("list templates: %w", err)
100+
}
101+
102+
if len(templates) == 0 {
103+
return xerrors.Errorf("no task templates configured")
104+
}
105+
106+
// When a deployment has only 1 AI task template, we will
107+
// allow omitting the template. Otherwise we will require
108+
// the user to be explicit with their choice of template.
109+
if len(templates) > 1 {
110+
templateNames := make([]string, 0, len(templates))
111+
for _, template := range templates {
112+
templateNames = append(templateNames, template.Name)
113+
}
114+
115+
return xerrors.Errorf("template name not provided, available templates: %s", strings.Join(templateNames, ", "))
116+
}
117+
118+
if templateVersionName != "" {
119+
templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, templates[0].Name, templateVersionName)
120+
if err != nil {
121+
return xerrors.Errorf("get template version: %w", err)
122+
}
123+
124+
templateVersionID = templateVersion.ID
125+
} else {
126+
templateVersionID = templates[0].ActiveVersionID
127+
}
128+
129+
case templateVersionName != "":
78130
templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, templateName, templateVersionName)
79131
if err != nil {
80132
return xerrors.Errorf("get template version: %w", err)
81133
}
82134

83135
templateVersionID = templateVersion.ID
84-
} else {
136+
137+
default:
85138
template, err := client.TemplateByName(ctx, organization.ID, templateName)
86139
if err != nil {
87140
return xerrors.Errorf("get template: %w", err)
Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ func TestTaskCreate(t *testing.T) {
6060
Name: presetName,
6161
},
6262
})
63+
case "/api/v2/templates":
64+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Template{
65+
{
66+
ID: templateID,
67+
Name: templateName,
68+
ActiveVersionID: templateVersionID,
69+
},
70+
})
6371
case "/api/experimental/tasks/me":
6472
var req codersdk.CreateTaskRequest
6573
if !httpapi.Read(ctx, w, r, &req) {
@@ -88,71 +96,80 @@ func TestTaskCreate(t *testing.T) {
8896
tests := []struct {
8997
args []string
9098
env []string
99+
stdin string
91100
expectError string
92101
expectOutput string
93102
handler func(t *testing.T, ctx context.Context) http.HandlerFunc
94103
}{
95104
{
96-
args: []string{"my-template@my-template-version", "--input", "my custom prompt", "--org", organizationID.String()},
105+
args: []string{"--stdin"},
106+
stdin: "reads prompt from stdin",
107+
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
108+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
109+
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "reads prompt from stdin")
110+
},
111+
},
112+
{
113+
args: []string{"my custom prompt"},
97114
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
98115
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
99116
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
100117
},
101118
},
102119
{
103-
args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()},
104-
env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"},
120+
args: []string{"my custom prompt", "--template", "my-template", "--template-version", "my-template-version", "--org", organizationID.String()},
105121
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
106122
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
107123
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
108124
},
109125
},
110126
{
111-
args: []string{"--input", "my custom prompt", "--org", organizationID.String()},
112-
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"},
127+
args: []string{"my custom prompt", "--template", "my-template", "--org", organizationID.String()},
128+
env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"},
113129
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
114130
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
115131
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
116132
},
117133
},
118134
{
119-
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt", "CODER_ORGANIZATION=" + organizationID.String()},
135+
args: []string{"my custom prompt", "--org", organizationID.String()},
136+
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"},
120137
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
121138
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
122139
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
123140
},
124141
},
125142
{
126-
args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()},
143+
args: []string{"my custom prompt", "--template", "my-template", "--org", organizationID.String()},
127144
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
128145
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
129146
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "", "my custom prompt")
130147
},
131148
},
132149
{
133-
args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset", "--org", organizationID.String()},
150+
args: []string{"my custom prompt", "--template", "my-template", "--preset", "my-preset", "--org", organizationID.String()},
134151
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
135152
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
136153
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
137154
},
138155
},
139156
{
140-
args: []string{"my-template", "--input", "my custom prompt"},
157+
args: []string{"my custom prompt", "--template", "my-template"},
141158
env: []string{"CODER_TASK_PRESET_NAME=my-preset"},
142159
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
143160
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
144161
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
145162
},
146163
},
147164
{
148-
args: []string{"my-template", "--input", "my custom prompt", "--preset", "not-real-preset"},
165+
args: []string{"my custom prompt", "--template", "my-template", "--preset", "not-real-preset"},
149166
expectError: `preset "not-real-preset" not found`,
150167
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
151168
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
152169
},
153170
},
154171
{
155-
args: []string{"my-template@not-real-template-version", "--input", "my custom prompt"},
172+
args: []string{"my custom prompt", "--template", "my-template", "--template-version", "not-real-template-version"},
156173
expectError: httpapi.ResourceNotFoundResponse.Message,
157174
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
158175
return func(w http.ResponseWriter, r *http.Request) {
@@ -163,6 +180,11 @@ func TestTaskCreate(t *testing.T) {
163180
ID: organizationID,
164181
}},
165182
})
183+
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", organizationID):
184+
httpapi.Write(ctx, w, http.StatusOK, codersdk.Template{
185+
ID: templateID,
186+
ActiveVersionID: templateVersionID,
187+
})
166188
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/not-real-template-version", organizationID):
167189
httpapi.ResourceNotFound(w)
168190
default:
@@ -172,7 +194,7 @@ func TestTaskCreate(t *testing.T) {
172194
},
173195
},
174196
{
175-
args: []string{"not-real-template", "--input", "my custom prompt", "--org", organizationID.String()},
197+
args: []string{"my custom prompt", "--template", "not-real-template", "--org", organizationID.String()},
176198
expectError: httpapi.ResourceNotFoundResponse.Message,
177199
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
178200
return func(w http.ResponseWriter, r *http.Request) {
@@ -192,7 +214,7 @@ func TestTaskCreate(t *testing.T) {
192214
},
193215
},
194216
{
195-
args: []string{"template-in-different-org", "--input", "my-custom-prompt", "--org", anotherOrganizationID.String()},
217+
args: []string{"my-custom-prompt", "--template", "template-in-different-org", "--org", anotherOrganizationID.String()},
196218
expectError: httpapi.ResourceNotFoundResponse.Message,
197219
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
198220
return func(w http.ResponseWriter, r *http.Request) {
@@ -212,7 +234,7 @@ func TestTaskCreate(t *testing.T) {
212234
},
213235
},
214236
{
215-
args: []string{"no-org", "--input", "my-custom-prompt"},
237+
args: []string{"no-org-prompt"},
216238
expectError: "Must select an organization with --org=<org_name>",
217239
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
218240
return func(w http.ResponseWriter, r *http.Request) {
@@ -225,6 +247,49 @@ func TestTaskCreate(t *testing.T) {
225247
}
226248
},
227249
},
250+
{
251+
args: []string{"no task templates"},
252+
expectError: "no task templates configured",
253+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
254+
return func(w http.ResponseWriter, r *http.Request) {
255+
switch r.URL.Path {
256+
case "/api/v2/users/me/organizations":
257+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
258+
{MinimalOrganization: codersdk.MinimalOrganization{
259+
ID: organizationID,
260+
}},
261+
})
262+
case "/api/v2/templates":
263+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Template{})
264+
default:
265+
t.Errorf("unexpected path: %s", r.URL.Path)
266+
}
267+
}
268+
},
269+
},
270+
{
271+
args: []string{"no template name provided"},
272+
expectError: "template name not provided, available templates: wibble, wobble",
273+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
274+
return func(w http.ResponseWriter, r *http.Request) {
275+
switch r.URL.Path {
276+
case "/api/v2/users/me/organizations":
277+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
278+
{MinimalOrganization: codersdk.MinimalOrganization{
279+
ID: organizationID,
280+
}},
281+
})
282+
case "/api/v2/templates":
283+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Template{
284+
{Name: "wibble"},
285+
{Name: "wobble"},
286+
})
287+
default:
288+
t.Errorf("unexpected path: %s", r.URL.Path)
289+
}
290+
}
291+
},
292+
},
228293
}
229294

230295
for _, tt := range tests {
@@ -244,6 +309,7 @@ func TestTaskCreate(t *testing.T) {
244309

245310
inv, root := clitest.New(t, append(args, tt.args...)...)
246311
inv.Environ = serpent.ParseEnviron(tt.env, "")
312+
inv.Stdin = strings.NewReader(tt.stdin)
247313
inv.Stdout = &sb
248314
inv.Stderr = &sb
249315
clitest.SetupConfig(t, client, root)

0 commit comments

Comments
 (0)