Skip to content

Commit 28279a7

Browse files
Jason Barnettclaude
authored andcommitted
feat(cli): add --no-wait and --no-prompt flags to create command
Add two new flags to the `coder create` command: 1. `--no-wait`: Returns immediately after triggering workspace creation, without waiting for the build to complete. The workspace build will continue in the background. 2. `--no-prompt`: Disables all interactive prompts. Parameters with default values will use those defaults. Required parameters without defaults, missing workspace name, or missing template name will cause the command to fail with an appropriate error message. These flags enable non-interactive automation scenarios where the CLI should fail fast if any required input is missing, rather than hanging while waiting for user input. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 96fca01 commit 28279a7

File tree

3 files changed

+144
-3
lines changed

3 files changed

+144
-3
lines changed

cli/create.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
4646
parameterFlags workspaceParameterFlags
4747
autoUpdates string
4848
copyParametersFrom string
49+
noWait bool
50+
noPrompt bool
4951
// Organization context is only required if more than 1 template
5052
// shares the same name across multiple organizations.
5153
orgContext = NewOrganizationContext()
@@ -75,6 +77,9 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
7577
}
7678

7779
if workspaceName == "" {
80+
if noPrompt {
81+
return xerrors.Errorf("workspace name is required; use the first argument or specify with the command")
82+
}
7883
workspaceName, err = cliui.Prompt(inv, cliui.PromptOptions{
7984
Text: "Specify a name for your workspace:",
8085
Validate: func(workspaceName string) error {
@@ -122,6 +127,9 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
122127
var templateVersionID uuid.UUID
123128
switch {
124129
case templateName == "":
130+
if noPrompt {
131+
return xerrors.Errorf("template name is required; use --template to specify one")
132+
}
125133
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
126134

127135
templates, err := client.Templates(inv.Context(), codersdk.TemplateFilter{})
@@ -298,6 +306,9 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
298306
return xerrors.Errorf("unable to resolve preset: %w", err)
299307
}
300308
// If no preset found, prompt the user to choose a preset
309+
if noPrompt {
310+
return xerrors.Errorf("preset selection required but prompting is disabled; use --preset to specify one or --preset=none to skip")
311+
}
301312
if preset, err = promptPresetSelection(inv, tvPresets); err != nil {
302313
return xerrors.Errorf("unable to prompt user for preset: %w", err)
303314
}
@@ -330,6 +341,8 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
330341
RichParameterDefaults: cliBuildParameterDefaults,
331342

332343
SourceWorkspaceParameters: sourceWorkspaceParameters,
344+
345+
NoPrompt: noPrompt,
333346
})
334347
if err != nil {
335348
return xerrors.Errorf("prepare build: %w", err)
@@ -369,6 +382,11 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
369382

370383
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
371384

385+
if noWait {
386+
_, _ = fmt.Fprintf(inv.Stdout, "The %s workspace has been created. Building in the background...\n", cliui.Keyword(workspace.Name))
387+
return nil
388+
}
389+
372390
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, workspace.LatestBuild.ID)
373391
if err != nil {
374392
return xerrors.Errorf("watch build: %w", err)
@@ -436,6 +454,18 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
436454
Description: "Specify the source workspace name to copy parameters from.",
437455
Value: serpent.StringOf(&copyParametersFrom),
438456
},
457+
serpent.Option{
458+
Flag: "no-wait",
459+
Env: "CODER_NO_WAIT",
460+
Description: "Return immediately after creating the workspace. The workspace build will continue in the background.",
461+
Value: serpent.BoolOf(&noWait),
462+
},
463+
serpent.Option{
464+
Flag: "no-prompt",
465+
Env: "CODER_NO_PROMPT",
466+
Description: "Disable all prompts. Parameters with default values will use those defaults. Required parameters without defaults will cause the command to fail.",
467+
Value: serpent.BoolOf(&noPrompt),
468+
},
439469
cliui.SkipPromptOption(),
440470
)
441471
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
@@ -460,6 +490,9 @@ type prepWorkspaceBuildArgs struct {
460490
RichParameters []codersdk.WorkspaceBuildParameter
461491
RichParameterFile string
462492
RichParameterDefaults []codersdk.WorkspaceBuildParameter
493+
494+
// NoPrompt causes the build to fail if user input would be required
495+
NoPrompt bool
463496
}
464497

465498
// resolvePreset returns the preset matching the given presetName (if specified),
@@ -562,7 +595,8 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
562595
WithPromptRichParameters(args.PromptRichParameters).
563596
WithRichParameters(args.RichParameters).
564597
WithRichParametersFile(parameterFile).
565-
WithRichParametersDefaults(args.RichParameterDefaults)
598+
WithRichParametersDefaults(args.RichParameterDefaults).
599+
WithNoPrompt(args.NoPrompt)
566600
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
567601
if err != nil {
568602
return nil, err

cli/create_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,87 @@ func TestCreate(t *testing.T) {
297297
assert.Nil(t, ws.AutostartSchedule, "expected workspace autostart schedule to be nil")
298298
}
299299
})
300+
301+
t.Run("NoWait", func(t *testing.T) {
302+
t.Parallel()
303+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
304+
owner := coderdtest.CreateFirstUser(t, client)
305+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
306+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
307+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
308+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
309+
310+
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "-y", "--no-wait")
311+
clitest.SetupConfig(t, member, root)
312+
pty := ptytest.New(t).Attach(inv)
313+
314+
err := inv.Run()
315+
require.NoError(t, err)
316+
317+
pty.ExpectMatch("Building in the background")
318+
319+
// Workspace should exist even though we didn't wait
320+
ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
321+
require.NoError(t, err)
322+
require.Equal(t, template.Name, ws.TemplateName)
323+
})
324+
325+
t.Run("NoPrompt/MissingWorkspaceName", func(t *testing.T) {
326+
t.Parallel()
327+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
328+
owner := coderdtest.CreateFirstUser(t, client)
329+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
330+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
331+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
332+
_ = coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
333+
334+
// Don't provide workspace name and use --no-prompt
335+
inv, root := clitest.New(t, "create", "--no-prompt", "-y")
336+
clitest.SetupConfig(t, member, root)
337+
338+
err := inv.Run()
339+
require.Error(t, err)
340+
require.Contains(t, err.Error(), "workspace name is required")
341+
})
342+
343+
t.Run("NoPrompt/MissingTemplateName", func(t *testing.T) {
344+
t.Parallel()
345+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
346+
owner := coderdtest.CreateFirstUser(t, client)
347+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
348+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
349+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
350+
_ = coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
351+
352+
// Don't provide template name and use --no-prompt
353+
inv, root := clitest.New(t, "create", "my-workspace", "--no-prompt", "-y")
354+
clitest.SetupConfig(t, member, root)
355+
356+
err := inv.Run()
357+
require.Error(t, err)
358+
require.Contains(t, err.Error(), "template name is required")
359+
})
360+
361+
t.Run("NoPrompt/Success", func(t *testing.T) {
362+
t.Parallel()
363+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
364+
owner := coderdtest.CreateFirstUser(t, client)
365+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
366+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
367+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
368+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
369+
370+
// Provide all required values so no prompts are needed
371+
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--no-prompt", "-y")
372+
clitest.SetupConfig(t, member, root)
373+
374+
err := inv.Run()
375+
require.NoError(t, err)
376+
377+
ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
378+
require.NoError(t, err)
379+
require.Equal(t, template.Name, ws.TemplateName)
380+
})
300381
}
301382

