use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Result;
use cleanlib_client::{config, proxy};
fn dedup_preserving_order(input: &[String]) -> Vec<String> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut out: Vec<String> = Vec::with_capacity(input.len());
for s in input {
let trimmed = s.trim().to_string();
if trimmed.is_empty() {
continue;
}
if seen.insert(trimmed.clone()) {
out.push(trimmed);
}
}
out
}
pub fn run(
ecosystems: Vec<String>,
scope: Option<String>,
write: bool,
write_to: Option<PathBuf>,
inline_token: bool,
force: bool,
) -> Result<()> {
let path = config::default_path();
let cfg = config::load_with_env_overrides(path.as_deref())?;
if inline_token {
let has_usable_key = cfg
.auth
.api_key
.as_deref()
.map(|k| !k.trim().is_empty())
.unwrap_or(false);
if !has_usable_key {
anyhow::bail!(
"CLIENT_INLINE_TOKEN_NO_KEY — `--inline-token` requires a non-empty API key.\n\
Run `cleanlib login --api-key <KEY>` first, set CLEANLIB_ENRICH_BEARER in your\n\
environment, or drop `--inline-token` to emit a `${{CLEANLIBRARY_API_KEY}}`\n\
placeholder instead (resolves at runtime from the env tier)."
);
}
}
let opts = proxy::EmitOptions {
endpoint: cfg.endpoint.url.clone(),
scope,
inline_token,
api_key: cfg.auth.api_key.clone(),
};
let write_mode = write || write_to.is_some();
let ecosystems = dedup_preserving_order(&ecosystems);
let mut prepared: Vec<(proxy::Ecosystem, proxy::ProxyConfig)> = Vec::with_capacity(ecosystems.len());
for eco_str in &ecosystems {
let eco = proxy::Ecosystem::parse(eco_str).ok_or_else(|| {
anyhow::anyhow!(
"unknown ecosystem: {} (locked Phase 1 Tier A vocab: npm | pypi | go per matrix §8)",
eco_str
)
})?;
let proxy_cfg = proxy::emit(eco, &opts)?;
prepared.push((eco, proxy_cfg));
}
for (eco, proxy_cfg) in prepared {
if !write_mode {
println!(
"# === {} ({}) ===",
eco.as_str(),
if proxy_cfg.canonical_location.as_os_str().is_empty() {
"no canonical config file; shell-snippet form".to_string()
} else {
proxy_cfg.canonical_location.display().to_string()
}
);
print!("{}", proxy_cfg.config_blob);
println!();
continue;
}
let target: PathBuf =
match (&write_to, proxy_cfg.canonical_location.as_os_str().is_empty()) {
(Some(p), _) => p.clone(),
(None, true) => {
eprintln!(
"# {}: no canonical config file; printing shell-snippet to stdout",
eco.as_str()
);
print!("{}", proxy_cfg.config_blob);
continue;
}
(None, false) => proxy_cfg.canonical_location.clone(),
};
write_with_backup(&target, &proxy_cfg.config_blob, force)?;
eprintln!("wrote {} ({} bytes)", target.display(), proxy_cfg.config_blob.len());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dedup_strips_consecutive_duplicates() {
let input = vec!["npm".to_string(), "npm".to_string()];
assert_eq!(dedup_preserving_order(&input), vec!["npm".to_string()]);
}
#[test]
fn dedup_strips_interleaved_duplicates() {
let input = vec![
"npm".to_string(),
"pypi".to_string(),
"npm".to_string(),
"go".to_string(),
"pypi".to_string(),
];
assert_eq!(
dedup_preserving_order(&input),
vec!["npm".to_string(), "pypi".to_string(), "go".to_string()]
);
}
#[test]
fn dedup_preserves_first_occurrence_order() {
let input = vec!["go".to_string(), "npm".to_string(), "pypi".to_string()];
assert_eq!(
dedup_preserving_order(&input),
vec!["go".to_string(), "npm".to_string(), "pypi".to_string()]
);
}
#[test]
fn dedup_trims_whitespace_and_drops_empty() {
let input = vec![
"npm".to_string(),
" pypi ".to_string(),
"".to_string(),
" ".to_string(),
"pypi".to_string(),
];
assert_eq!(
dedup_preserving_order(&input),
vec!["npm".to_string(), "pypi".to_string()]
);
}
#[test]
fn dedup_empty_input_yields_empty() {
let input: Vec<String> = Vec::new();
assert!(dedup_preserving_order(&input).is_empty());
}
}
pub fn write_with_backup(target: &Path, blob: &str, force: bool) -> Result<()> {
if let Some(parent) = target.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
if target.exists() && !force {
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()
);
}
std::fs::write(target, blob)?;
Ok(())
}