Skip to content

Commit 574158c

Browse files
authored
Fail fast on interactive prompts in non-interactive shells (#209)
## Problem Interactive pterm prompts (confirm / select / text input) **never return when stdin is not a terminal**. The underlying keyboard listener (`atomicgo.dev/keyboard`) swallows both the "provided file is not a console" init error and the subsequent nil-file read errors, so it spins forever re-rendering the prompt. Empirically: `kernel create < /dev/null` produced **~13 MB of ANSI output in 10 seconds** at 100% CPU and never exited. Any AI agent or CI script that hits a prompting code path hangs until killed — and floods the agent's context with escape codes on the way down. ## Fix **`pkg/interactive` owns every prompt.** `Confirm`, `Select`, and `TextInput` execute the pterm prompts behind a stdin TTY gate; command code supplies only the action, hint, and options. No direct pterm interactive calls exist outside the package, so the no-hang invariant is enforced by the boundary, not a call-site convention. In a non-interactive shell each primitive fails fast with a typed, actionable error: **Confirmation prompts** → direct the caller to `--yes`: ``` ERROR: Cannot prompt for confirmation to delete deployment 'fake-id': stdin is not an interactive terminal; re-run with --yes to skip the confirmation prompt ``` Gated: `api-keys/app/auth connections/credential-providers/credentials/deploy/extensions/profiles/proxies delete`, the `browsers create` pool-conflict confirm, and the `create` overwrite confirm. **Text/select prompts** → name the exact flag(s) and valid values to pass instead. `kernel create` (the only command with multiple promptable inputs) resolves **everything up front** and reports all problems in a single error — one problem per line, flag tokens never split by terminal-width re-wrapping: ``` ERROR: Cannot prompt for input: stdin is not an interactive terminal; fix all of the following and re-run: - --name is required (e.g. --name my-kernel-app) - --language 'ruby' is invalid: must be one of: typescript (ts), python (py) - --template is required (run 'kernel create --help' for the full list) ``` An existing target directory (which would need the overwrite confirm) is folded into the same error as a `pass --yes ...` item. Single-input prompts stay single-line: ``` ERROR: Cannot prompt for viewport selection: stdin is not an interactive terminal; pass --viewport instead of --viewport-interactive (e.g. --viewport 1920x1080@25) ``` All errors exit non-zero (previously, cancelled confirms exited 0, and prompt errors were silently dropped via `ok, _ :=`). ## Design - **`pkg/interactive`** (new): `IsInteractive()` + `Confirm`/`Select`/`TextInput` prompt primitives + typed **`PromptError`**. `Error()` is single-line (Go convention, problems numbered inline); `Display()` renders one problem per line. The root error handler renders `PromptError` with fang's `ErrorText` width and transform unset — the width-based word-wrap split flag tokens (`--` + newline + `template`) and the transform (`strings.Fields` + `Join`) collapsed newlines. Prompts execute through a **`Prompter`** that carries its terminal capability per value (zero value = ambient stdin detection; `NewPrompterWithTerminal(false)` for deterministic tests) — no package-level mutable state. Command structs carry an injected `prompter` field; plain-function commands use the default Prompter. - **`create.ResolveInput`** (new): the single canonical resolver from raw flags to a normalized `CreateInput` plus typed `Problem`s (name, language, template, overwrite). Both modes consume the same problem model: the interactive flow prompts to resolve one problem at a time and re-resolves (so the template list reflects the chosen language, and invalid provided values warn before re-prompting); the non-interactive flow reports every problem in one fail-fast error. A `--template` that exists for no language is reported even when `--language` is also invalid, to avoid a hidden extra retry. Prompts in `pkg/create` are thin per-field wrappers with no validation logic, and every emitted `Problem` carries its own interactive resolution step, so the resolver/dispatcher contract is exhaustive by construction (no field switch to drift; a step-less Problem fails with an internal error naming the field instead of looping). - **`--yes` stays the single skip-confirmation pattern** (no `--force`/`--confirm` mixing; existing `--force` flags all mean something semantically different and are untouched). Two confirms had no bypass and now gain one: - `kernel create --yes/-y` — overwrite an existing directory without asking - `kernel browsers create --yes/-y` — proceed past the pool-conflict confirm (`yes` added to `poolLeaseAllowedFlags` so it doesn't count as a conflicting flag itself) - Updated `kernel create` long help to document the fail-fast behavior. ## Intentionally out of scope `kernel login`, `kernel browsers ssh`, and `kernel upgrade` are left alone — agents can use them as-is. (Possible follow-up: a two-step `kernel login` that prints the URL and confirms separately. Another possible follow-up: a forbidigo lint rule banning `pterm.DefaultInteractive*` outside `pkg/interactive`; the repo has no golangci config today.) ## Testing - `make test` passes. Gating tests **inject the terminal capability** (`interactive.NewPrompterWithTerminal(false)`) instead of relying on the harness's ambient stdin — no global state, parallel-safe (verified with `-race -shuffle=on -count=2` and by running the compiled test binaries under a PTY); the detector is tested against an explicit `os.Pipe()` fd. - **End-to-end subprocess test**: `TestMain` re-execs the test binary as the real CLI, `TestCreateNonInteractiveE2E` runs it with stdin explicitly `/dev/null` under a 30s timeout and asserts on final rendered stderr — exit 1, every flag token intact, each problem on its own line. - Resolver tests cover aggregation, per-language template validation, and dir-exists ± `--yes`; delete-gating tests assert the API is never called without confirmation. - Interactive flows verified end-to-end under a real PTY (name → language select → template select → scaffold; invalid-value warn-then-reprompt), plus `< /dev/null` smoke tests of every gated path. - Pre-existing lint warnings only (`golangci-lint`). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches many delete/confirm code paths and changes create’s input flow; behavior change is intentional but broad enough that a missed call site could still hang or skip confirmation handling. > > **Overview** > Introduces **`pkg/interactive`** as the only place that runs pterm confirm/select/text prompts, gated on stdin being a TTY. When stdin is not a terminal, prompts return **`PromptError`** with actionable fixes (typically **`--yes`** for confirmations or explicit flags/values for inputs) instead of hanging. > > **Delete and confirm paths** across api-keys, apps, auth connections, credentials, credential providers, deploy, extensions, profiles, and proxies now go through **`Prompter.Confirm`** (or package **`interactive.Confirm`**) and propagate errors instead of ignoring them with `ok, _ :=`. > > **`kernel create`** is reworked around **`create.ResolveInput`**: one resolver builds normalized input plus ordered **`Problem`**s for name, language, template, and overwrite. Interactive mode resolves one problem at a time and re-resolves; non-interactive mode returns every problem in a single error. Adds **`--yes`** for overwrite without prompting and updates long help for fail-fast behavior. > > **`kernel browsers create`** adds **`--yes`** for the pool-conflict confirm and routes viewport selection through **`interactive.Select`** with a non-interactive hint to use **`--viewport`**. > > The **root error handler** renders **`PromptError`** via **`Display()`** without fang’s width wrap/transform so flag tokens stay intact on separate lines. > > Tests cover injected non-TTY prompters, resolver aggregation, and an **E2E subprocess** of **`kernel create`** with **`/dev/null`** stdin. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 954f364. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 25b3cf0 commit 574158c

21 files changed

Lines changed: 1132 additions & 192 deletions

‎cmd/api_keys.go‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/kernel/cli/pkg/interactive"
78
"github.com/kernel/cli/pkg/util"
89
"github.com/kernel/kernel-go-sdk"
910
"github.com/kernel/kernel-go-sdk/option"
@@ -21,7 +22,8 @@ type APIKeysService interface {
2122
}
2223

2324
type APIKeysCmd struct {
24-
apiKeys APIKeysService
25+
apiKeys APIKeysService
26+
prompter interactive.Prompter
2527
}
2628

2729
type APIKeysCreateInput struct {
@@ -181,9 +183,13 @@ func (c APIKeysCmd) Update(ctx context.Context, in APIKeysUpdateInput) error {
181183

182184
func (c APIKeysCmd) Delete(ctx context.Context, in APIKeysDeleteInput) error {
183185
if !in.SkipConfirm {
184-
msg := fmt.Sprintf("Are you sure you want to delete API key '%s'?", in.ID)
185-
pterm.DefaultInteractiveConfirm.DefaultText = msg
186-
ok, _ := pterm.DefaultInteractiveConfirm.Show()
186+
ok, err := c.prompter.Confirm(
187+
fmt.Sprintf("delete API key '%s'", in.ID),
188+
fmt.Sprintf("Are you sure you want to delete API key '%s'?", in.ID),
189+
)
190+
if err != nil {
191+
return err
192+
}
187193
if !ok {
188194
pterm.Info.Println("Deletion cancelled")
189195
return nil

‎cmd/app.go‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"strings"
66

7+
"github.com/kernel/cli/pkg/interactive"
78
"github.com/kernel/cli/pkg/util"
89
"github.com/kernel/kernel-go-sdk"
910
"github.com/pterm/pterm"
@@ -256,9 +257,13 @@ func runAppDelete(cmd *cobra.Command, args []string) error {
256257
if version != "" {
257258
scope = fmt.Sprintf("version '%s'", version)
258259
}
259-
msg := fmt.Sprintf("Delete all deployments for app '%s' (%s)? This cannot be undone.", appName, scope)
260-
pterm.DefaultInteractiveConfirm.DefaultText = msg
261-
ok, _ := pterm.DefaultInteractiveConfirm.Show()
260+
ok, err := interactive.Confirm(
261+
fmt.Sprintf("delete all deployments for app '%s' (%s)", appName, scope),
262+
fmt.Sprintf("Delete all deployments for app '%s' (%s)? This cannot be undone.", appName, scope),
263+
)
264+
if err != nil {
265+
return err
266+
}
262267
if !ok {
263268
pterm.Info.Println("Deletion cancelled")
264269
return nil

‎cmd/auth_connections.go‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"time"
88

9+
"github.com/kernel/cli/pkg/interactive"
910
"github.com/kernel/cli/pkg/util"
1011
"github.com/kernel/kernel-go-sdk"
1112
"github.com/kernel/kernel-go-sdk/option"
@@ -29,7 +30,8 @@ type AuthConnectionService interface {
2930

3031
// AuthConnectionCmd handles auth connection operations independent of cobra.
3132
type AuthConnectionCmd struct {
32-
svc AuthConnectionService
33+
svc AuthConnectionService
34+
prompter interactive.Prompter
3335
}
3436

3537
type AuthConnectionCreateInput struct {
@@ -491,9 +493,13 @@ func (c AuthConnectionCmd) List(ctx context.Context, in AuthConnectionListInput)
491493

492494
func (c AuthConnectionCmd) Delete(ctx context.Context, in AuthConnectionDeleteInput) error {
493495
if !in.SkipConfirm {
494-
msg := fmt.Sprintf("Are you sure you want to delete managed auth '%s'?", in.ID)
495-
pterm.DefaultInteractiveConfirm.DefaultText = msg
496-
ok, _ := pterm.DefaultInteractiveConfirm.Show()
496+
ok, err := c.prompter.Confirm(
497+
fmt.Sprintf("delete managed auth '%s'", in.ID),
498+
fmt.Sprintf("Are you sure you want to delete managed auth '%s'?", in.ID),
499+
)
500+
if err != nil {
501+
return err
502+
}
497503
if !ok {
498504
pterm.Info.Println("Deletion cancelled")
499505
return nil

‎cmd/browsers.go‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"strings"
1818
"time"
1919

20+
"github.com/kernel/cli/pkg/interactive"
2021
"github.com/kernel/cli/pkg/table"
2122
"github.com/kernel/cli/pkg/util"
2223
"github.com/kernel/kernel-go-sdk"
@@ -2713,6 +2714,7 @@ func init() {
27132714
browsersCreateCmd.Flags().String("telemetry", "", "Configure telemetry (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)")
27142715
browsersCreateCmd.Flags().String("name", "", "Optional unique name for the browser session (used to find it later; can be changed with 'browsers update --name')")
27152716
browsersCreateCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the session (repeatable; up to 50 pairs)")
2717+
browsersCreateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompts")
27162718
browsersCreateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object")
27172719
browsersCreateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)")
27182720
browsersCreateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file")
@@ -2803,6 +2805,7 @@ func poolLeaseAllowedFlags() map[string]bool {
28032805
"tag": true,
28042806
"telemetry": true,
28052807
"output": true,
2808+
"yes": true,
28062809
// Global persistent flags that don't configure browsers
28072810
"no-color": true,
28082811
"log-level": true,
@@ -2835,6 +2838,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
28352838
chromePolicy, _ := cmd.Flags().GetString("chrome-policy")
28362839
chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file")
28372840
output, _ := cmd.Flags().GetString("output")
2841+
skipConfirm, _ := cmd.Flags().GetBool("yes")
28382842

28392843
if poolID != "" && poolName != "" {
28402844
pterm.Error.Println("must specify at most one of --pool-id or --pool-name")
@@ -2863,10 +2867,18 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
28632867
pterm.Info.Println("When using a pool, all browser configuration comes from the pool itself.")
28642868
pterm.Info.Println("The conflicting flags will be ignored.")
28652869

2866-
result, _ := pterm.DefaultInteractiveConfirm.Show("Continue with pool configuration?")
2867-
if !result {
2868-
pterm.Info.Println("Cancelled. Remove conflicting flags or omit the pool flag.")
2869-
return nil
2870+
if !skipConfirm {
2871+
result, err := interactive.Confirm(
2872+
fmt.Sprintf("ignore conflicting browser configuration flags (%s) and continue with %s", strings.Join(conflicts, ", "), flagLabel),
2873+
"Continue with pool configuration?",
2874+
)
2875+
if err != nil {
2876+
return err
2877+
}
2878+
if !result {
2879+
pterm.Info.Println("Cancelled. Remove conflicting flags or omit the pool flag.")
2880+
return nil
2881+
}
28702882
}
28712883
pterm.Success.Println("Proceeding with pool configuration...")
28722884
}
@@ -2911,17 +2923,17 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
29112923

29122924
// Handle interactive viewport selection
29132925
if viewportInteractive {
2914-
if viewport != "" {
2926+
if viewport != "" && interactive.IsInteractive() {
29152927
pterm.Warning.Println("Both --viewport and --viewport-interactive specified; using interactive mode")
29162928
}
2917-
options := getAvailableViewports()
2918-
selectedViewport, err := pterm.DefaultInteractiveSelect.
2919-
WithOptions(options).
2920-
WithDefaultText("Select a viewport size:").
2921-
Show()
2929+
selectedViewport, err := interactive.Select(
2930+
"viewport selection",
2931+
"pass --viewport instead of --viewport-interactive (e.g. --viewport 1920x1080@25)",
2932+
"Select a viewport size:",
2933+
getAvailableViewports(),
2934+
)
29222935
if err != nil {
2923-
pterm.Error.Printf("Failed to select viewport: %v\n", err)
2924-
return nil
2936+
return err
29252937
}
29262938
viewport = selectedViewport
29272939
}

‎cmd/create.go‎

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import (
99
"strings"
1010

1111
"github.com/kernel/cli/pkg/create"
12+
"github.com/kernel/cli/pkg/interactive"
1213
"github.com/pterm/pterm"
1314
"github.com/spf13/cobra"
1415
)
1516

1617
// CreateCmd is a cobra-independent command handler for create operations
17-
type CreateCmd struct{}
18+
type CreateCmd struct {
19+
prompter interactive.Prompter
20+
}
1821

1922
// Create executes the creating a new Kernel app logic
2023
func (c CreateCmd) Create(ctx context.Context, ci create.CreateInput) error {
@@ -23,16 +26,20 @@ func (c CreateCmd) Create(ctx context.Context, ci create.CreateInput) error {
2326
return fmt.Errorf("failed to resolve app path: %w", err)
2427
}
2528

26-
// Check if directory already exists and prompt for overwrite
29+
// Check if directory already exists and prompt for overwrite. This is a
30+
// backstop for direct callers; runCreateApp resolves the overwrite
31+
// confirmation before calling Create.
2732
if _, err := os.Stat(appPath); err == nil {
28-
overwrite, err := create.PromptForOverwrite(ci.Name)
29-
if err != nil {
30-
return fmt.Errorf("failed to prompt for overwrite: %w", err)
31-
}
32-
33-
if !overwrite {
34-
pterm.Warning.Println("Operation cancelled.")
35-
return nil
33+
if !ci.SkipConfirm {
34+
overwrite, err := create.PromptOverwrite(c.prompter, ci.Name)
35+
if err != nil {
36+
return err
37+
}
38+
39+
if !overwrite {
40+
pterm.Warning.Println("Operation cancelled.")
41+
return nil
42+
}
3643
}
3744

3845
// Remove existing directory
@@ -81,6 +88,7 @@ func init() {
8188
createCmd.Flags().StringP("name", "n", "", "Name of the application")
8289
createCmd.Flags().StringP("language", "l", "", fmt.Sprintf("Language of the application (%s)", strings.Join(supportedLanguageDisplay(), ", ")))
8390
createCmd.Flags().StringP("template", "t", "", "Template to use for the application (see 'kernel create --help' for the full list)")
91+
createCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompts (overwrite an existing directory without asking)")
8492
}
8593

8694
// supportedLanguageDisplay returns each supported language with its shorthand,
@@ -104,7 +112,9 @@ func buildCreateLongHelp() string {
104112
var b strings.Builder
105113
b.WriteString("Commands for creating new Kernel applications.\n\n")
106114
b.WriteString("Pass --name, --language and --template to scaffold non-interactively;\n")
107-
b.WriteString("any omitted flag falls back to an interactive prompt.\n\n")
115+
b.WriteString("any omitted flag falls back to an interactive prompt. In a\n")
116+
b.WriteString("non-interactive shell the command fails fast instead of prompting.\n")
117+
b.WriteString("Pass --yes to overwrite an existing directory without confirmation.\n\n")
108118

109119
b.WriteString("Languages:\n")
110120
for _, l := range create.SupportedLanguages {
@@ -140,29 +150,47 @@ func buildCreateLongHelp() string {
140150
}
141151

142152
func runCreateApp(cmd *cobra.Command, args []string) error {
143-
appName, _ := cmd.Flags().GetString("name")
144-
language, _ := cmd.Flags().GetString("language")
145-
template, _ := cmd.Flags().GetString("template")
146-
147-
appName, err := create.PromptForAppName(appName)
148-
if err != nil {
149-
return fmt.Errorf("failed to get app name: %w", err)
150-
}
153+
return createApp(cmd, interactive.NewPrompter())
154+
}
151155

152-
language, err = create.PromptForLanguage(language)
153-
if err != nil {
154-
return fmt.Errorf("failed to get language: %w", err)
155-
}
156+
// createApp resolves the create inputs and scaffolds the app. The prompter
157+
// carries the terminal capability, so tests can drive the non-interactive
158+
// path deterministically without mutating any package state.
159+
func createApp(cmd *cobra.Command, prompter interactive.Prompter) error {
160+
raw := create.RawInput{}
161+
raw.Name, _ = cmd.Flags().GetString("name")
162+
raw.Language, _ = cmd.Flags().GetString("language")
163+
raw.Template, _ = cmd.Flags().GetString("template")
164+
raw.SkipOverwriteConfirm, _ = cmd.Flags().GetBool("yes")
165+
166+
c := CreateCmd{prompter: prompter}
167+
168+
// create.ResolveInput is the single resolver from raw flags to a
169+
// normalized CreateInput for both modes. Interactively, each remaining
170+
// problem carries its own resolution step: prompt for that field, apply
171+
// the answer, re-resolve (so e.g. the template list reflects the chosen
172+
// language). Non-interactively, every problem is reported at once in a
173+
// single fail-fast error.
174+
for {
175+
in, problems := create.ResolveInput(raw)
176+
if len(problems) == 0 {
177+
return c.Create(cmd.Context(), in)
178+
}
179+
if !prompter.CanPrompt() {
180+
return interactive.ErrInputsRequired(create.ProblemMessages(problems))
181+
}
156182

157-
template, err = create.PromptForTemplate(template, language)
158-
if err != nil {
159-
return fmt.Errorf("failed to get template: %w", err)
183+
p := problems[0]
184+
if p.Invalid {
185+
pterm.Warning.Println(p.Message)
186+
}
187+
cancelled, err := p.Resolve(prompter, &raw)
188+
if err != nil {
189+
return err
190+
}
191+
if cancelled {
192+
pterm.Warning.Println("Operation cancelled.")
193+
return nil
194+
}
160195
}
161-
162-
c := CreateCmd{}
163-
return c.Create(cmd.Context(), create.CreateInput{
164-
Name: appName,
165-
Language: language,
166-
Template: template,
167-
})
168196
}

‎cmd/credential_providers.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strings"
77

8+
"github.com/kernel/cli/pkg/interactive"
89
"github.com/kernel/cli/pkg/util"
910
"github.com/kernel/kernel-go-sdk"
1011
"github.com/kernel/kernel-go-sdk/option"
@@ -27,6 +28,7 @@ type CredentialProvidersService interface {
2728
// CredentialProvidersCmd handles credential provider operations independent of cobra.
2829
type CredentialProvidersCmd struct {
2930
providers CredentialProvidersService
31+
prompter interactive.Prompter
3032
}
3133

3234
type CredentialProvidersListInput struct {
@@ -254,9 +256,13 @@ func (c CredentialProvidersCmd) Update(ctx context.Context, in CredentialProvide
254256

255257
func (c CredentialProvidersCmd) Delete(ctx context.Context, in CredentialProvidersDeleteInput) error {
256258
if !in.SkipConfirm {
257-
msg := fmt.Sprintf("Are you sure you want to delete credential provider '%s'?", in.ID)
258-
pterm.DefaultInteractiveConfirm.DefaultText = msg
259-
ok, _ := pterm.DefaultInteractiveConfirm.Show()
259+
ok, err := c.prompter.Confirm(
260+
fmt.Sprintf("delete credential provider '%s'", in.ID),
261+
fmt.Sprintf("Are you sure you want to delete credential provider '%s'?", in.ID),
262+
)
263+
if err != nil {
264+
return err
265+
}
260266
if !ok {
261267
pterm.Info.Println("Deletion cancelled")
262268
return nil

‎cmd/credentials.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strings"
77

8+
"github.com/kernel/cli/pkg/interactive"
89
"github.com/kernel/cli/pkg/util"
910
"github.com/kernel/kernel-go-sdk"
1011
"github.com/kernel/kernel-go-sdk/option"
@@ -26,6 +27,7 @@ type CredentialsService interface {
2627
// CredentialsCmd handles credential operations independent of cobra.
2728
type CredentialsCmd struct {
2829
credentials CredentialsService
30+
prompter interactive.Prompter
2931
}
3032

3133
type CredentialsListInput struct {
@@ -281,9 +283,13 @@ func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) e
281283

282284
func (c CredentialsCmd) Delete(ctx context.Context, in CredentialsDeleteInput) error {
283285
if !in.SkipConfirm {
284-
msg := fmt.Sprintf("Are you sure you want to delete credential '%s'?", in.Identifier)
285-
pterm.DefaultInteractiveConfirm.DefaultText = msg
286-
ok, _ := pterm.DefaultInteractiveConfirm.Show()
286+
ok, err := c.prompter.Confirm(
287+
fmt.Sprintf("delete credential '%s'", in.Identifier),
288+
fmt.Sprintf("Are you sure you want to delete credential '%s'?", in.Identifier),
289+
)
290+
if err != nil {
291+
return err
292+
}
287293
if !ok {
288294
pterm.Info.Println("Deletion cancelled")
289295
return nil

0 commit comments

Comments
 (0)