mod agents;
mod cli_tree;
mod doctor;
mod entry;
mod framework;
mod mcp_tools;
mod skills;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub(crate) type GeneratedSet = BTreeMap<&'static str, String>;
pub(crate) fn generate() -> GeneratedSet {
let mut set = GeneratedSet::new();
set.insert("tm-capabilities.md", entry::render());
set.insert("tm-capabilities/references/cli.md", cli_tree::render());
set.insert(
"tm-capabilities/references/mcp-tools.md",
mcp_tools::render(),
);
set.insert("tm-capabilities/references/agents.md", agents::render());
set.insert("tm-capabilities/references/skills.md", skills::render());
set.insert("tm-capabilities/references/doctor.md", doctor::render());
set.insert(
"tm-capabilities/references/framework.md",
framework::render(),
);
set
}
fn skills_asset_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("src/assets/skills")
}
pub(crate) fn write(set: &GeneratedSet, root: &Path) -> anyhow::Result<()> {
for (rel_path, content) in set {
let target = root.join(rel_path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&target, content)?;
}
Ok(())
}
pub(crate) fn diff(set: &GeneratedSet, root: &Path) -> Vec<String> {
let mut drifted = Vec::new();
for (rel_path, content) in set {
let target = root.join(rel_path);
match std::fs::read_to_string(&target) {
Ok(existing) if &existing == content => {}
Ok(_) => drifted.push(format!("{rel_path} (content differs)")),
Err(_) => drifted.push(format!("{rel_path} (missing)")),
}
}
drifted
}
pub(crate) fn run_capabilities(check: bool) -> anyhow::Result<()> {
let set = generate();
let root = skills_asset_dir();
if check {
let drifted = diff(&set, &root);
if drifted.is_empty() {
println!(
"tm-capabilities: up to date ({} generated files).",
set.len()
);
Ok(())
} else {
eprintln!(
"tm-capabilities: drift detected in {} file(s):",
drifted.len()
);
for d in &drifted {
eprintln!(" - {d}");
}
eprintln!(
"\nRun `tm generate capabilities` (no --check) to regenerate, then commit the diff."
);
anyhow::bail!("tm-capabilities drift check failed");
}
} else {
write(&set, &root)?;
println!(
"tm-capabilities: wrote {} generated files under crates/trusty-mpm/src/assets/skills/tm-capabilities*",
set.len()
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_set_has_seven_entries() {
let set = generate();
assert_eq!(set.len(), 7);
assert!(set.contains_key("tm-capabilities.md"));
assert!(set.contains_key("tm-capabilities/references/cli.md"));
assert!(set.contains_key("tm-capabilities/references/mcp-tools.md"));
assert!(set.contains_key("tm-capabilities/references/agents.md"));
assert!(set.contains_key("tm-capabilities/references/skills.md"));
assert!(set.contains_key("tm-capabilities/references/doctor.md"));
assert!(set.contains_key("tm-capabilities/references/framework.md"));
}
#[test]
fn generated_set_is_deterministic() {
let a = generate();
let b = generate();
assert_eq!(a, b);
}
#[test]
fn generated_set_no_content_is_empty() {
for (path, content) in generate() {
assert!(!content.trim().is_empty(), "{path} generated empty content");
}
}
#[test]
fn diff_reports_missing_file_against_empty_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut set = GeneratedSet::new();
set.insert("tm-capabilities/whatever.md", "content".to_string());
let drifted = diff(&set, tmp.path());
assert_eq!(drifted.len(), 1);
assert!(drifted[0].contains("missing"), "{drifted:?}");
}
#[test]
fn diff_reports_content_mismatch() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join("tm-capabilities.md"), "stale content").unwrap();
let mut set = GeneratedSet::new();
set.insert("tm-capabilities.md", "fresh content".to_string());
let drifted = diff(&set, tmp.path());
assert_eq!(drifted.len(), 1);
assert!(drifted[0].contains("content differs"), "{drifted:?}");
}
#[test]
fn write_then_diff_round_trips_clean() {
let tmp = tempfile::tempdir().expect("tempdir");
let set = generate();
write(&set, tmp.path()).expect("write succeeds");
let drifted = diff(&set, tmp.path());
assert!(
drifted.is_empty(),
"unexpected drift after write: {drifted:?}"
);
}
}