use anyhow::Result;
use cleanlib_client::{config, transport, types::PolicyDecision};
use crate::wrappers::{cargo as wrap_cargo, go as wrap_go, npm as wrap_npm, pip as wrap_pip, WrappedPackage};
pub async fn run(manager: &str, args: Vec<String>) -> Result<()> {
let mut argv_owned: Vec<String> = vec![manager.to_string()];
argv_owned.extend(args);
let argv: Vec<&str> = argv_owned.iter().map(|s| s.as_str()).collect();
let packages: Vec<WrappedPackage> = match manager {
"npm" => wrap_npm::parse(&argv),
"pip" => wrap_pip::parse(&argv),
"cargo" => wrap_cargo::parse(&argv),
"go" => wrap_go::parse(&argv),
other => return Err(anyhow::anyhow!("unsupported wrapper: {}", other)),
};
if packages.is_empty() {
eprintln!(
"# cleanlib {}: no install verb / packages detected in args; nothing to gate",
manager
);
eprintln!(
"# pass-through: run the underlying `{}` command directly, or use `cleanlib shell-init` for transparent gating",
manager
);
return Ok(());
}
eprintln!(
"# cleanlib {}: parsed {} package(s) for verdict gating",
manager,
packages.len()
);
for p in &packages {
eprintln!("# - {}/{}@{}", p.ecosystem, p.name, p.version);
}
let path = config::default_path();
let cfg = config::load_with_env_overrides(path.as_deref())?;
let client = transport::Client::from_config(&cfg)?;
let mut decisions: Vec<PolicyDecision> = Vec::with_capacity(packages.len());
let mut any_unreachable = false;
for p in &packages {
match client.fetch_verdict(&p.ecosystem, &p.name, &p.version).await {
Ok(v) => {
let dec = derive_decision_from_verdict(&v);
eprintln!(
"# {}/{}@{}: decision={}",
p.ecosystem, p.name, p.version, dec.decision
);
decisions.push(dec);
}
Err(e) => {
any_unreachable = true;
eprintln!(
"# {}/{}@{}: verdict fetch failed: {} — treating as pass-through (UNREACHABLE)",
p.ecosystem, p.name, p.version, e
);
}
}
}
if any_unreachable && decisions.is_empty() {
eprintln!("# all verdict fetches failed; pass-through mode");
return Ok(());
}
let code = super::scan_exit_code(&decisions);
if code != 0 {
std::process::exit(code);
}
Ok(())
}
fn derive_decision_from_verdict(v: &cleanlib_client::types::Verdict) -> PolicyDecision {
let decision_str = v.decision.clone().unwrap_or_else(|| {
match v.verdict.as_str() {
"VECTOR_VERDICT" | "DM_THRESHOLD_BLOCK" => "DENY".to_string(),
"INSUFFICIENT_DATA" => "WARN".to_string(),
"ALLOWED_NO_FINDINGS" => "ALLOW".to_string(),
_ => "ALLOW".to_string(),
}
});
PolicyDecision {
decision: decision_str,
..PolicyDecision::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supported_managers_resolve_without_unsupported_error() {
for manager in ["npm", "pip", "cargo", "go"] {
let argv = vec![manager.to_string()];
let argv_refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
let packages = match manager {
"npm" => wrap_npm::parse(&argv_refs),
"pip" => wrap_pip::parse(&argv_refs),
"cargo" => wrap_cargo::parse(&argv_refs),
"go" => wrap_go::parse(&argv_refs),
_ => unreachable!(),
};
assert!(packages.is_empty(), "{}: empty argv should yield empty packages", manager);
}
}
#[test]
fn unsupported_wrapper_returns_error() {
let manager = "maven";
let result = match manager {
"npm" | "pip" | "cargo" | "go" => Ok(()),
other => Err(format!("unsupported wrapper: {}", other)),
};
assert!(result.is_err());
assert!(result.unwrap_err().contains("maven"));
}
}