use crate::error::MarsError;
use crate::hash;
use super::output;
#[derive(Debug, clap::Args)]
pub struct DoctorArgs {}
pub fn run(_args: &DoctorArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
let mut issues = Vec::new();
match crate::config::load(&ctx.managed_root) {
Ok(_) => {}
Err(e) => {
issues.push(format!("config error: {e}"));
}
}
let lock = match crate::lock::load(&ctx.managed_root) {
Ok(l) => l,
Err(e) => {
issues.push(format!("lock file error: {e}"));
output::print_doctor(&issues, json);
return Ok(2);
}
};
for (dest_path_str, item) in &lock.items {
let disk_path = ctx.managed_root.join(dest_path_str);
if !disk_path.exists() {
issues.push(format!(
"{dest_path_str} missing from disk. Run `mars sync` to reinstall or `mars repair` to rebuild"
));
continue;
}
if item.kind == crate::lock::ItemKind::Agent
&& let Ok(content) = std::fs::read_to_string(&disk_path)
&& content.contains("<<<<<<<")
&& content.contains(">>>>>>>")
{
issues.push(format!("{dest_path_str} has unresolved conflict markers"));
}
match hash::compute_hash(&disk_path, item.kind) {
Ok(disk_hash) => {
if disk_hash != item.installed_checksum {
}
}
Err(e) => {
issues.push(format!("can't hash {dest_path_str}: {e}"));
}
}
}
if let Ok(config) = crate::config::load(&ctx.managed_root) {
let local = crate::config::load_local(&ctx.managed_root).unwrap_or_default();
if let Ok(effective) = crate::config::merge_with_root(config, local, &ctx.managed_root) {
for source_name in effective.sources.keys() {
if !lock.sources.contains_key(source_name) {
issues.push(format!(
"source `{source_name}` is in config but not in lock — run `mars sync`"
));
}
}
}
}
{
use std::collections::HashSet;
let installed = crate::discover::discover_installed(&ctx.managed_root)?;
for item in installed.agents.iter().chain(installed.skills.iter()) {
if item.is_symlink {
let kind = if item.id.kind == crate::lock::ItemKind::Agent {
"agent"
} else {
"skill"
};
issues.push(format!(
"skipping symlinked {kind} `{}` — individual symlinks in managed dirs are not validated",
item.id.name
));
}
}
let available_skills: HashSet<String> = installed
.skills
.iter()
.filter(|s| !s.is_symlink)
.map(|s| s.id.name.to_string())
.collect();
let agents_for_check: Vec<(String, std::path::PathBuf)> = installed
.agents
.iter()
.filter(|a| !a.is_symlink)
.map(|a| (a.id.name.to_string(), a.path.clone()))
.collect();
if let Ok(warnings) = crate::validate::check_deps(&agents_for_check, &available_skills) {
for w in &warnings {
match w {
crate::validate::ValidationWarning::MissingSkill {
agent,
skill_name,
suggestion,
} => {
let msg = match suggestion {
Some(s) => format!(
"agent `{}` references missing skill `{skill_name}` (did you mean `{s}`?)",
agent.name
),
None => format!(
"agent `{}` references missing skill `{skill_name}` — \
add a source that provides it, or create it locally in skills/{skill_name}/",
agent.name
),
};
issues.push(msg);
}
}
}
}
}
if let Ok(config) = crate::config::load(&ctx.managed_root) {
for link_target in &config.settings.links {
check_link_health(ctx, link_target, &mut issues);
}
}
output::print_doctor(&issues, json);
if issues.is_empty() { Ok(0) } else { Ok(2) }
}
fn check_link_health(ctx: &super::MarsContext, target: &str, issues: &mut Vec<String>) {
let target_dir = ctx.project_root.join(target);
if !target_dir.exists() {
issues.push(format!(
"link `{target}` — directory doesn't exist. Run `mars link --unlink {target}` to remove stale entry"
));
return;
}
for subdir in ["agents", "skills"] {
let link_path = target_dir.join(subdir);
let expected = ctx.managed_root.join(subdir);
if link_path.symlink_metadata().is_err() {
issues.push(format!(
"link `{target}` — missing {target}/{subdir} symlink. Run `mars link {target}` to fix"
));
continue;
}
match link_path.read_link() {
Ok(actual_target) => {
let resolved = target_dir.join(&actual_target);
let points_to_managed = match (resolved.canonicalize(), expected.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => false,
};
if !points_to_managed {
issues.push(format!(
"link `{target}` — {target}/{subdir} points to {} (expected {})",
actual_target.display(),
expected.display()
));
} else if !link_path.exists() {
issues.push(format!(
"link `{target}` — {target}/{subdir} is a broken symlink"
));
}
}
Err(_) => {
issues.push(format!(
"link `{target}` — {target}/{subdir} is a real directory, not a symlink. Run `mars link {target}` to merge and link"
));
}
}
}
}