pub(crate) mod detached;
pub(crate) mod launcher;
mod prepared;
mod types;
pub(crate) use prepared::PreparedAgentLaunch;
pub(crate) use prepared::insert_after_host;
pub(crate) use types::RunOverrides;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::agents::CodingAgent;
pub(crate) fn shell_quote_arg_for_platform(raw: &str, windows: bool) -> String {
if windows {
return cmd_quote_arg(raw);
}
posix_quote_arg(raw)
}
fn posix_quote_arg(raw: &str) -> String {
if raw.is_empty() {
"''".into()
} else if raw
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | ':' | '.' | '_' | '-'))
{
raw.to_string()
} else {
format!("'{}'", raw.replace('\'', "'\\''"))
}
}
fn cmd_quote_arg(raw: &str) -> String {
if raw.is_empty() {
return "\"\"".into();
}
if raw.chars().all(|ch| {
ch.is_ascii_alphanumeric()
|| matches!(ch, '/' | '\\' | ':' | '.' | '_' | '-' | '=' | '@' | '+')
}) {
return raw.to_string();
}
let mut escaped = String::new();
for ch in raw.chars() {
match ch {
'%' => escaped.push_str("%%cd:~,%"),
'"' => escaped.push_str("\"\""),
_ => escaped.push(ch),
}
}
format!("\"{escaped}\"")
}
#[cfg(windows)]
pub(crate) fn portable_executable_path(path: PathBuf) -> PathBuf {
use std::ffi::OsString;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
let encoded = path.as_os_str().encode_wide().collect::<Vec<_>>();
strip_windows_verbatim_prefix(&encoded)
.map(|value| OsString::from_wide(&value))
.map(PathBuf::from)
.unwrap_or(path)
}
#[cfg(not(windows))]
pub(crate) fn portable_executable_path(path: PathBuf) -> PathBuf {
path
}
#[cfg(any(test, windows))]
pub(crate) fn strip_windows_verbatim_prefix(encoded: &[u16]) -> Option<Vec<u16>> {
const PREFIX: &[u16] = &[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16];
const UNC: &[u16] = &[
b'\\' as u16,
b'\\' as u16,
b'?' as u16,
b'\\' as u16,
b'U' as u16,
b'N' as u16,
b'C' as u16,
b'\\' as u16,
];
if let Some(rest) = encoded.strip_prefix(UNC) {
let mut normalized = vec![b'\\' as u16, b'\\' as u16];
normalized.extend_from_slice(rest);
Some(normalized)
} else {
encoded.strip_prefix(PREFIX).map(ToOwned::to_owned)
}
}
pub(crate) fn command_argv(command: &str) -> Vec<String> {
command.split_whitespace().map(ToOwned::to_owned).collect()
}
pub(crate) fn version_probe_argv(agent: CodingAgent, argv: &[String]) -> Vec<String> {
let mut probe = argv
.iter()
.rposition(|argument| CodingAgent::infer(argument) == Some(agent))
.map_or_else(|| argv.to_vec(), |index| argv[..=index].to_vec());
if probe.is_empty() {
probe.push(agent.executable().into());
}
probe.push("--version".into());
probe
}
pub(crate) fn resolve_executable(command: &str) -> Option<PathBuf> {
resolve_executable_for_platform(
command,
std::env::var_os("PATH").as_deref(),
std::env::var_os("PATHEXT").as_deref(),
cfg!(windows),
)
}
pub(crate) fn resolve_executable_in_path(command: &str, path: Option<&OsStr>) -> Option<PathBuf> {
resolve_executable_for_platform(
command,
path,
std::env::var_os("PATHEXT").as_deref(),
cfg!(windows),
)
}
pub(crate) fn resolve_executable_for_platform(
command: &str,
path: Option<&OsStr>,
path_ext: Option<&OsStr>,
windows: bool,
) -> Option<PathBuf> {
if command.is_empty() {
return None;
}
let command_path = Path::new(command);
let extensions = executable_extensions(command_path, path_ext, windows);
if command_path.is_absolute() || command_path.components().count() > 1 {
return resolve_candidate(command_path, &extensions);
}
path.into_iter()
.flat_map(std::env::split_paths)
.find_map(|directory| resolve_candidate(&directory.join(command), &extensions))
}
fn executable_extensions(command: &Path, path_ext: Option<&OsStr>, windows: bool) -> Vec<OsString> {
if !windows || command.extension().is_some() {
return vec![OsString::new()];
}
path_ext
.and_then(OsStr::to_str)
.filter(|value| !value.is_empty())
.unwrap_or(".EXE;.CMD;.BAT;.COM")
.split(';')
.filter(|extension| !extension.is_empty())
.map(OsString::from)
.collect()
}
fn resolve_candidate(base: &Path, extensions: &[OsString]) -> Option<PathBuf> {
extensions.iter().find_map(|extension| {
let candidate = if extension.is_empty() {
base.to_path_buf()
} else {
let mut value = base.as_os_str().to_os_string();
value.push(extension);
PathBuf::from(value)
};
candidate.is_file().then_some(candidate)
})
}
pub(crate) fn std_command(argv: &[String]) -> Command {
debug_assert!(!argv.is_empty());
let program = resolve_executable(&argv[0]).unwrap_or_else(|| PathBuf::from(&argv[0]));
let mut command = Command::new(program);
command.args(&argv[1..]);
command
}
pub(crate) fn tokio_command(argv: &[String]) -> tokio::process::Command {
debug_assert!(!argv.is_empty());
let program = resolve_executable(&argv[0]).unwrap_or_else(|| PathBuf::from(&argv[0]));
let mut command = tokio::process::Command::new(program);
command.args(&argv[1..]);
command
}
mod supervision;
pub(crate) use supervision::SupervisedChild;
#[cfg(test)]
#[path = "../../tests/coverage/shared/agent_process_tests.rs"]
mod tests;