use anodizer_core::log::{StageLogger, Verbosity};
use anyhow::Result;
use colored::Colorize;
struct ToolCheck {
name: &'static str,
description: &'static str,
}
const TOOLS: &[ToolCheck] = &[
ToolCheck {
name: "cargo",
description: "Rust package manager",
},
ToolCheck {
name: "git",
description: "Version control",
},
ToolCheck {
name: "docker",
description: "Container runtime",
},
ToolCheck {
name: "podman",
description: "Container runtime (Linux-only alt backend)",
},
ToolCheck {
name: "nfpm",
description: "Linux package builder (deb/rpm/apk)",
},
ToolCheck {
name: "cargo-zigbuild",
description: "Cross-compilation via Zig",
},
ToolCheck {
name: "zig",
description: "Zig toolchain (linker/libc behind cargo-zigbuild)",
},
ToolCheck {
name: "cross",
description: "Cross-compilation via Docker",
},
ToolCheck {
name: "gpg",
description: "GNU Privacy Guard (signing)",
},
ToolCheck {
name: "cosign",
description: "Sigstore container signing",
},
ToolCheck {
name: "aws",
description: "AWS CLI (S3 blob storage)",
},
ToolCheck {
name: "gsutil",
description: "Google Cloud Storage CLI",
},
ToolCheck {
name: "az",
description: "Azure CLI (Blob storage)",
},
];
use anodizer_core::tool_detect::{ToolProbe, runs, tool_version};
pub fn run() -> Result<()> {
let log = StageLogger::new("healthcheck", Verbosity::Normal);
log.status(&format!("{}", "Anodizer Environment Health Check".bold()));
log.status(&"=".repeat(40));
let mut available_count = 0;
let mut missing_count = 0;
let mut unprobeable_count = 0;
for tool in TOOLS {
match runs(tool.name) {
ToolProbe::Available => {
let version = match tool_version(tool.name) {
Ok(v) => v,
Err(e) => {
tracing::trace!(tool = tool.name, error = %e, "version probe failed");
None
}
};
let parenthetical = version
.map(|v| format!(" ({})", v.dimmed()))
.unwrap_or_default();
log.status(&format!(
"{} {:<20} {}{}",
"\u{2713}".green().bold(),
tool.name,
tool.description.dimmed(),
parenthetical
));
available_count += 1;
}
ToolProbe::Unavailable => {
log.status(&format!(
"{} {:<20} {}",
"\u{2717}".red().bold(),
tool.name,
tool.description.dimmed()
));
missing_count += 1;
}
ToolProbe::ProbeFailed(e) => {
log.status(&format!(
"{} {:<20} {} (probe failed: {})",
"?".yellow().bold(),
tool.name,
tool.description.dimmed(),
e
));
unprobeable_count += 1;
}
}
}
let mut summary = format!(
"{} available, {} missing",
available_count.to_string().green().bold(),
missing_count.to_string().yellow().bold()
);
if unprobeable_count > 0 {
summary.push_str(&format!(
", {} unprobeable",
unprobeable_count.to_string().red().bold()
));
}
log.status(&summary);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_runs_cargo() {
assert!(
matches!(runs("cargo"), ToolProbe::Available),
"cargo should be available"
);
}
#[test]
fn test_runs_nonexistent_is_unavailable() {
assert!(matches!(
runs("this-tool-does-not-exist-12345"),
ToolProbe::Unavailable
));
}
#[test]
fn test_tool_version_cargo() {
let version = tool_version("cargo").expect("cargo should spawn");
assert!(version.is_some(), "cargo --version should produce output");
assert!(
version.unwrap().contains("cargo"),
"cargo version should contain 'cargo'"
);
}
#[test]
fn test_tool_version_nonexistent_surfaces_error() {
let res = tool_version("this-tool-does-not-exist-12345");
assert!(res.is_err(), "nonexistent tool must surface a spawn error");
}
#[test]
fn test_tools_list_is_not_empty() {
assert!(!TOOLS.is_empty(), "TOOLS list should not be empty");
}
#[test]
fn test_healthcheck_run_succeeds() {
let result = run();
assert!(result.is_ok(), "healthcheck should always succeed");
}
}