pub(crate) fn install(force: bool) -> anyhow::Result<()> {
let paths = trusty_mpm::core::paths::FrameworkPaths::default();
let report = install_to(&paths, force)?;
println!(
"Installing trusty-mpm framework artifacts to {}",
paths.framework.display()
);
for line in &report {
println!(" {line}");
}
match trusty_mpm::core::instruction_pipeline::install_system_prompt_to(
&paths.framework_instructions_path(),
) {
Ok(()) => println!(" \u{2713} instructions/INSTRUCTIONS.md (assembled)"),
Err(e) => eprintln!("warning: failed to assemble system prompt: {e:#}"),
}
println!(
"Composing agents into {}",
paths.claude_agents_dir().display()
);
let deploy = trusty_mpm::core::agent_deployer::deploy_agents(
&paths.agent_source_dir(),
&paths.claude_agents_dir(),
)?;
for line in deploy_report_lines(&deploy, &paths.agent_source_dir()) {
println!(" {line}");
}
println!(
"Deploying skills into {}",
paths.claude_skills_dir().display()
);
let skill_deploy = trusty_mpm::core::skill_deployer::deploy_skills(
&paths.skill_source_dir(),
&paths.claude_skills_dir(),
)?;
for line in skill_report_lines(&skill_deploy) {
println!(" {line}");
}
if let Err(e) = install_claude_hooks() {
eprintln!("warning: failed to install Claude Code hooks: {e:#}");
}
println!("Framework installed. Run `trusty-mpm daemon` to start.");
Ok(())
}
pub(crate) fn install_claude_hooks() -> anyhow::Result<usize> {
use colored::Colorize;
use trusty_common::claude_config::{
default_settings_max_depth, discover_claude_settings, merge_hook_entries, write_json_atomic,
};
let home =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not resolve home directory"))?;
println!(
"Wiring MPM hooks into Claude Code settings under {}…",
home.display()
);
let additions = mpm_hook_additions();
let files = discover_claude_settings(&home, default_settings_max_depth());
let target_files: Vec<std::path::PathBuf> = if files.is_empty() {
let fallback = home.join(".claude").join("settings.json");
println!(" no settings files found; creating {}", fallback.display());
vec![fallback]
} else {
files
};
let mut changed = 0usize;
for path in &target_files {
let original: serde_json::Value = match std::fs::read_to_string(path) {
Ok(s) if s.trim().is_empty() => serde_json::Value::Object(serde_json::Map::new()),
Ok(s) => match serde_json::from_str(&s) {
Ok(v) => v,
Err(e) => {
eprintln!(
" {} {} {}",
"✗".red(),
path.display(),
format!("(parse error: {e})").red()
);
continue;
}
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
serde_json::Value::Object(serde_json::Map::new())
}
Err(e) => {
eprintln!(
" {} {} {}",
"✗".red(),
path.display(),
format!("(read error: {e})").red()
);
continue;
}
};
let merged = merge_hook_entries(&original, &additions);
if merged == original {
println!(
" {} {} {}",
"↻".cyan(),
path.display().to_string().dimmed(),
"(already configured)".dimmed()
);
continue;
}
match write_json_atomic(path, &merged) {
Ok(()) => {
changed += 1;
println!(" {} {}", "✓".green(), path.display());
}
Err(e) => {
eprintln!(
" {} {} {}",
"✗".red(),
path.display(),
format!("({e})").red()
);
}
}
}
if changed > 0 {
println!(
" installed MPM hooks in {} settings file{}.",
changed,
if changed == 1 { "" } else { "s" }
);
}
Ok(changed)
}
pub(crate) fn mpm_hook_additions() -> serde_json::Value {
serde_json::json!({
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "trusty-mpm hook",
"timeout": 5
}]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "trusty-mpm hook",
"timeout": 60,
"async": true
}]
}],
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "trusty-mpm hook",
"timeout": 5
}]
}]
}
})
}
pub(crate) fn deploy_report_lines(
deploy: &trusty_mpm::core::agent_deployer::DeployResult,
source_dir: &std::path::Path,
) -> Vec<String> {
let mut lines = Vec::new();
for file in &deploy.deployed {
let name = file.trim_end_matches(".md");
let chain = trusty_mpm::core::agent_builder::source_chain(name, source_dir)
.map(|c| c.join(" \u{2192} "))
.unwrap_or_else(|_| name.to_string());
lines.push(format!("\u{2713} {file} (composed: {chain})"));
}
for file in &deploy.skipped {
lines.push(format!("~ {file} (skipped \u{2014} user-modified)"));
}
for file in &deploy.unchanged {
lines.push(format!("= {file} (unchanged)"));
}
lines
}
pub(crate) fn skill_report_lines(
deploy: &trusty_mpm::core::skill_deployer::DeployStats,
) -> Vec<String> {
let mut lines = Vec::new();
for file in &deploy.deployed {
lines.push(format!("\u{2713} {file}"));
}
for file in &deploy.skipped {
lines.push(format!("~ {file} (skipped \u{2014} user-modified)"));
}
for file in &deploy.unchanged {
lines.push(format!("= {file} (unchanged)"));
}
lines
}
pub(crate) fn install_to(
paths: &trusty_mpm::core::paths::FrameworkPaths,
force: bool,
) -> anyhow::Result<Vec<String>> {
let mut report = Vec::new();
for artifact in trusty_mpm::core::bundle::ALL {
let dest = paths.framework.join(artifact.rel_path);
if dest.exists() && !force {
report.push(format!("- {} (exists, skipped)", artifact.rel_path));
continue;
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&dest, artifact.contents)?;
report.push(format!("\u{2713} {}", artifact.rel_path));
}
Ok(report)
}