Files
coder/cli/task_create.go
T
Mathias Fredriksson ad8ba4aac6 feat(cli): promote tasks commands from experimental to GA (#20916)
## Overview

This change promotes the tasks CLI commands from `coder exp task` to
`coder task`, marking them as generally available (GA).

## Migration

Users will need to update their scripts from:

```shell
coder exp task create "my task"
```

To:
```shell
coder task create "my task"
```

---

🤖 This change was written by Claude Sonnet 4.5 Thinking using [mux](https://github.com/coder/mux) and reviewed by a human 🏄🏻‍♂️
2025-11-25 13:50:22 +00:00

238 lines
6.5 KiB
Go

package cli
import (
"fmt"
"io"
"strings"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) taskCreate() *serpent.Command {
var (
orgContext = NewOrganizationContext()
ownerArg string
taskName string
templateName string
templateVersionName string
presetName string
stdin bool
quiet bool
)
cmd := &serpent.Command{
Use: "create [input]",
Short: "Create a task",
Long: FormatExamples(
Example{
Description: "Create a task with direct input",
Command: "coder task create \"Add authentication to the user service\"",
},
Example{
Description: "Create a task with stdin input",
Command: "echo \"Add authentication to the user service\" | coder task create",
},
Example{
Description: "Create a task with a specific name",
Command: "coder task create --name task1 \"Add authentication to the user service\"",
},
Example{
Description: "Create a task from a specific template / preset",
Command: "coder task create --template backend-dev --preset \"My Preset\" \"Add authentication to the user service\"",
},
Example{
Description: "Create a task for another user (requires appropriate permissions)",
Command: "coder task create --owner user@example.com \"Add authentication to the user service\"",
},
),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 1),
),
Options: serpent.OptionSet{
{
Name: "name",
Flag: "name",
Description: "Specify the name of the task. If you do not specify one, a name will be generated for you.",
Value: serpent.StringOf(&taskName),
Required: false,
Default: "",
},
{
Name: "owner",
Flag: "owner",
Description: "Specify the owner of the task. Defaults to the current user.",
Value: serpent.StringOf(&ownerArg),
Required: false,
Default: codersdk.Me,
},
{
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),
},
{
Name: "quiet",
Flag: "quiet",
FlagShorthand: "q",
Description: "Only display the created task's ID.",
Value: serpent.BoolOf(&quiet),
},
},
Handler: func(inv *serpent.Invocation) error {
client, err := r.InitClient(inv)
if err != nil {
return err
}
var (
ctx = inv.Context()
expClient = codersdk.NewExperimentalClient(client)
taskInput string
templateVersionID uuid.UUID
templateVersionPresetID uuid.UUID
)
organization, err := orgContext.Selected(inv, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}
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 taskInput == "" {
return xerrors.Errorf("a task cannot be started with an empty input")
}
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
default:
template, err := client.TemplateByName(ctx, organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template: %w", err)
}
templateVersionID = template.ActiveVersionID
}
if presetName != PresetNone {
templatePresets, err := client.TemplateVersionPresets(ctx, templateVersionID)
if err != nil {
return xerrors.Errorf("get template presets: %w", err)
}
preset, err := resolvePreset(templatePresets, presetName)
if err != nil {
return xerrors.Errorf("resolve preset: %w", err)
}
templateVersionPresetID = preset.ID
}
task, err := expClient.CreateTask(ctx, ownerArg, codersdk.CreateTaskRequest{
Name: taskName,
TemplateVersionID: templateVersionID,
TemplateVersionPresetID: templateVersionPresetID,
Input: taskInput,
})
if err != nil {
return xerrors.Errorf("create task: %w", err)
}
if quiet {
_, _ = fmt.Fprintln(inv.Stdout, task.ID)
} else {
_, _ = fmt.Fprintf(
inv.Stdout,
"The task %s has been created at %s!\n",
cliui.Keyword(task.Name),
cliui.Timestamp(task.CreatedAt),
)
}
return nil
},
}
orgContext.AttachOptions(cmd)
return cmd
}