302383
func prepareEchoResponses(parameters []*proto.RichParameter, presets ...*proto.Preset) *echo.Responses {

cli/parameterresolver.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type ParameterResolver struct {
3434

3535
promptRichParameters bool
3636
promptEphemeralParameters bool
37+
38+
// noPrompt causes the resolver to return an error if user input would be required
39+
noPrompt bool
3740
}
3841

3942
func (pr *ParameterResolver) WithLastBuildParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
@@ -86,6 +89,11 @@ func (pr *ParameterResolver) WithPromptEphemeralParameters(promptEphemeralParame
8689
return pr
8790
}
8891

92+
func (pr *ParameterResolver) WithNoPrompt(noPrompt bool) *ParameterResolver {
93+
pr.noPrompt = noPrompt
94+
return pr
95+
}
96+
8997
// Resolve gathers workspace build parameters in a layered fashion, applying values from various sources
9098
// in order of precedence: parameter file < CLI/ENV < source build < last build < preset < user input.
9199
func (pr *ParameterResolver) Resolve(inv *serpent.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
@@ -255,13 +263,31 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
255263
firstTimeUse := pr.isFirstTimeUse(tvp.Name)
256264
promptParameterOption := pr.isLastBuildParameterInvalidOption(tvp)
257265

258-
if (tvp.Ephemeral && pr.promptEphemeralParameters) ||
266+
needsInput := (tvp.Ephemeral && pr.promptEphemeralParameters) ||
259267
(action == WorkspaceCreate && tvp.Required) ||
260268
(action == WorkspaceCreate && !tvp.Ephemeral) ||
261269
(action == WorkspaceUpdate && promptParameterOption) ||
262270
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
263271
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
264-
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
272+
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters)
273+
274+
if needsInput {
275+
// If the parameter has a default value and we're in no-prompt mode, use the default
276+
if pr.noPrompt {
277+
if tvp.DefaultValue != "" {
278+
resolved = append(resolved, codersdk.WorkspaceBuildParameter{
279+
Name: tvp.Name,
280+
Value: tvp.DefaultValue,
281+
})
282+
continue
283+
}
284+
if tvp.Required {
285+
return nil, xerrors.Errorf("required parameter %q requires a value; use --parameter %s=<value>", tvp.Name, tvp.Name)
286+
}
287+
// Non-required parameter with no default, skip prompting
288+
continue
289+
}
290+
265291
parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
266292
if err != nil {
267293
return nil, err

0 commit comments

Comments
 (0)