feat(cli): add headless mode for non-TTY environments#143
Open
SupremaLex wants to merge 1 commit into
Open
Conversation
`scell -d` previously failed with `No such device or address (os error 6)`
when stdout was not a TTY (CI, scripts, agents, `nohup`). Root cause:
`run::run` unconditionally called `Terminal::new()`, which enables
crossterm raw mode via `tcsetattr` on stdout — `ENXIO` on non-TTY fds.
The detach flag was checked only later, so `-d` never had a chance.
The same `Terminal::new()` was called from `ls`, `stop`, and `cleanup`,
so all four TUI subcommands were unusable headless. The TUI `preparing`
renderer also panicked in `chunks(0)` when render area was ≤5 cols.
This change:
- Adds `--headless` flag plus auto-detect via `std::io::IsTerminal` on
stdout. Non-TTY stdout implies `--headless`; `--headless` implies
`--detach` for `run` (interactive shell needs a TTY).
- `run::run` now skips `Terminal::new()` when detached and dispatches
to a new `App::run_headless()` that reuses the existing
`PreparingState` channels and streams build logs to stderr.
- `PreparingState::prepare` propagates `UserError` through `tx` in
detach mode instead of hanging on `future::pending()` — TUI behaviour
preserved when interactive.
- New `headless` modules under `src/cli/{ls,stop,cleanup}` print
plain-text output (table for `ls`, line-per-action for `stop`/
`cleanup`). All share the same `BuildKitD` business logic as the TUI
variants — only presentation differs.
- Clamps `chunks(area_width.saturating_sub(5).max(1))` in the
preparing UI to avoid the latent `size != 0` panic on tiny terminals.
`init` already prints plain text — no change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
scell -d(andscell ls/stop/cleanup) crash withNo such device or address (os error 6)when stdout is not a TTY:This makes shell-cell unusable from CI, scripts, agents (e.g. Claude Code),
nohup, container init systems — any environment without an interactive terminal. The--detachflag specifically advertises a non-interactive use case, yet was the most prominently broken path.Why
-ddidn't worksrc/cli/run/mod.rscallsTerminal::new()before the detach branch:Terminal::new()enables crossterm raw mode (tcsetattr+ioctlon stdout fd). The kernel returnsENXIOwhen the fd isn't a real TTY, soTerminal::new()aborts before any detach logic runs.The same pattern exists in
ls/mod.rs,stop/mod.rs, andcleanup/mod.rs— all unconditionally initialise the TUI even though their work (listing/stopping/removing containers) is naturally plain text. So 4 of 5 subcommands were headless-unusable; onlyinitworked (it never touchedTerminal).A latent panic in
src/cli/run/app/preparing/ui.rs:45compounded the problem underscript-wrapped PTYs:When the render area is ≤5 cols (or 0, which can happen when terminal width can't be detected), the app panics with
assertion failed: size != 0.Why it works now
std::io::IsTerminalon stdout inCli::exec_inner. Non-TTY stdout implicitly enables--headless;--headlessimplicitly enables--detachforrun(an interactive shell session still needs a TTY).--headlessflag for forcing headless mode even on a TTY (useful for scripted invocations).run::runskipsTerminal::new()when detached, dispatching to a newApp::run_headless()that reuses the existingPreparingState— same tokio task, same channels — but drains build logs to stderr instead of rendering them. Business logic is shared with the TUI path; only the sink differs.PreparingState::preparepropagatesUserErrorthroughtxin detach mode instead of hanging onfuture::pending()forever. TUI behaviour preserved when interactive.ls/stop/cleanupgainheadless.rssiblings that call the sameBuildKitD::{list,stop,cleanup}_*methods used by the TUI state machines and print plain text:ls: tab-aligned table (NAME STATUS TARGET LOCATION FLAGS).stop:stopped <name>per success /error: failed to stop <name>: <err>per failure /stopped N, failed Msummary, non-zero exit on any failure.cleanup:removed container/image <name>per item / same error+summary format / non-zero exit on any failure.chunks(area_width.saturating_sub(5).max(1))clamps the preparing UI splitter so the latentsize != 0panic can't fire on tiny terminals.Routing summary:
runbehaviour-d--headlessinitlsstopcleanuprunVerified
cargo build --release— cleancargo clippy --all-targets --release— clean (strict pedantic config kept)cargo test -- --test-threads 1— 107 / 107 passed🤖 Generated with Claude Code