use ::litany::cmd::{RoutedCall, RoutedCapture};
use super::{Site, capture, loaded, remote};
use crate::registry::roster::ClientRow;
pub fn answer(site: &Site, call: &RoutedCall<'_>) -> RoutedCapture {
let roster = match super::ask::roster(&site.state_root, &site.workspace, site.budget, call.stop)
{
Ok(rows) => rows,
Err(reason) => return capture(call.name, Err(reason)),
};
match verdict(&roster, call.name) {
Ok(entry) => routed(site, &entry, call),
Err(refusal) => capture(call.name, Err(refusal)),
}
}
fn verdict(roster: &[ClientRow], name: &str) -> Result<loaded::Entry, String> {
let mut consenting: Vec<loaded::Entry> = Vec::new();
let mut advertisers: Vec<String> = Vec::new();
for row in roster {
for tool in &row.tools {
if tool.name != name {
continue;
}
advertisers.push(row.client.clone());
if tool.subject_cwd {
consenting.push(loaded::Entry {
client: row.client.clone(),
tool: tool.clone(),
});
}
}
}
match consenting.len() {
1 => Ok(consenting.remove(0)),
0 => Err(unconsented(name, &advertisers)),
_ => Err(format!(
"{} machines consent to run {name} in this conversation's working \
directory ({}), and one execution needs one machine: the operator \
must leave \"subject_cwd\": true on exactly one entry",
consenting.len(),
names(
&consenting
.iter()
.map(|e| e.client.clone())
.collect::<Vec<_>>()
),
)),
}
}
fn unconsented(name: &str, advertisers: &[String]) -> String {
if advertisers.is_empty() {
return format!(
"no tool of that name is loaded in this conversation and no machine \
of this workspace advertises {name}; use the clients tool to see \
this workspace's machines and load what one advertises — or the \
operator enrolls a thrall on the box that holds this server's \
worktrees, with \"subject_cwd\": true on a {name} entry in its \
tools.json"
);
}
format!(
"{} advertises {name}, but no machine of this workspace consents to run \
it in this conversation's working directory; load it with the clients \
tool to run it in that machine's own directory, or the operator adds \
\"subject_cwd\": true to the {name} entry in tools.json on the box \
that holds this server's worktrees",
names(advertisers),
)
}
fn names(clients: &[String]) -> String {
clients.join(", ")
}
fn routed(site: &Site, entry: &loaded::Entry, call: &RoutedCall<'_>) -> RoutedCapture {
let cwd = call.cwd.to_string_lossy().into_owned();
match remote::invoke(site, entry, call.input, Some(&cwd), call.stop) {
Ok(got) => RoutedCapture {
stdout: got.stdout.into_bytes(),
stderr: got.stderr.into_bytes(),
exit_code: got.exit_code,
},
Err(reason) => capture(call.name, Err(reason)),
}
}
#[cfg(test)]
mod tests;