use std::path::Path;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Result;
use cleanlib_client::risk_acceptance;
fn require_nonblank(value: &str, flag: &str, why: &str) -> Result<()> {
if value.trim().is_empty() {
anyhow::bail!("--{flag} must not be empty: {why}");
}
Ok(())
}
pub fn run(
ecosystem: String,
package: String,
version: String,
justification: String,
proposed_by: Option<String>,
write_to: Option<PathBuf>,
force: bool,
) -> Result<()> {
require_nonblank(
&ecosystem,
"ecosystem",
"a risk-acceptance record with no ecosystem cannot be disambiguated across package registries \
(a `requests` package exists in both PyPI and RubyGems)",
)?;
require_nonblank(
&package,
"package",
"a risk-acceptance record with no package name cannot be matched against any package in CDP",
)?;
require_nonblank(
&version,
"version",
"an empty version range cannot be matched against any package version in CDP",
)?;
require_nonblank(
&justification,
"justification",
"the justification is the security rationale for the exception; an empty one creates a useless audit trail",
)?;
let rule = risk_acceptance::Rule {
ecosystem,
package,
version_range: version,
justification,
proposed_by,
proposed_at: chrono::Utc::now(),
};
let yaml = risk_acceptance::emit_yaml(&rule);
match write_to {
Some(path) => {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
anyhow::bail!(
"directory does not exist: {} — create it first, or choose an existing directory",
parent.display()
);
}
}
if path.exists() && !force {
backup_existing(&path)?;
}
std::fs::write(&path, &yaml)?;
eprintln!("wrote {} ({} bytes)", path.display(), yaml.len());
}
None => print!("{yaml}"),
}
Ok(())
}
fn backup_existing(target: &Path) -> Result<()> {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let backup_path = target.with_extension(format!(
"{}cleanlib-backup-{}",
target
.extension()
.map(|e| format!("{}.", e.to_string_lossy()))
.unwrap_or_default(),
ts
));
std::fs::copy(target, &backup_path)?;
eprintln!(
"backed up existing {} → {}",
target.display(),
backup_path.display()
);
Ok(())
}