pub mod builtin;
mod execute;
mod expand;
mod fetch;
mod plan;
pub use execute::*;
pub use expand::*;
pub use fetch::{fetch_github, read_collection_spec};
pub use plan::*;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::spec::{AgentName, OnTargetModified, Ownership, Source};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeploymentStatus {
Create,
Update,
Conflict { reason: String },
Orphan,
UpToDate,
}
#[derive(Debug, Clone)]
pub struct PlannedDeployment {
pub rule_id: String,
pub agent: AgentName,
pub source: Source,
pub source_hash: String,
pub target_path: PathBuf,
pub rendered_content: String,
pub status: DeploymentStatus,
}
#[derive(Debug)]
pub struct Plan {
pub items: Vec<PlannedDeployment>,
pub owners: Vec<Ownership>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedGithubFetch {
pub rule_id: String,
pub source: String,
pub message: String,
}
#[derive(Debug)]
pub struct PlanReport {
pub plan: Plan,
pub skipped_fetches: Vec<SkippedGithubFetch>,
}
#[derive(Debug)]
pub struct AmbiguityReport {
pub ambiguities: Vec<AmbiguousPath>,
pub skipped_fetches: Vec<SkippedGithubFetch>,
}
impl Plan {
pub fn is_clean(&self) -> bool {
self.items
.iter()
.all(|d| d.status == DeploymentStatus::UpToDate)
}
pub fn has_conflicts(&self) -> bool {
self.items
.iter()
.any(|d| matches!(d.status, DeploymentStatus::Conflict { .. }))
}
pub fn has_orphans(&self) -> bool {
self.items
.iter()
.any(|d| d.status == DeploymentStatus::Orphan)
}
}
#[derive(Debug, Clone)]
pub struct ExpandedItem {
pub rule_id: String,
pub source: Source,
pub source_content: String,
pub source_hash: String,
pub kind: ExpandedKind,
}
#[derive(Debug, Clone)]
pub enum ExpandedKind {
Skill(crate::agent::Skill),
Agent(crate::agent::Agent),
System(SystemFile),
}
#[derive(Debug, Clone)]
pub struct SystemFile {
pub file: PathBuf,
pub body: String,
}
#[derive(Debug, Clone)]
pub struct RenderedTarget {
pub rule_id: String,
pub agent: AgentName,
pub source: Source,
pub source_hash: String,
pub target_path: PathBuf,
pub content: String,
pub content_hash: String,
}
pub fn hash_content(content: &str) -> String {
blake3::hash(content.as_bytes()).to_hex().to_string()
}
pub fn effective_policy(
rule_policy: Option<OnTargetModified>,
default: OnTargetModified,
) -> OnTargetModified {
rule_policy.unwrap_or(default)
}
pub fn managed_gitignore_entries(plan: &Plan, rule_filter: Option<&str>) -> Vec<String> {
let mut entries = BTreeSet::new();
for item in &plan.items {
if item.status == DeploymentStatus::Orphan {
continue;
}
if rule_filter.is_some_and(|rule_id| item.rule_id != rule_id) {
continue;
}
entries.insert(gitignore_entry_for_target(&item.target_path));
}
entries.into_iter().collect()
}
fn gitignore_entry_for_target(target_path: &Path) -> String {
let normalized = target_path.to_string_lossy().replace('\\', "/");
if let Some(parent) = normalized.strip_suffix("/SKILL.md") {
return format!("{parent}/");
}
normalized
}