use std::{
collections::HashSet,
path::{Path, PathBuf},
};
const AGENTS_FILE_NAME: &str = "AGENTS.md";
const MAX_SUBDIR_AGENTS_BYTES: u64 = 256 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DiscoveredInstruction {
pub path: PathBuf,
pub content: String,
pub bytes: usize,
}
pub(crate) fn discover_subdir_instructions(
touched_path: &Path,
root_canonical: &Path,
already_loaded: &HashSet<PathBuf>,
) -> (Vec<DiscoveredInstruction>, Vec<String>) {
discover_subdir_instructions_with_diagnostics(touched_path, root_canonical, already_loaded)
}
fn discover_subdir_instructions_with_diagnostics(
touched_path: &Path,
root_canonical: &Path,
already_loaded: &HashSet<PathBuf>,
) -> (Vec<DiscoveredInstruction>, Vec<String>) {
let mut diagnostics = Vec::new();
let root = match root_canonical.canonicalize() {
Ok(root) => root,
Err(error) => {
diagnostics.push(format!("failed to canonicalize root: {error}"));
return (Vec::new(), diagnostics);
}
};
let touched = match touched_path.canonicalize() {
Ok(touched) => touched,
Err(error) => {
diagnostics.push(format!("failed to canonicalize touched path: {error}"));
return (Vec::new(), diagnostics);
}
};
if !touched.starts_with(&root) {
return (Vec::new(), diagnostics);
}
let mut cursor = if touched.is_dir() {
touched
} else {
touched.parent().unwrap_or(&touched).to_path_buf()
};
let mut discovered = Vec::new();
let mut seen = HashSet::new();
while cursor.starts_with(&root) && cursor != root {
let candidate = cursor.join(AGENTS_FILE_NAME);
if candidate.exists() {
match candidate.canonicalize() {
Ok(canonical) => {
if canonical.starts_with(&root)
&& !already_loaded.contains(&canonical)
&& seen.insert(canonical.clone())
{
match load_candidate(&canonical) {
Ok(instruction) => discovered.push(instruction),
Err(message) => {
diagnostics.push(format!("{}: {message}", canonical.display()))
}
}
}
}
Err(error) => diagnostics.push(format!(
"{}: failed to canonicalize candidate: {error}",
candidate.display()
)),
}
}
let Some(parent) = cursor.parent() else {
break;
};
cursor = parent.to_path_buf();
}
(discovered, diagnostics)
}
fn load_candidate(path: &Path) -> Result<DiscoveredInstruction, String> {
let file = crate::prompt_file::read_prompt_file(path, MAX_SUBDIR_AGENTS_BYTES, false)
.map_err(|error| error.to_string())?;
Ok(DiscoveredInstruction {
path: path.to_path_buf(),
content: file.text,
bytes: file.bytes,
})
}