Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/cmd/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,12 @@ pub(crate) fn run_menu(parsed: &ParsedArgs, env: &BTreeMap<String, String>) -> R
}

if !term::is_interactive() {
let (_, actions) = launcher_hud(&path, parsed, env, &mut CreditsCache::fresh());
let (status, actions) = launcher_hud(&path, parsed, env, &mut CreditsCache::fresh());
// Without a TTY the menu can't be driven, but the status line is the
// whole point of the "preflight" launcher: account · model · agent ·
// credits. Print it first so a piped `anyr menu` still reports state,
// then list the actions it would offer.
println!("{status}");
println!(
"{}",
actions
Expand Down
45 changes: 45 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2359,3 +2359,48 @@ fn yes_equals_true_skips_confirm_parse() {
"{stdout}"
);
}

#[test]
fn menu_non_tty_prints_status_before_actions() {
// WHY: a piped `anyr menu` can't be driven, but the status line is the
// preflight payoff — account · model · agent · credits. It must lead so
// scripts/CI see state, not just a bare action list.
let dir = temp_home();
std::fs::write(
dir.join("config.yaml"),
"\
active_profile: default
profiles:
default:
api_key: sk-ar-v1-menu-status-secret-abcdef
default_model: auto
",
)
.unwrap();
let out = anyr()
.arg("menu")
.env("ANYROUTER_HOME", &dir)
.output()
.expect("menu non-tty");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(out.status.code().unwrap_or(1), 0, "{stdout}{stderr}");
// First line is the status, not an action: it names the binary and the
// signed-in dot, and never leaks the full secret.
let first = stdout.lines().next().unwrap_or("");
assert!(first.contains("anyr"), "status must name anyr: {first}");
assert!(
first.contains("auto") || first.contains("claude"),
"status must show model/agent: {first}"
);
assert!(
!stdout.contains("menu-status-secret"),
"leaked secret: {stdout}"
);
// Actions follow the status line.
assert!(
stdout.contains("Config") && stdout.contains("Quit"),
"{stdout}"
);
let _ = std::fs::remove_dir_all(&dir);
}
Loading