use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Result, bail};
use crate::adapters::claude_code;
use crate::config::Config;
use crate::mode::{self, Mode};
const MAX_BODY_CHARS: usize = 10_000;
pub fn execute(modes_dirs: Vec<PathBuf>, config: &Config) -> Result<()> {
let dirs = config.resolve_modes_dirs(modes_dirs)?;
let mut warnings = 0;
let mut errors = 0;
println!("modes_dirs:");
for dir in &dirs {
let status = if dir.is_dir() { "ok" } else { "missing" };
println!(" [{status}] {}", dir.display());
if !dir.is_dir() {
warnings += 1;
}
}
let paths = mode::list_mode_paths(&dirs)?;
let mut seen = std::collections::HashSet::new();
if paths.is_empty() {
println!("modes: none found");
warnings += 1;
} else {
println!("modes: {} file(s)", paths.len());
}
for path in &paths {
let text = match fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
println!(" ! {}: error: {e}", path.display());
errors += 1;
continue;
}
};
let m = mode::parse_mode(&text, path);
for issue in validate_mode(&m) {
println!(" ! {}: {issue}", m.name);
if issue.starts_with("error:") {
errors += 1;
} else {
warnings += 1;
}
}
let stem = m.path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if stem != m.name {
println!(
" ! {}: warning: name {:?} does not match filename stem {:?}",
m.name, m.name, stem
);
warnings += 1;
}
if !seen.insert(m.name.clone()) {
println!(
" ! {}: warning: duplicate name, earlier directory wins at runtime",
m.name
);
warnings += 1;
}
}
match claude_code::registered_command(&claude_code::settings_path())? {
Some(cmd) => {
let path = Path::new(&cmd);
let status = if path.is_file() {
"ok"
} else {
"missing binary"
};
println!("hook: registered -> {cmd} [{status}]");
if !path.is_file() {
errors += 1;
}
}
None => {
println!("hook: not registered");
warnings += 1;
}
}
if errors > 0 {
bail!("{errors} error(s), {warnings} warning(s)");
}
println!("check passed ({warnings} warning(s))");
Ok(())
}
fn validate_mode(mode: &Mode) -> Vec<String> {
let mut issues = Vec::new();
if mode.triggers_re.is_none() {
issues.push("error: triggers contain no usable term".into());
}
for term in &mode.terms {
if term.contains('|') {
issues.push(format!(
"warning: term {term:?} contains '|' — triggers are comma-separated literal terms, not regex"
));
}
}
if mode.body.chars().count() > MAX_BODY_CHARS {
issues.push(format!(
"warning: body exceeds {MAX_BODY_CHARS} characters (Claude Code truncates hook output)"
));
}
issues
}