use super::{GitRunner, Path, agent_exists, agent_ids, agent_ref, repo_git};
use std::io;
pub const NAME_FILE: &str = "name";
#[derive(Debug, thiserror::Error)]
pub enum Unavailable {
#[error(
"agent name {0:?} is not a single unbroken word — a name is one path component with \
no whitespace (ARCH §2.3); pick a name like `pale-otter`"
)]
Malformed(String),
#[error(
"agent name {0:?} starts with an agent-id timestamp (`YYYYMMDDTHHMMSSZ`) — names and \
ids are disjoint spaces so addressing never has to guess (ARCH §2.3); pick a name \
that does not begin like an id"
)]
IdShaped(String),
#[error(
"agent name {name:?} is already worn by agent {holder} in this workspace — a name is \
unique among living agents (ARCH §2.3); pick another, or `lernie delete` the holder \
to recycle it"
)]
Taken { name: String, holder: String },
#[error("scan the workspace's agent names: {0}")]
Scan(#[source] io::Error),
}
#[derive(Debug, thiserror::Error)]
#[error(
"agent name {name:?} is ambiguous in this workspace — it is worn by {candidates}; \
address the one you mean by its agent id (ARCH §2.11)"
)]
pub struct Ambiguous {
name: String,
candidates: String,
}
fn is_id_timestamp(s: &str) -> bool {
let b = s.as_bytes();
b.len() == 16
&& b[8] == b'T'
&& b[15] == b'Z'
&& b[..8].iter().all(u8::is_ascii_digit)
&& b[9..15].iter().all(u8::is_ascii_digit)
}
pub fn read(workspace: &Path, agent_id: &str, git: &dyn GitRunner) -> Option<String> {
let spec = format!("{}:{NAME_FILE}", agent_ref(agent_id));
let out = git
.run_capture(&repo_git(workspace), &["show", spec.as_str()])
.ok()?;
(!out.is_empty()).then_some(out)
}
pub fn in_worktree(worktree: &Path) -> Option<String> {
let body = std::fs::read_to_string(worktree.join(NAME_FILE)).ok()?;
let name = body.trim();
(!name.is_empty()).then(|| name.to_owned())
}
pub fn named(workspace: &Path, git: &dyn GitRunner) -> io::Result<Vec<(String, String)>> {
Ok(agent_ids(workspace, git)?
.into_iter()
.filter_map(|id| read(workspace, &id, git).map(|name| (id, name)))
.collect())
}
pub fn require_available(
workspace: &Path,
name: &str,
git: &dyn GitRunner,
) -> Result<(), Unavailable> {
if !crate::name::is_component(name) || name.chars().any(char::is_whitespace) {
return Err(Unavailable::Malformed(name.to_owned()));
}
if name.split('-').next().is_some_and(is_id_timestamp) {
return Err(Unavailable::IdShaped(name.to_owned()));
}
match named(workspace, git)
.map_err(Unavailable::Scan)?
.into_iter()
.find(|(_, worn)| worn == name)
{
Some((holder, _)) => Err(Unavailable::Taken {
name: name.to_owned(),
holder,
}),
None => Ok(()),
}
}
pub fn resolve(workspace: &Path, needle: &str, git: &dyn GitRunner) -> Result<String, Ambiguous> {
if agent_exists(workspace, needle, git) {
return Ok(needle.to_owned());
}
let mut hits: Vec<String> = named(workspace, git)
.unwrap_or_default()
.into_iter()
.filter(|(_, worn)| worn == needle)
.map(|(id, _)| id)
.collect();
if hits.len() < 2 {
return Ok(hits.pop().unwrap_or_else(|| needle.to_owned()));
}
hits.sort();
Err(Ambiguous {
name: needle.to_owned(),
candidates: crate::name::pool(&hits),
})
}
pub fn settle(
worktree: &Path,
name: Option<&str>,
git: &dyn GitRunner,
) -> Result<(), std::io::Error> {
let body = name.map_or_else(String::new, |n| format!("{n}\n"));
std::fs::write(worktree.join(NAME_FILE), body)?;
git.run(worktree, &["add", NAME_FILE])
}
#[cfg(test)]
mod tests;