mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(cli): re-order CLI create command (#19658)
Relates to https://github.com/coder/internal/issues/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.
This commit is contained in:
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -20,11 +21,11 @@ func (r *RootCmd) taskCreate() *serpent.Command {
|
||||
templateName string
|
||||
templateVersionName string
|
||||
presetName string
|
||||
taskInput string
|
||||
stdin bool
|
||||
)
|
||||
|
||||
cmd := &serpent.Command{
|
||||
Use: "create [template]",
|
||||
Use: "create [input]",
|
||||
Short: "Create an experimental task",
|
||||
Middleware: serpent.Chain(
|
||||
serpent.RequireRangeArgs(0, 1),
|
||||
@@ -32,31 +33,37 @@ func (r *RootCmd) taskCreate() *serpent.Command {
|
||||
),
|
||||
Options: serpent.OptionSet{
|
||||
{
|
||||
Flag: "input",
|
||||
Env: "CODER_TASK_INPUT",
|
||||
Value: serpent.StringOf(&taskInput),
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "template",
|
||||
Flag: "template",
|
||||
Env: "CODER_TASK_TEMPLATE_NAME",
|
||||
Value: serpent.StringOf(&templateName),
|
||||
},
|
||||
{
|
||||
Name: "template-version",
|
||||
Flag: "template-version",
|
||||
Env: "CODER_TASK_TEMPLATE_VERSION",
|
||||
Value: serpent.StringOf(&templateVersionName),
|
||||
},
|
||||
{
|
||||
Name: "preset",
|
||||
Flag: "preset",
|
||||
Env: "CODER_TASK_PRESET_NAME",
|
||||
Value: serpent.StringOf(&presetName),
|
||||
Default: PresetNone,
|
||||
},
|
||||
{
|
||||
Name: "stdin",
|
||||
Flag: "stdin",
|
||||
Description: "Reads from stdin for the task input.",
|
||||
Value: serpent.BoolOf(&stdin),
|
||||
},
|
||||
},
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
var (
|
||||
ctx = inv.Context()
|
||||
expClient = codersdk.NewExperimentalClient(client)
|
||||
|
||||
taskInput string
|
||||
templateVersionID uuid.UUID
|
||||
templateVersionPresetID uuid.UUID
|
||||
)
|
||||
@@ -66,22 +73,68 @@ func (r *RootCmd) taskCreate() *serpent.Command {
|
||||
return xerrors.Errorf("get current organization: %w", err)
|
||||
}
|
||||
|
||||
if len(inv.Args) > 0 {
|
||||
templateName, templateVersionName, _ = strings.Cut(inv.Args[0], "@")
|
||||
if stdin {
|
||||
bytes, err := io.ReadAll(inv.Stdin)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("reading stdin: %w", err)
|
||||
}
|
||||
|
||||
taskInput = string(bytes)
|
||||
} else {
|
||||
if len(inv.Args) != 1 {
|
||||
return xerrors.Errorf("expected an input for task")
|
||||
}
|
||||
|
||||
taskInput = inv.Args[0]
|
||||
}
|
||||
|
||||
if templateName == "" {
|
||||
return xerrors.Errorf("template name not provided")
|
||||
if taskInput == "" {
|
||||
return xerrors.Errorf("a task cannot be started with an empty input")
|
||||
}
|
||||
|
||||
if templateVersionName != "" {
|
||||
switch {
|
||||
case templateName == "":
|
||||
templates, err := client.Templates(ctx, codersdk.TemplateFilter{SearchQuery: "has-ai-task:true", OrganizationID: organization.ID})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("list templates: %w", err)
|
||||
}
|
||||
|
||||
if len(templates) == 0 {
|
||||
return xerrors.Errorf("no task templates configured")
|
||||
}
|
||||
|
||||
// When a deployment has only 1 AI task template, we will
|
||||
// allow omitting the template. Otherwise we will require
|
||||
// the user to be explicit with their choice of template.
|
||||
if len(templates) > 1 {
|
||||
templateNames := make([]string, 0, len(templates))
|
||||
for _, template := range templates {
|
||||
templateNames = append(templateNames, template.Name)
|
||||
}
|
||||
|
||||
return xerrors.Errorf("template name not provided, available templates: %s", strings.Join(templateNames, ", "))
|
||||
}
|
||||
|
||||
if templateVersionName != "" {
|
||||
templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, templates[0].Name, templateVersionName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template version: %w", err)
|
||||
}
|
||||
|
||||
templateVersionID = templateVersion.ID
|
||||
} else {
|
||||
templateVersionID = templates[0].ActiveVersionID
|
||||
}
|
||||
|
||||
case templateVersionName != "":
|
||||
templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, templateName, templateVersionName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template version: %w", err)
|
||||
}
|
||||
|
||||
templateVersionID = templateVersion.ID
|
||||
} else {
|
||||
|
||||
default:
|
||||
template, err := client.TemplateByName(ctx, organization.ID, templateName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template: %w", err)
|
||||
@@ -60,6 +60,14 @@ func TestTaskCreate(t *testing.T) {
|
||||
Name: presetName,
|
||||
},
|
||||
})
|
||||
case "/api/v2/templates":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Template{
|
||||
{
|
||||
ID: templateID,
|
||||
Name: templateName,
|
||||
ActiveVersionID: templateVersionID,
|
||||
},
|
||||
})
|
||||
case "/api/experimental/tasks/me":
|
||||
var req codersdk.CreateTaskRequest
|
||||
if !httpapi.Read(ctx, w, r, &req) {
|
||||
@@ -88,19 +96,35 @@ func TestTaskCreate(t *testing.T) {
|
||||
tests := []struct {
|
||||
args []string
|
||||
env []string
|
||||
stdin string
|
||||
expectError string
|
||||
expectOutput string
|
||||
handler func(t *testing.T, ctx context.Context) http.HandlerFunc
|
||||
}{
|
||||
{
|
||||
args: []string{"my-template@my-template-version", "--input", "my custom prompt", "--org", organizationID.String()},
|
||||
args: []string{"--stdin"},
|
||||
stdin: "reads prompt from stdin",
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "reads prompt from stdin")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my custom prompt"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()},
|
||||
args: []string{"my custom prompt", "--template", "my-template", "--template-version", "my-template-version", "--org", organizationID.String()},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my custom prompt", "--template", "my-template", "--org", organizationID.String()},
|
||||
env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
@@ -108,7 +132,7 @@ func TestTaskCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--input", "my custom prompt", "--org", organizationID.String()},
|
||||
args: []string{"my custom prompt", "--org", organizationID.String()},
|
||||
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
@@ -116,28 +140,21 @@ func TestTaskCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
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()},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()},
|
||||
args: []string{"my custom prompt", "--template", "my-template", "--org", organizationID.String()},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset", "--org", organizationID.String()},
|
||||
args: []string{"my custom prompt", "--template", "my-template", "--preset", "my-preset", "--org", organizationID.String()},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt"},
|
||||
args: []string{"my custom prompt", "--template", "my-template"},
|
||||
env: []string{"CODER_TASK_PRESET_NAME=my-preset"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
@@ -145,14 +162,14 @@ func TestTaskCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt", "--preset", "not-real-preset"},
|
||||
args: []string{"my custom prompt", "--template", "my-template", "--preset", "not-real-preset"},
|
||||
expectError: `preset "not-real-preset" not found`,
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template@not-real-template-version", "--input", "my custom prompt"},
|
||||
args: []string{"my custom prompt", "--template", "my-template", "--template-version", "not-real-template-version"},
|
||||
expectError: httpapi.ResourceNotFoundResponse.Message,
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -163,6 +180,11 @@ func TestTaskCreate(t *testing.T) {
|
||||
ID: organizationID,
|
||||
}},
|
||||
})
|
||||
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", organizationID):
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.Template{
|
||||
ID: templateID,
|
||||
ActiveVersionID: templateVersionID,
|
||||
})
|
||||
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/not-real-template-version", organizationID):
|
||||
httpapi.ResourceNotFound(w)
|
||||
default:
|
||||
@@ -172,7 +194,7 @@ func TestTaskCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"not-real-template", "--input", "my custom prompt", "--org", organizationID.String()},
|
||||
args: []string{"my custom prompt", "--template", "not-real-template", "--org", organizationID.String()},
|
||||
expectError: httpapi.ResourceNotFoundResponse.Message,
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -192,7 +214,7 @@ func TestTaskCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"template-in-different-org", "--input", "my-custom-prompt", "--org", anotherOrganizationID.String()},
|
||||
args: []string{"my-custom-prompt", "--template", "template-in-different-org", "--org", anotherOrganizationID.String()},
|
||||
expectError: httpapi.ResourceNotFoundResponse.Message,
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -212,7 +234,7 @@ func TestTaskCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"no-org", "--input", "my-custom-prompt"},
|
||||
args: []string{"no-org-prompt"},
|
||||
expectError: "Must select an organization with --org=<org_name>",
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -225,6 +247,49 @@ func TestTaskCreate(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"no task templates"},
|
||||
expectError: "no task templates configured",
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v2/users/me/organizations":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
|
||||
{MinimalOrganization: codersdk.MinimalOrganization{
|
||||
ID: organizationID,
|
||||
}},
|
||||
})
|
||||
case "/api/v2/templates":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Template{})
|
||||
default:
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"no template name provided"},
|
||||
expectError: "template name not provided, available templates: wibble, wobble",
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v2/users/me/organizations":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
|
||||
{MinimalOrganization: codersdk.MinimalOrganization{
|
||||
ID: organizationID,
|
||||
}},
|
||||
})
|
||||
case "/api/v2/templates":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Template{
|
||||
{Name: "wibble"},
|
||||
{Name: "wobble"},
|
||||
})
|
||||
default:
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -244,6 +309,7 @@ func TestTaskCreate(t *testing.T) {
|
||||
|
||||
inv, root := clitest.New(t, append(args, tt.args...)...)
|
||||
inv.Environ = serpent.ParseEnviron(tt.env, "")
|
||||
inv.Stdin = strings.NewReader(tt.stdin)
|
||||
inv.Stdout = &sb
|
||||
inv.Stderr = &sb
|
||||
clitest.SetupConfig(t, client, root)
|
||||
Reference in New Issue
Block a user