use std::path::{Path, PathBuf};
use crate::error::ZigError;
use crate::paths::expand_path;
use crate::workflow::model::ResourceSpec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceOrigin {
GlobalShared,
GlobalWorkflow,
Cwd,
Workflow,
Step,
}
impl ResourceOrigin {
pub fn label(self) -> &'static str {
match self {
ResourceOrigin::GlobalShared => "global:_shared",
ResourceOrigin::GlobalWorkflow => "global:workflow",
ResourceOrigin::Cwd => "cwd",
ResourceOrigin::Workflow => "inline:workflow",
ResourceOrigin::Step => "inline:step",
}
}
}
#[derive(Debug, Clone)]
pub struct Resource {
pub abs_path: PathBuf,
pub name: String,
pub description: Option<String>,
pub origin: ResourceOrigin,
}
#[derive(Debug, Clone, Default)]
pub struct ResourceSet {
entries: Vec<Resource>,
}
impl ResourceSet {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn iter(&self) -> impl Iterator<Item = &Resource> {
self.entries.iter()
}
fn push(&mut self, res: Resource) {
if self.entries.iter().any(|e| e.abs_path == res.abs_path) {
return;
}
self.entries.push(res);
}
}
pub struct ResourceCollector<'a> {
pub workflow_resources: &'a [ResourceSpec],
pub workflow_dir: &'a Path,
pub global_shared_dir: Option<PathBuf>,
pub global_workflow_dir: Option<PathBuf>,
pub cwd_resources_dir: Option<PathBuf>,
pub disabled: bool,
}
impl<'a> ResourceCollector<'a> {
pub fn from_env(
workflow_name: &str,
workflow_resources: &'a [ResourceSpec],
workflow_dir: &'a Path,
disabled: bool,
) -> Self {
Self {
workflow_resources,
workflow_dir,
global_shared_dir: crate::paths::global_shared_resources_dir(),
global_workflow_dir: crate::paths::global_resources_for(workflow_name),
cwd_resources_dir: crate::paths::cwd_resources_dir(),
disabled,
}
}
pub fn collect_for_step(
&self,
step_resources: &[ResourceSpec],
) -> Result<ResourceSet, ZigError> {
let mut set = ResourceSet::new();
if self.disabled {
return Ok(set);
}
if let Some(dir) = self.global_shared_dir.as_deref() {
scan_directory_into(dir, ResourceOrigin::GlobalShared, &mut set);
}
if let Some(dir) = self.global_workflow_dir.as_deref() {
scan_directory_into(dir, ResourceOrigin::GlobalWorkflow, &mut set);
}
if let Some(dir) = self.cwd_resources_dir.as_deref() {
scan_directory_into(dir, ResourceOrigin::Cwd, &mut set);
}
for spec in self.workflow_resources {
if let Some(res) = resolve_inline(spec, self.workflow_dir, ResourceOrigin::Workflow)? {
set.push(res);
}
}
for spec in step_resources {
if let Some(res) = resolve_inline(spec, self.workflow_dir, ResourceOrigin::Step)? {
set.push(res);
}
}
Ok(set)
}
}
fn scan_directory_into(dir: &Path, origin: ResourceOrigin, set: &mut ResourceSet) {
if !dir.is_dir() {
return;
}
let mut stack = vec![dir.to_path_buf()];
while let Some(current) = stack.pop() {
let entries = match std::fs::read_dir(¤t) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let metadata = match std::fs::metadata(&path) {
Ok(m) => m,
Err(_) => continue,
};
if metadata.is_dir() {
stack.push(path);
continue;
}
if !metadata.is_file() {
continue;
}
let abs_path = match std::fs::canonicalize(&path) {
Ok(p) => p,
Err(_) => continue,
};
let name = abs_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| abs_path.display().to_string());
set.push(Resource {
abs_path,
name,
description: None,
origin,
});
}
}
}
fn resolve_inline(
spec: &ResourceSpec,
workflow_dir: &Path,
origin: ResourceOrigin,
) -> Result<Option<Resource>, ZigError> {
let raw_path = spec.path();
if raw_path.is_empty() {
return Err(ZigError::Execution(
"resource entry has an empty path".into(),
));
}
let joined = workflow_dir.join(expand_path(raw_path));
let abs_path = match std::fs::canonicalize(&joined) {
Ok(p) => p,
Err(_) => {
if spec.required() {
return Err(ZigError::Execution(format!(
"required resource '{}' not found (looked at {})",
raw_path,
joined.display()
)));
}
eprintln!(
" warning: resource '{}' not found at {} — skipping",
raw_path,
joined.display()
);
return Ok(None);
}
};
let name = spec
.name()
.map(str::to_string)
.or_else(|| {
abs_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
})
.unwrap_or_else(|| raw_path.to_string());
Ok(Some(Resource {
abs_path,
name,
description: spec.description().map(str::to_string),
origin,
}))
}
pub fn collect_inline_resources(
workflow_resources: &[ResourceSpec],
step_resources: &[ResourceSpec],
workflow_dir: &Path,
) -> Result<ResourceSet, ZigError> {
let mut set = ResourceSet::new();
for spec in workflow_resources {
if let Some(res) = resolve_inline(spec, workflow_dir, ResourceOrigin::Workflow)? {
set.push(res);
}
}
for spec in step_resources {
if let Some(res) = resolve_inline(spec, workflow_dir, ResourceOrigin::Step)? {
set.push(res);
}
}
Ok(set)
}
pub fn render_system_block(set: &ResourceSet) -> String {
if set.is_empty() {
return String::new();
}
let mut out = String::from("<resources>\n");
out.push_str(
"You have access to the following reference files. Read them with your file tools when the user's request relates to them.\n\n",
);
for res in set.iter() {
out.push_str("- ");
out.push_str(&res.abs_path.display().to_string());
if let Some(desc) = res.description.as_deref() {
out.push_str(" — ");
out.push_str(desc);
} else {
out.push_str(" (");
out.push_str(&res.name);
out.push(')');
}
out.push('\n');
}
out.push_str("</resources>\n\n");
out
}
#[cfg(test)]
#[path = "resources_tests.rs"]
mod tests;