skiller 0.2.0

Declarative project and global skill management over the Vercel Skills CLI
use std::path::Path;

use anyhow::{Context, Result, bail};

pub fn apply_manual_mode(skill_root: &Path) -> Result<()> {
    let skill_md = skill_root.join("SKILL.md");
    let raw = std::fs::read_to_string(&skill_md)
        .with_context(|| format!("reading {}", skill_md.display()))?;
    let updated = set_frontmatter_field(&raw, "disable-model-invocation", "true")?;
    std::fs::write(&skill_md, updated)
        .with_context(|| format!("writing {}", skill_md.display()))?;
    set_codex_policy(&skill_root.join("agents/openai.yaml"))
}

pub fn rename_skill(skill_root: &Path, installed_name: &str) -> Result<()> {
    let skill_md = skill_root.join("SKILL.md");
    let raw = std::fs::read_to_string(&skill_md)
        .with_context(|| format!("reading {}", skill_md.display()))?;
    let updated = set_frontmatter_field(&raw, "name", installed_name)?;
    std::fs::write(&skill_md, updated).with_context(|| format!("writing {}", skill_md.display()))
}

fn set_frontmatter_field(raw: &str, key: &str, value: &str) -> Result<String> {
    let lines: Vec<&str> = raw.lines().collect();
    if lines.first().copied() != Some("---") {
        bail!("SKILL.md is missing YAML frontmatter");
    }
    let closing = lines
        .iter()
        .enumerate()
        .skip(1)
        .find_map(|(index, line)| (*line == "---").then_some(index))
        .context("SKILL.md frontmatter is not closed")?;
    let matches: Vec<usize> = lines[..closing]
        .iter()
        .enumerate()
        .filter_map(|(index, line)| {
            line.split_once(':')
                .filter(|(candidate, _)| {
                    candidate.trim() == key && !candidate.starts_with(char::is_whitespace)
                })
                .map(|_| index)
        })
        .collect();
    if matches.len() > 1 {
        bail!("SKILL.md contains duplicate {key} fields");
    }
    let mut output = Vec::with_capacity(lines.len() + 1);
    for (index, line) in lines.iter().enumerate() {
        if matches.first() == Some(&index) {
            output.push(format!("{key}: {value}"));
        } else if index == closing && matches.is_empty() {
            output.push(format!("{key}: {value}"));
            output.push((*line).to_owned());
        } else {
            output.push((*line).to_owned());
        }
    }
    Ok(format!("{}\n", output.join("\n")))
}

fn set_codex_policy(path: &Path) -> Result<()> {
    let parent = path.parent().context("Codex sidecar has no parent")?;
    crate::paths::ensure_real_dir(parent)?;
    let raw = match std::fs::read_to_string(path) {
        Ok(raw) => raw,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            return std::fs::write(path, "policy:\n  allow_implicit_invocation: false\n")
                .with_context(|| format!("writing {}", path.display()));
        }
        Err(error) => return Err(error).with_context(|| format!("reading {}", path.display())),
    };
    let mut lines: Vec<String> = raw.lines().map(str::to_owned).collect();
    let allow_lines: Vec<usize> = lines
        .iter()
        .enumerate()
        .filter_map(|(index, line)| {
            line.trim_start()
                .starts_with("allow_implicit_invocation:")
                .then_some(index)
        })
        .collect();
    if allow_lines.len() > 1 {
        bail!("Codex sidecar contains duplicate allow_implicit_invocation fields");
    }
    if let Some(index) = allow_lines.first() {
        let indent = lines[*index].len() - lines[*index].trim_start().len();
        lines[*index] = format!("{}allow_implicit_invocation: false", " ".repeat(indent));
    } else {
        let policy_lines: Vec<usize> = lines
            .iter()
            .enumerate()
            .filter_map(|(index, line)| (line == "policy:").then_some(index))
            .collect();
        if policy_lines.len() > 1 {
            bail!("Codex sidecar contains duplicate policy mappings");
        }
        if let Some(index) = policy_lines.first() {
            lines.insert(index + 1, "  allow_implicit_invocation: false".to_owned());
        } else {
            if !lines.is_empty() && !lines.last().is_some_and(String::is_empty) {
                lines.push(String::new());
            }
            lines.extend([
                "policy:".to_owned(),
                "  allow_implicit_invocation: false".to_owned(),
            ]);
        }
    }
    std::fs::write(path, format!("{}\n", lines.join("\n")))
        .with_context(|| format!("writing {}", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn frontmatter_field_is_inserted_or_replaced() {
        let raw = "---\nname: old\ndescription: Test\n---\nBody\n";
        assert!(
            set_frontmatter_field(raw, "disable-model-invocation", "true")
                .unwrap()
                .contains("disable-model-invocation: true\n---")
        );
        assert!(
            set_frontmatter_field(raw, "name", "new-scope")
                .unwrap()
                .contains("name: new-scope")
        );
    }
}