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)]
#[non_exhaustive]
pub enum ExpandedKind {
Skill(crate::agent::Skill),
SkillAsset(SkillAsset),
Agent(crate::agent::Agent),
System(SystemFile),
}
#[derive(Debug, Clone)]
pub struct SkillAsset {
pub skill_name: String,
pub source_file: PathBuf,
pub relative_path: PathBuf,
pub content: String,
}
#[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(skill_root) = skill_root_for_target(&normalized) {
return format!("{skill_root}/");
}
normalized
}
fn skill_root_for_target(normalized: &str) -> Option<&str> {
let mut segments = normalized.split('/').peekable();
let mut consumed = Vec::new();
while let Some(segment) = segments.next() {
consumed.push(segment);
if segment != "skills" {
continue;
}
let skill_name = segments.next()?;
consumed.push(skill_name);
return Some(&normalized[..consumed.join("/").len()]);
}
None
}