Repository navigation
Commit 574158c
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
File tree
- cmd
- proxies
- pkg
- create
- interactive
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
24 | | - | |
| 25 | + | |
| 26 | + | |
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
| |||
181 | 183 | | |
182 | 184 | | |
183 | 185 | | |
184 | | - | |
185 | | - | |
186 | | - | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
187 | 193 | | |
188 | 194 | | |
189 | 195 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
256 | 257 | | |
257 | 258 | | |
258 | 259 | | |
259 | | - | |
260 | | - | |
261 | | - | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
262 | 267 | | |
263 | 268 | | |
264 | 269 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
32 | | - | |
| 33 | + | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| |||
491 | 493 | | |
492 | 494 | | |
493 | 495 | | |
494 | | - | |
495 | | - | |
496 | | - | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
497 | 503 | | |
498 | 504 | | |
499 | 505 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
2713 | 2714 | | |
2714 | 2715 | | |
2715 | 2716 | | |
| 2717 | + | |
2716 | 2718 | | |
2717 | 2719 | | |
2718 | 2720 | | |
| |||
2803 | 2805 | | |
2804 | 2806 | | |
2805 | 2807 | | |
| 2808 | + | |
2806 | 2809 | | |
2807 | 2810 | | |
2808 | 2811 | | |
| |||
2835 | 2838 | | |
2836 | 2839 | | |
2837 | 2840 | | |
| 2841 | + | |
2838 | 2842 | | |
2839 | 2843 | | |
2840 | 2844 | | |
| |||
2863 | 2867 | | |
2864 | 2868 | | |
2865 | 2869 | | |
2866 | | - | |
2867 | | - | |
2868 | | - | |
2869 | | - | |
| 2870 | + | |
| 2871 | + | |
| 2872 | + | |
| 2873 | + | |
| 2874 | + | |
| 2875 | + | |
| 2876 | + | |
| 2877 | + | |
| 2878 | + | |
| 2879 | + | |
| 2880 | + | |
| 2881 | + | |
2870 | 2882 | | |
2871 | 2883 | | |
2872 | 2884 | | |
| |||
2911 | 2923 | | |
2912 | 2924 | | |
2913 | 2925 | | |
2914 | | - | |
| 2926 | + | |
2915 | 2927 | | |
2916 | 2928 | | |
2917 | | - | |
2918 | | - | |
2919 | | - | |
2920 | | - | |
2921 | | - | |
| 2929 | + | |
| 2930 | + | |
| 2931 | + | |
| 2932 | + | |
| 2933 | + | |
| 2934 | + | |
2922 | 2935 | | |
2923 | | - | |
2924 | | - | |
| 2936 | + | |
2925 | 2937 | | |
2926 | 2938 | | |
2927 | 2939 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
17 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
18 | 21 | | |
19 | 22 | | |
20 | 23 | | |
| |||
23 | 26 | | |
24 | 27 | | |
25 | 28 | | |
26 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
27 | 32 | | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
36 | 43 | | |
37 | 44 | | |
38 | 45 | | |
| |||
81 | 88 | | |
82 | 89 | | |
83 | 90 | | |
| 91 | + | |
84 | 92 | | |
85 | 93 | | |
86 | 94 | | |
| |||
104 | 112 | | |
105 | 113 | | |
106 | 114 | | |
107 | | - | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
108 | 118 | | |
109 | 119 | | |
110 | 120 | | |
| |||
140 | 150 | | |
141 | 151 | | |
142 | 152 | | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
| 153 | + | |
| 154 | + | |
151 | 155 | | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
156 | 182 | | |
157 | | - | |
158 | | - | |
159 | | - | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
160 | 195 | | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | 196 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| 31 | + | |
30 | 32 | | |
31 | 33 | | |
32 | 34 | | |
| |||
254 | 256 | | |
255 | 257 | | |
256 | 258 | | |
257 | | - | |
258 | | - | |
259 | | - | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
260 | 266 | | |
261 | 267 | | |
262 | 268 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| 30 | + | |
29 | 31 | | |
30 | 32 | | |
31 | 33 | | |
| |||
281 | 283 | | |
282 | 284 | | |
283 | 285 | | |
284 | | - | |
285 | | - | |
286 | | - | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
287 | 293 | | |
288 | 294 | | |
289 | 295 | | |
| |||
0 commit comments