Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(cli): add task create command
  • Loading branch information
DanielleMaywood committed Aug 26, 2025
commit 8c55100fc7400faff34bea2ebcb93b21222dc237
173 changes: 173 additions & 0 deletions cli/exp_taskcreate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cli

import (
"fmt"
"slices"
"strings"
"time"

"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
"github.com/google/uuid"
"golang.org/x/xerrors"
)

func (r *RootCmd) taskCreate() *serpent.Command {
var (
orgContext = NewOrganizationContext()
client = new(codersdk.Client)

templateName string
templateVersionName string
presetName string
taskInput string
)

return &serpent.Command{
Use: "create [task]",
Short: "Create an experimental task",
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 1),
r.InitClient(client),
),
Options: serpent.OptionSet{
{
Flag: "input",
Env: "CODER_TASK_INPUT",
Value: serpent.StringOf(&taskInput),
Required: true,
},
{
Env: "CODER_TEMPLATE_NAME",
Value: serpent.StringOf(&templateName),
},
{
Env: "CODER_TEMPLATE_VERSION",
Value: serpent.StringOf(&templateVersionName),
},
{
Flag: "preset",
Env: "CODER_PRESET_NAME",
Value: serpent.StringOf(&presetName),
Default: PresetNone,
},
},
Handler: func(inv *serpent.Invocation) error {
var (
ctx = inv.Context()
expClient = codersdk.NewExperimentalClient(client)

templateVersionID uuid.UUID
templateVersionPresetID uuid.UUID
)

organization, err := orgContext.Selected(inv, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}

if len(inv.Args) > 0 {
templateName, templateVersionName, _ = strings.Cut(inv.Args[0], "@")
}

if templateName == "" {
templates, err := client.Templates(ctx, codersdk.TemplateFilter{SearchQuery: "has-ai-task:true"})
if err != nil {
return xerrors.Errorf("get templates: %w", err)
}

slices.SortFunc(templates, func(a, b codersdk.Template) int {
return slice.Descending(a.ActiveUserCount, b.ActiveUserCount)
})

templateNames := make([]string, 0, len(templates))
templateByName := make(map[string]codersdk.Template, len(templates))

// If more than 1 organization exists in the list of templates,
// then include the organization name in the select options.
uniqueOrganizations := make(map[uuid.UUID]bool)
for _, template := range templates {
uniqueOrganizations[template.OrganizationID] = true
}

for _, template := range templates {
templateName := template.Name
if len(uniqueOrganizations) > 1 {
templateName += cliui.Placeholder(
fmt.Sprintf(
" (%s)",
template.OrganizationName,
),
)
}

if template.ActiveUserCount > 0 {
templateName += cliui.Placeholder(
fmt.Sprintf(
" used by %s",
formatActiveDevelopers(template.ActiveUserCount),
),
)
}

templateNames = append(templateNames, templateName)
templateByName[templateName] = template
}

option, err := cliui.Select(inv, cliui.SelectOptions{
Options: templateNames,
HideSearch: true,
})

templateName = templateByName[option].Name
}

if 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 {
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)
}

templateVersionID = preset.ID
}

workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: templateVersionID,
TemplateVersionPresetID: templateVersionPresetID,
Prompt: taskInput,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Not sure if we're too far gone to do this, but potential for rename: Prompt -> Input depending on how we want to standardize this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is all experimental we technically still have time

})

_, _ = fmt.Fprintf(
inv.Stdout,
"The task %s has been created at %s!\n",
cliui.Keyword(workspace.Name),
cliui.Timestamp(time.Now()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workspace.CreatedAt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I've implemented it is how we currently do it for workspace creation but I'm happy to create a quick follow up PR to address this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you tbh, I'm good with either just thought it made sense so threw it out there. 👍🏻

)

return nil
},
}
}
119 changes: 119 additions & 0 deletions cli/taskcreate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package cli_test

import (
"fmt"
"testing"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)

func TestTaskCreate(t *testing.T) {
t.Parallel()

createAITemplate := func(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse) (codersdk.TemplateVersion, codersdk.Template) {
t.Helper()

taskAppID := uuid.New()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true,
AiTasks: []*proto.AITask{},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "example",
Apps: []*proto.App{
{
Id: taskAppID.String(),
Slug: "task-sidebar",
DisplayName: "Task Sidebar",
},
},
}},
}},
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
AiTasks: []*proto.AITask{{
SidebarApp: &proto.AITaskSidebarApp{
Id: taskAppID.String(),
},
}},
},
},
},
},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)

return version, template
}

t.Run("CreateWithTemplateNameAndVersion", func(t *testing.T) {
t.Parallel()

var (
ctx = testutil.Context(t, testutil.WaitShort)

prompt = "Task prompt"
)

client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
templateVersion, template := createAITemplate(t, client, owner)

member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
expMember := codersdk.NewExperimentalClient(member)

tasks, err := expMember.Tasks(ctx, nil)
require.NoError(t, err)
require.Empty(t, tasks)

args := []string{
"exp",
"task",
"create",
fmt.Sprintf("%s@%s", template.Name, templateVersion.Name),
"--input", prompt,
}

inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)

err = inv.Run()
require.NoError(t, err)

workspaces, err := member.Workspaces(ctx, codersdk.WorkspaceFilter{FilterQuery: "has-ai-task:true"})
require.NoError(t, err)
require.Len(t, workspaces.Workspaces, 1)

coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, workspaces.Workspaces[0].LatestBuild.ID)

tasks, err = expMember.Tasks(ctx, nil)
require.NoError(t, err)
require.Len(t, tasks, 1)

require.Equal(t, prompt, tasks[0].InitialPrompt)
})
}