diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 9a1c5d980..2617d1068 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -104,7 +104,15 @@ impl Context { } #[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None, name = "surfpool", bin_name = "surfpool")] +#[clap( + author, + version, + about, + long_about = "Where you train before surfing Solana\n\nAutomation mode:\nSet NO_DNA=1 to disable interactive prompts and + TUI-oriented UX for agent/CI execution.", + name = "surfpool", + bin_name = "surfpool" +)] struct Opts { #[clap(subcommand)] command: Command, @@ -585,6 +593,10 @@ pub async fn handle_mcp_command(_ctx: &Context) -> Result<(), String> { fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> { match opts.command { Command::Simnet(mut cmd) => { + if crate::no_dna::is_no_dna() { + cmd.no_tui = true; + } + if cmd.ci { cmd.disable_instruction_profiling = true; cmd.no_studio = true; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 27ce6ad03..0cf5a72a0 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,5 +1,6 @@ #[macro_use] mod macros; +mod no_dna; extern crate hiro_system_kit; diff --git a/crates/cli/src/no_dna.rs b/crates/cli/src/no_dna.rs new file mode 100644 index 000000000..f7db92384 --- /dev/null +++ b/crates/cli/src/no_dna.rs @@ -0,0 +1,42 @@ +use std::env; + +/// Returns true when NO_DNA requests non-interactive agent mode. +/// Truthy values (case-insensitive) +pub fn is_no_dna() -> bool { + parse_truthy(env::var("NO_DNA").ok().as_deref()) +} + +fn parse_truthy(value: Option<&str>) -> bool { + match value { + Some(v) => { + let v = v.trim().to_ascii_lowercase(); + matches!(v.as_str(), "1" | "true" | "yes" | "on") + } + None => false, + } +} + +#[cfg(test)] +mod tests { + use super::parse_truthy; + + #[test] + fn parse_truthy_values() { + assert!(parse_truthy(Some("1"))); + assert!(parse_truthy(Some("true"))); + assert!(parse_truthy(Some("TRUE"))); + assert!(parse_truthy(Some("yes"))); + assert!(parse_truthy(Some("on"))); + assert!(parse_truthy(Some(" On "))); + } + + #[test] + fn parse_falsey_values() { + assert!(!parse_truthy(None)); + assert!(!parse_truthy(Some("0"))); + assert!(!parse_truthy(Some("fals e"))); + assert!(!parse_truthy(Some("no"))); + assert!(!parse_truthy(Some("off"))); + assert!(!parse_truthy(Some(""))); + } +} diff --git a/crates/cli/src/runbook/mod.rs b/crates/cli/src/runbook/mod.rs index 05ff9cf94..81dba263f 100644 --- a/crates/cli/src/runbook/mod.rs +++ b/crates/cli/src/runbook/mod.rs @@ -309,10 +309,20 @@ pub async fn execute_runbook( ..ColorfulTheme::default() }; - let confirm = Confirm::with_theme(&theme) - .with_prompt("Do you want to continue?") - .interact() - .unwrap(); + let confirm = if crate::no_dna::is_no_dna() { + // non-interactive mode should not block on prompt + // safest behavior: require explicit unsupervised/yes-style intent if available + if cmd.unsupervised { + true + } else { + return Err("NO_DNA=1 disables interactive confirmation. Re-run with non-interactive intent (e.g. --unsupervised or equivalent explicit approval flag).".to_string()); + } + } else { + Confirm::with_theme(&theme) + .with_prompt("Do you want to continue?") + .interact() + .map_err(|e| format!("failed to read confirmation input: {e}"))? + }; if !confirm { return Ok(()); diff --git a/crates/cli/src/scaffold/mod.rs b/crates/cli/src/scaffold/mod.rs index 33dffe4e2..08dd5123b 100644 --- a/crates/cli/src/scaffold/mod.rs +++ b/crates/cli/src/scaffold/mod.rs @@ -223,21 +223,25 @@ pub fn scaffold_iac_layout( let selected_programs = match auto_generate_runbooks { true => programs, false => { - let selection = MultiSelect::with_theme(&theme) - .with_prompt("Select the programs to deploy (all by default):") - .items_checked( - &programs - .iter() - .map(|p| (p.name.as_str(), true)) - .collect::>(), - ) - .interact() - .map_err(|e| format!("unable to select programs to deploy: {e}"))?; - - &selection - .iter() - .map(|i| programs[*i].clone()) - .collect::>() + if crate::no_dna::is_no_dna() { + programs + } else { + let selection = MultiSelect::with_theme(&theme) + .with_prompt("Select the programs to deploy (all by default):") + .items_checked( + &programs + .iter() + .map(|p| (p.name.as_str(), true)) + .collect::>(), + ) + .interact() + .map_err(|e| format!("unable to select programs to deploy: {e}"))?; + + &selection + .iter() + .map(|i| programs[*i].clone()) + .collect::>() + } } };