use crate::error::MarsError;
use crate::hash;
use crate::types::SourceOrigin;
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 errors = Vec::new();
let mut warnings = Vec::new();
match crate::config::load(&ctx.project_root) {
Ok(_) => {}
Err(e) => {
errors.push(format!("config error: {e}"));
}
}
let lock = match crate::lock::load(&ctx.project_root) {
Ok(l) => l,
Err(e) => {
errors.push(format!("lock file error: {e}"));
output::print_doctor(&errors, &warnings, json);
return Ok(2);
}
};
let mars_dir = ctx.project_root.join(".mars");
for (dest_path_str, item) in &lock.items {
let disk_path = mars_dir.join(dest_path_str);
if !disk_path.exists() {
errors.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(">>>>>>>")
{
errors.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) => {
errors.push(format!("can't hash {dest_path_str}: {e}"));
}
}
}
if let Ok(config) = crate::config::load(&ctx.project_root) {
let local = crate::config::load_local(&ctx.project_root).unwrap_or_default();
if let Ok((effective, _diagnostics)) =
crate::config::merge_with_root(config, local, &ctx.project_root)
{
for source_name in effective.dependencies.keys() {
if !lock.dependencies.contains_key(source_name) {
errors.push(format!(
"dependency `{source_name}` is in config but not in lock — run `mars sync`"
));
}
}
}
}
{
use std::collections::HashSet;
let installed = crate::discover::discover_installed(&mars_dir)?;
for item in installed.agents.iter().chain(installed.skills.iter()) {
if item.is_symlink {
if is_self_symlink(item, ctx, &lock) {
continue;
}
let kind = if item.id.kind == crate::lock::ItemKind::Agent {
"agent"
} else {
"skill"
};
warnings.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
),
};
errors.push(msg);
}
}
}
}
}
check_mars_gitignore(&ctx.project_root, &mut warnings);
output::print_doctor(&errors, &warnings, json);
if errors.is_empty() { Ok(0) } else { Ok(2) }
}
fn check_mars_gitignore(project_root: &std::path::Path, warnings: &mut Vec<String>) {
let mars_dir = project_root.join(".mars");
if !mars_dir.exists() {
return;
}
let gitignore_path = project_root.join(".gitignore");
let is_ignored = match std::fs::read_to_string(&gitignore_path) {
Ok(content) => content.lines().any(|line| {
let trimmed = line.trim();
trimmed == ".mars" || trimmed == ".mars/" || trimmed == "/.mars" || trimmed == "/.mars/"
}),
Err(_) => false,
};
if !is_ignored {
warnings.push(
".mars/ is not in .gitignore — add `.mars/` to your .gitignore to avoid committing cached data"
.to_string(),
);
}
}
fn is_self_symlink(
item: &crate::discover::InstalledItem,
ctx: &super::MarsContext,
lock: &crate::lock::LockFile,
) -> bool {
let mars_dir = ctx.project_root.join(".mars");
let Ok(rel_path) = item.path.strip_prefix(&mars_dir) else {
return false;
};
let dest_path = crate::types::DestPath::from(rel_path);
let local_source_name = SourceOrigin::LocalPackage.to_string();
lock.items
.get(&dest_path)
.is_some_and(|locked| locked.source.as_ref() == local_source_name.as_str())
}