use crate::extensions::ssh_remote::{
state::MAX_TIMEOUT_SECONDS,
transport::{self, RemoteConn},
};
use std::{sync::Arc, time::Duration};
pub struct ExecRequest {
pub command: String,
pub cwd: Option<String>,
pub timeout_secs: Option<u32>,
pub tail_lines: Option<usize>,
}
pub async fn run(conn: &Arc<RemoteConn>, req: &ExecRequest) -> Result<String, String> {
let limits = transport::limits();
let command = match req.tail_lines {
Some(n) if n > 0 => format!("{} 2>&1 | tail -n {n}", req.command),
_ => req.command.clone(),
};
let timeout = req
.timeout_secs
.map(|s| Duration::from_secs(s.clamp(1, MAX_TIMEOUT_SECONDS) as u64));
let wanted = limits.exec_max_bytes.min(limits.turn_max_bytes);
let granted = transport::take_turn_budget(wanted);
if granted == 0 {
return Ok(format!(
"Output budget for this turn is exhausted; `{}` was not run. Narrow the next command \
(grep/head) or start a new turn.",
req.command
));
}
let out = transport::exec(conn, &command, req.cwd.as_deref(), timeout, granted).await?;
let mut body = out.combined();
let total_lines = body.lines().count();
let mut notes: Vec<String> = Vec::new();
if total_lines > limits.exec_max_lines {
let skip = total_lines - limits.exec_max_lines;
body = body.lines().skip(skip).collect::<Vec<_>>().join("\n");
notes.push(format!(
"kept the last {} of {total_lines} lines",
limits.exec_max_lines
));
}
if out.truncated {
notes.push(format!(
"output hit the {} byte cap (the command was still running when it was cut)",
granted
));
}
if body.trim().is_empty() {
body = "(no output)".to_string();
}
let exit = match out.exit_code {
Some(code) => format!("exit {code}"),
None => "exit unknown (the command was cut off)".to_string(),
};
let mut header = format!(
"{} · {}",
conn.endpoint().display(),
req.cwd.clone().unwrap_or_else(|| conn.endpoint().cwd)
);
if !notes.is_empty() {
header.push_str(" · ");
header.push_str(¬es.join("; "));
}
Ok(format!(
"{header}\n$ {}\n{}\n({exit})",
req.command,
body.trim_end()
))
}
#[cfg(test)]
mod tests {
#[test]
fn timeout_is_clamped() {
let clamp = |s: u32| s.clamp(1, crate::extensions::ssh_remote::state::MAX_TIMEOUT_SECONDS);
assert_eq!(clamp(0), 1);
assert_eq!(
clamp(u32::MAX),
crate::extensions::ssh_remote::state::MAX_TIMEOUT_SECONDS
);
}
}