mod compose;
mod write;
use std::fmt;
use std::io;
use std::path::Path;
use std::process::ExitCode;
use serde_json::json;
use crate::json;
use compose::Host;
const SCHEMA: &str = "loft-skill/1";
macro_rules! source {
($($path:literal),* $(,)?) => {
&[$(($path, include_str!(concat!("../../skill/loft-spec/", $path)))),*]
};
}
const SOURCE: &[(&str, &str)] = source![
"SKILL.md",
"agents/claude/claude-host.md",
"agents/claude/frontmatter.yaml",
"agents/claude/route.md",
"agents/openai.yaml",
"references/changing-a-specified-product.md",
"references/format.md",
"references/lint-findings.md",
"references/new-document.md",
"references/publishing-and-merging.md",
"references/reviewing.md",
"references/the-loop.md",
"references/writing-rules.md",
];
#[derive(Debug)]
pub struct SkillError {
directory: &'static str,
error: io::Error,
}
impl fmt::Display for SkillError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { directory, error } = self;
write!(formatter, "{directory}: cannot write the skill: {error}")
}
}
pub fn install(root: &Path) -> Result<Vec<&'static str>, SkillError> {
let mut written = Vec::new();
for host in Host::ALL {
let files = compose::compose(host, SOURCE).expect("the embedded skill composes");
let directory = host.directory();
write::replace(&root.join(directory), &files)
.map_err(|error| SkillError { directory, error })?;
written.push(directory);
}
Ok(written)
}
pub fn error_json(error: &SkillError) -> serde_json::Value {
json::error("install", &error.to_string(), &[])
}
pub fn report(written: &[&str]) {
for directory in written {
println!("installed {directory}");
}
}
pub fn run(root: &Path, as_json: bool) -> ExitCode {
match install(root) {
Ok(written) => {
if as_json {
let fields = json!({ "skills": written });
json::print(&json::result(SCHEMA, "installed", fields, Vec::new()));
} else {
report(&written);
}
ExitCode::SUCCESS
}
Err(error) => json::stop(SCHEMA, vec![error_json(&error)], &error, as_json),
}
}