use std::path::{Path, PathBuf};
use anyhow::Result;
use cleanlib_client::{config, transport, types};
use crate::render::terminal;
use super::scan_exit_code;
pub async fn run(ecosystem: String, packages_path: PathBuf, output: String) -> Result<()> {
let path = config::default_path();
let cfg = config::load_with_env_overrides(path.as_deref())?;
let client = transport::Client::from_config(&cfg)?;
let packages = parse_packages_file(&packages_path, &ecosystem)?;
let req = types::PolicyPreviewRequest {
packages,
policy: None,
};
let resp = client.policy_preview(&req).await?;
match output.as_str() {
"json" => println!("{}", serde_json::to_string_pretty(&resp)?),
_ => terminal::render_decisions(&resp.decisions),
}
let code = scan_exit_code(&resp.decisions);
if code != 0 {
std::process::exit(code);
}
Ok(())
}
pub fn parse_packages_file(path: &Path, ecosystem: &str) -> Result<Vec<types::PackageRef>> {
let content = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("read {}: {}", path.display(), e))?;
let mut packages = Vec::new();
for (lineno, raw_line) in content.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (name, version) = match line.rsplit_once('@') {
Some((n, v)) if !n.is_empty() && !v.is_empty() => (n.to_string(), v.to_string()),
_ => anyhow::bail!(
"{}:{}: malformed packages-file line (expected `name@version`): {}",
path.display(),
lineno + 1,
line
),
};
packages.push(types::PackageRef {
ecosystem: ecosystem.to_string(),
name,
version,
});
}
Ok(packages)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_packages_file(contents: &str) -> PathBuf {
let dir = std::env::temp_dir();
let path = dir.join(format!(
"cleanlib-scan-test-{}.txt",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::write(&path, contents).unwrap();
path
}
#[test]
fn parses_simple() {
let p = tmp_packages_file("lodash@4.17.21\ncors@2.8.5\n");
let pkgs = parse_packages_file(&p, "npm").unwrap();
assert_eq!(pkgs.len(), 2);
assert_eq!(pkgs[0].name, "lodash");
assert_eq!(pkgs[0].version, "4.17.21");
let _ = std::fs::remove_file(&p);
}
#[test]
fn skips_comments_and_blank() {
let p = tmp_packages_file("# header\n\ncors@2.8.5\n# trailer\n");
let pkgs = parse_packages_file(&p, "npm").unwrap();
assert_eq!(pkgs.len(), 1);
let _ = std::fs::remove_file(&p);
}
#[test]
fn malformed_line_errors() {
let p = tmp_packages_file("invalid-no-at-sign\n");
let err = parse_packages_file(&p, "npm").unwrap_err();
assert!(err.to_string().contains("malformed"));
let _ = std::fs::remove_file(&p);
}
}