fix(cli): call it a program, and say that stdin does not reach it

`tty7 run -- /no/such/binary` answered "no such shell on this machine".
The check in front of a spawn serves both the configured shell and a
command handed to `run`, and its three sentences all said "shell" —
so someone who typed `tty7 run -- ./build.sh` was told tty7 had
misunderstood what they asked for. They say "program" now, which is
true of both callers; the CLI already prefixes "spawning `…`" with the
context.

The other half is worse and is documented rather than changed:
`echo hi | tty7 run -- cat` never returns. The command reads the pane's
terminal, which nothing is typing into, so it waits for input that
cannot arrive — and there is no error, because an idle terminal is not
a failure. Output streams back; input does not go the other way.

Measured, after a first attempt sat for ten minutes: a bounded re-run
with `head -1` was still running at ten seconds with the pipe unread.
For a CLI whose stated audience is agents, `cmd | tty7 run -- …` is a
natural thing to try and an unbounded wait is the worst way to answer
it. Forwarding stdin would be a new capability; saying so is not, and
`sh -c 'cat < input.txt'` gets the input there today.
This commit is contained in:
l0ng-ai
2026-08-16 09:06:57 +08:00
parent 2e2cdfa242
commit 9833c772b0
2 changed files with 22 additions and 9 deletions
+8 -1
View File
@@ -67,7 +67,14 @@ pub enum Command {
The environment comes from the server too, for the same reason, so \
`FOO=bar tty7 run -- ...` does not reach the command. Set it where \
the command can see it:\n\n \
tty7 run -- sh -c 'FOO=bar cargo test'"
tty7 run -- sh -c 'FOO=bar cargo test'\n\n\
Nor is your stdin forwarded: the command reads from the pane's \
terminal, which nothing is typing into. Output is streamed back, \
input does not go the other way, so `echo hi | tty7 run -- cat` \
waits for input that cannot arrive — with no error, because as far \
as the pane is concerned a terminal is simply idle. Give the \
command its input directly:\n\n \
tty7 run -- sh -c 'cat < input.txt'"
)]
Run(RunArgs),
+14 -8
View File
@@ -122,10 +122,16 @@ struct SpawnConfig {
shell: Option<ShellSpec>,
}
/// Why a configured shell cannot be run, in one sentence, before anything
/// tries. Without it a missing program arrives at the window wrapped four
/// deep — "daemon refused Spawn: spawn failed: Unable to spawn … (ENOENT: No
/// such file or directory)" — and the one fact that matters is buried in it.
/// Why a program cannot be run, in one sentence, before anything tries.
/// Without it a missing program arrives at the window wrapped four deep —
/// "daemon refused Spawn: spawn failed: Unable to spawn … (ENOENT: No such
/// file or directory)" — and the one fact that matters is buried in it.
///
/// "Program", not "shell": the same check stands in front of the configured
/// shell and of a command handed to `tty7 run`, and calling the latter a shell
/// tells someone who typed `tty7 run -- ./build.sh` that tty7 misunderstood
/// what they asked for. The caller supplies the context — the CLI prefixes
/// "spawning `…`" — so the sentence only has to carry the fact.
///
/// Only a program given as a path can be checked here; a bare name is resolved
/// through PATH by the OS, and guessing at that would be worse than silence.
@@ -135,16 +141,16 @@ fn shell_program_problem(program: &str) -> Option<String> {
return None;
}
let Ok(meta) = std::fs::metadata(path) else {
return Some(format!("no such shell on this machine: {program}"));
return Some(format!("no such program on this machine: {program}"));
};
if meta.is_dir() {
return Some(format!("the configured shell is a directory: {program}"));
return Some(format!("that program is a directory: {program}"));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if meta.permissions().mode() & 0o111 == 0 {
return Some(format!("the configured shell is not executable: {program}"));
return Some(format!("that program is not executable: {program}"));
}
}
None
@@ -3191,7 +3197,7 @@ mod tests {
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("not-a-shell");
let problem = shell_program_problem(&missing.to_string_lossy()).expect("a problem");
assert!(problem.contains("no such shell"), "{problem}");
assert!(problem.contains("no such program"), "{problem}");
assert!(problem.contains("not-a-shell"), "{problem}");
// One sentence: none of the layers this used to arrive wrapped in.
assert!(!problem.contains("ENOENT"), "{problem}");