From a81a98a1e54735b57f9063895953369d2731fcc5 Mon Sep 17 00:00:00 2001 From: Paul Thurlow Date: Tue, 12 May 2026 16:03:35 -0700 Subject: [PATCH] add no-input flag and tty checks for interactive commands --- src/auth.rs | 3 +++ src/connections_new.rs | 9 +++++++++ src/main.rs | 7 +++++++ src/util.rs | 21 +++++++++++++++++++++ src/workspace.rs | 8 ++++++++ 5 files changed, 48 insertions(+) diff --git a/src/auth.rs b/src/auth.rs index b688b73..e15ac16 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -261,6 +261,9 @@ pub fn login() { // Check if already authenticated if is_already_signed_in(&profile_config) { println!("{}", "You are already signed in.".green()); + if !crate::util::is_interactive() { + return; + } print!("Do you want to log in again? [y/N] "); use std::io::Write; std::io::stdout().flush().unwrap(); diff --git a/src/connections_new.rs b/src/connections_new.rs index 4a231df..9f374a9 100644 --- a/src/connections_new.rs +++ b/src/connections_new.rs @@ -260,6 +260,15 @@ fn walk_auth(schema: &Value) -> Map { // ── Entry point ─────────────────────────────────────────────────────────────── pub fn run(workspace_id: &str) { + if !crate::util::is_interactive() { + eprintln!( + "error: 'connections new' is interactive and stdin is not a TTY. \ + Use 'hotdata connections create list' to discover types and their config schemas, \ + then 'hotdata connections create --name --type --config '{{…}}''." + ); + std::process::exit(1); + } + let api = ApiClient::new(Some(workspace_id)); // Phase 1: Select connection type diff --git a/src/main.rs b/src/main.rs index 0c6e172..3b2b72d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,10 @@ struct Cli { #[arg(long, global = true, hide = true)] debug: bool, + /// Disable interactive prompts; commands that need input will error instead + #[arg(long = "no-input", global = true)] + no_input: bool, + #[command(subcommand)] command: Option, } @@ -134,6 +138,9 @@ fn main() { if cli.debug { util::set_debug(true); } + if cli.no_input { + util::set_no_input(true); + } let skip_skill_auto_update = cli.command.is_none() || matches!(&cli.command, Some(Commands::Skills { .. })); diff --git a/src/util.rs b/src/util.rs index 39a7462..a5ee4cb 100644 --- a/src/util.rs +++ b/src/util.rs @@ -13,6 +13,27 @@ pub fn spinner(msg: &str) -> indicatif::ProgressBar { pb } +static NO_INPUT: AtomicBool = AtomicBool::new(false); + +pub fn set_no_input(enabled: bool) { + NO_INPUT.store(enabled, Ordering::Relaxed); +} + +/// Returns true if interactive prompts are usable. Returns false when: +/// - the global `--no-input` flag was passed, +/// - the `CI` env var is set (most CI runners set this), +/// - stdin is not a TTY (piped, redirected, or invoked by an agent harness). +pub fn is_interactive() -> bool { + if NO_INPUT.load(Ordering::Relaxed) { + return false; + } + if std::env::var_os("CI").is_some() { + return false; + } + use std::io::IsTerminal; + std::io::stdin().is_terminal() +} + static DEBUG: AtomicBool = AtomicBool::new(false); pub fn set_debug(enabled: bool) { diff --git a/src/workspace.rs b/src/workspace.rs index 3783475..2dac178 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -43,6 +43,14 @@ pub fn set(workspace_id: Option<&str>) { eprintln!("error: no workspaces available."); std::process::exit(1); } + if !crate::util::is_interactive() { + eprintln!( + "error: stdin is not a TTY; cannot prompt for selection. \ + Run 'hotdata workspaces list' to see available IDs, \ + then 'hotdata workspaces set '." + ); + std::process::exit(1); + } let options: Vec = workspaces .iter() .map(|w| format!("{} ({})", w.name, w.public_id))