use std::path::{Path, PathBuf};
use super::PrepError;
pub(super) const OUTPUT_STYLE: &str = "trusty-mpm";
pub(super) const SPINNER_TIPS: &[&str] = &[
"tm launch — start a configured claude session for this project",
"make check = cargo test + clippy + fmt — must pass before any PR",
"API → CLI → TUI: implement at the lowest layer first",
"Delegate Rust code to rust-engineer — PM never edits .rs files",
"gh issue create to track work; commits include Closes #N",
"tmux ls shows all active tmpm-<folder> sessions",
"tm session list shows daemon-managed sessions",
"/compact at ~50% context to stay focused",
"Layer new features behind the HTTP API before wiring CLI or TUI",
];
pub(super) const TRUSTY_MEMORY_HOOKS: &str = r#"{
"PostToolUse": [
{
"matcher": "Write|Edit|Bash",
"hooks": [
{
"type": "command",
"command": "trusty-memory hooks fire claude.post-tool-use",
"timeout": 60
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "trusty-memory hooks fire claude.stop",
"timeout": 60
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "trusty-memory hooks fire claude.user-prompt",
"timeout": 60
}
]
}
]
}"#;
pub(super) const TRUSTY_MEMORY_MCP_SERVER: &str = r#"{
"type": "stdio",
"command": "trusty-memory",
"args": ["mcp", "serve"]
}"#;
pub(super) const GLOBAL_TRUSTY_MEMORY_EVENTS: &[&str] =
&["PostToolUse", "Stop", "UserPromptSubmit"];
pub(super) fn deploy_output_style(home: &Path) -> Result<PathBuf, PrepError> {
let style_dir = home.join(".claude").join("output-styles");
std::fs::create_dir_all(&style_dir).map_err(|source| PrepError::Io {
path: style_dir.clone(),
source,
})?;
let style_path = style_dir.join("trusty-mpm.md");
std::fs::write(&style_path, crate::core::bundle::OUTPUT_STYLE).map_err(|source| {
PrepError::Io {
path: style_path.clone(),
source,
}
})?;
Ok(style_path)
}
pub(super) fn write_output_style(project_dir: &Path) -> Result<(), PrepError> {
let claude_dir = project_dir.join(".claude");
std::fs::create_dir_all(&claude_dir).map_err(|source| PrepError::Io {
path: claude_dir.clone(),
source,
})?;
let settings_path = claude_dir.join("settings.json");
let mut settings = match std::fs::read_to_string(&settings_path) {
Ok(text) => serde_json::from_str::<serde_json::Value>(&text)
.ok()
.filter(serde_json::Value::is_object)
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new())),
Err(_) => serde_json::Value::Object(serde_json::Map::new()),
};
settings["outputStyle"] = serde_json::Value::String(OUTPUT_STYLE.to_string());
settings["spinnerTipsEnabled"] = serde_json::Value::Bool(true);
settings["spinnerTipsOverride"] = serde_json::json!({ "tips": SPINNER_TIPS });
let serialized = serde_json::to_string_pretty(&settings)
.map_err(|err| PrepError::Deploy(err.to_string()))?;
std::fs::write(&settings_path, serialized).map_err(|source| PrepError::Io {
path: settings_path.clone(),
source,
})?;
Ok(())
}
pub(super) fn write_project_hooks(project_dir: &Path) -> Result<(), PrepError> {
let claude_dir = project_dir.join(".claude");
std::fs::create_dir_all(&claude_dir).map_err(|source| PrepError::Io {
path: claude_dir.clone(),
source,
})?;
let settings_path = claude_dir.join("settings.json");
let mut settings = match std::fs::read_to_string(&settings_path) {
Ok(text) => serde_json::from_str::<serde_json::Value>(&text)
.ok()
.filter(serde_json::Value::is_object)
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new())),
Err(_) => serde_json::Value::Object(serde_json::Map::new()),
};
let hooks: serde_json::Value =
serde_json::from_str(TRUSTY_MEMORY_HOOKS).expect("bundled hook block is valid JSON");
settings["hooks"] = hooks;
let serialized = serde_json::to_string_pretty(&settings)
.map_err(|err| PrepError::Deploy(err.to_string()))?;
std::fs::write(&settings_path, serialized).map_err(|source| PrepError::Io {
path: settings_path.clone(),
source,
})?;
Ok(())
}
pub(super) fn inject_trusty_memory_mcp(project_path: &Path) -> Result<(), PrepError> {
let mcp_path = project_path.join(".mcp.json");
let mut config = match std::fs::read_to_string(&mcp_path) {
Ok(text) => serde_json::from_str::<serde_json::Value>(&text)
.ok()
.filter(serde_json::Value::is_object)
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new())),
Err(_) => serde_json::Value::Object(serde_json::Map::new()),
};
let server: serde_json::Value = serde_json::from_str(TRUSTY_MEMORY_MCP_SERVER)
.expect("bundled trusty-memory MCP server block is valid JSON");
let servers = config
.as_object_mut()
.expect("config starts as an object")
.entry("mcpServers")
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if !servers.is_object() {
*servers = serde_json::Value::Object(serde_json::Map::new());
}
let servers = servers
.as_object_mut()
.expect("mcpServers normalized to an object");
if servers.get("trusty-memory") == Some(&server) {
return Ok(());
}
servers.insert("trusty-memory".to_string(), server);
let serialized =
serde_json::to_string_pretty(&config).map_err(|err| PrepError::Deploy(err.to_string()))?;
std::fs::write(&mcp_path, serialized).map_err(|source| PrepError::Io {
path: mcp_path.clone(),
source,
})?;
Ok(())
}
pub(super) fn remove_global_trusty_memory_hooks() -> Result<(), PrepError> {
let home = match dirs::home_dir() {
Some(home) => home,
None => {
tracing::warn!("skipping global trusty-memory hook removal: home unresolved");
return Ok(());
}
};
let settings_path = home.join(".claude").join("settings.json");
clean_global_trusty_memory_hooks(&settings_path)
}
pub(super) fn clean_global_trusty_memory_hooks(settings_path: &Path) -> Result<(), PrepError> {
let text = match std::fs::read_to_string(settings_path) {
Ok(text) => text,
Err(_) => return Ok(()),
};
let mut settings = match serde_json::from_str::<serde_json::Value>(&text) {
Ok(value) if value.is_object() => value,
_ => return Ok(()),
};
let Some(hooks) = settings.get_mut("hooks").and_then(|h| h.as_object_mut()) else {
return Ok(());
};
for event in GLOBAL_TRUSTY_MEMORY_EVENTS {
let Some(groups) = hooks.get_mut(*event).and_then(|g| g.as_array_mut()) else {
continue;
};
groups.retain(|group| !group_is_trusty_memory(group));
if groups.is_empty() {
hooks.remove(*event);
}
}
let serialized = serde_json::to_string_pretty(&settings)
.map_err(|err| PrepError::Deploy(err.to_string()))?;
std::fs::write(settings_path, serialized).map_err(|source| PrepError::Io {
path: settings_path.to_path_buf(),
source,
})?;
Ok(())
}
pub(super) fn group_is_trusty_memory(group: &serde_json::Value) -> bool {
group
.get("hooks")
.and_then(|h| h.as_array())
.is_some_and(|handlers| {
handlers.iter().any(|handler| {
handler
.get("command")
.and_then(|c| c.as_str())
.is_some_and(|cmd| cmd.contains("trusty-memory hooks fire"))
})
})
}