use std::path::Path;
use std::process::Command;
use crate::core::config;
use crate::core::detect::Detection;
use crate::runners::containers;
pub struct Check {
pub name: String,
pub ok: bool,
pub detail: String,
}
pub fn repository_checks(root: &Path) -> Vec<Check> {
let mut checks = Vec::new();
let present = |rel: &str| root.join(rel).exists();
let mut file_check = |name: &str, rel: &str| {
checks.push(Check {
name: name.to_string(),
ok: present(rel),
detail: if present(rel) {
rel.to_string()
} else {
format!("missing {rel}")
},
});
};
file_check("CI workflow", ".github/workflows/ci.yml");
file_check("Release workflow", ".github/workflows/release.yml");
file_check("Changelog", "CHANGELOG.md");
file_check("Contributing guide", "CONTRIBUTING.md");
file_check("Security policy", "SECURITY.md");
file_check("Code of conduct", "CODE_OF_CONDUCT.md");
file_check("Issue templates", ".github/ISSUE_TEMPLATE");
file_check("PR template", ".github/PULL_REQUEST_TEMPLATE.md");
file_check("Documentation", "docs");
let examples_dir = root.join("examples");
if examples_dir.is_dir() {
let mut ok = true;
let mut missing = Vec::new();
if let Ok(entries) = std::fs::read_dir(&examples_dir) {
for e in entries.flatten() {
let p = e.path();
if p.is_dir() && !p.join(".flux").is_file() && !p.join("flux.workspace").is_file() {
ok = false;
if let Some(n) = p.file_name().and_then(|n| n.to_str()) {
missing.push(n.to_string());
}
}
}
}
checks.push(Check {
name: "Examples".to_string(),
ok,
detail: if ok {
"all carry a .flux".to_string()
} else {
format!("missing .flux: {}", missing.join(", "))
},
});
} else {
checks.push(Check {
name: "Examples".to_string(),
ok: false,
detail: "no examples/ directory".to_string(),
});
}
checks
}
pub fn run(root: &Path, detection: &Detection) -> Vec<Check> {
let mut checks = Vec::new();
checks.push(binary_check("git", "git"));
if let Some(lang) = &detection.language {
let tool = match lang.as_str() {
"rust" => "cargo",
"node" => "node",
"python" => "python",
"go" => "go",
other => other,
};
checks.push(binary_check(&format!("{lang} toolchain"), tool));
} else {
checks.push(Check {
name: "language".into(),
ok: false,
detail: "no supported project detected".into(),
});
}
checks.push(match containers::engine() {
Some(e) => Check {
name: "container engine".into(),
ok: true,
detail: e.to_string(),
},
None => Check {
name: "container engine".into(),
ok: true, detail: "none (optional)".into(),
},
});
let flux_path = root.join(config::CONFIG_FILE);
if flux_path.is_file() {
match config::load(&flux_path) {
Ok(_) => checks.push(Check {
name: ".flux config".into(),
ok: true,
detail: "present and valid".into(),
}),
Err(e) => checks.push(Check {
name: ".flux config".into(),
ok: false,
detail: format!("parse error: {e}"),
}),
}
} else {
checks.push(Check {
name: ".flux config".into(),
ok: false,
detail: "missing — run `flux init`".into(),
});
}
let cache_dir = root.join(".flux-cache");
let writable = std::fs::create_dir_all(&cache_dir)
.and_then(|_| {
let probe = cache_dir.join(".doctor-probe");
std::fs::write(&probe, b"ok")?;
std::fs::remove_file(&probe)
})
.is_ok();
checks.push(Check {
name: "cache writable".into(),
ok: writable,
detail: if writable {
".flux-cache/".into()
} else {
"cannot write to .flux-cache/".into()
},
});
let installed = crate::plugins::installed(root);
checks.push(Check {
name: "plugins".into(),
ok: true,
detail: if installed.is_empty() {
"none installed".into()
} else {
installed.join(", ")
},
});
checks
}
fn binary_check(name: &str, binary: &str) -> Check {
let version = binary_version(binary);
Check {
name: name.to_string(),
ok: version.is_some(),
detail: version.unwrap_or_else(|| format!("'{binary}' not found on PATH")),
}
}
fn binary_version(binary: &str) -> Option<String> {
let out = Command::new(binary).arg("--version").output().ok()?;
if !out.status.success() {
return None;
}
Some(
String::from_utf8_lossy(&out.stdout)
.lines()
.next()
.unwrap_or("")
.trim()
.to_string(),
)
}