use aube_registry::config::{NpmConfig, normalize_registry_url_pub, registry_uri_key_pub};
use miette::{Context, IntoDiagnostic, miette};
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn registry_host_key(url: &str) -> String {
registry_uri_key_pub(url)
}
pub struct NpmrcEdit {
lines: Vec<Line>,
}
enum Line {
Raw(String),
Entry { key: String, value: String },
}
impl NpmrcEdit {
pub fn load(path: &Path) -> miette::Result<Self> {
let content = if path.exists() {
std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", path.display()))?
} else {
String::new()
};
let mut lines = Vec::new();
for line in content.lines() {
let trimmed = line.trim_start();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
lines.push(Line::Raw(line.to_string()));
continue;
}
if let Some((k, v)) = line.split_once('=') {
lines.push(Line::Entry {
key: k.trim().to_string(),
value: v.trim().to_string(),
});
} else {
lines.push(Line::Raw(line.to_string()));
}
}
Ok(Self { lines })
}
pub fn set(&mut self, key: &str, value: &str) {
self.lines.retain(|line| match line {
Line::Entry { key: k, .. } => k != key,
Line::Raw(_) => true,
});
self.lines.push(Line::Entry {
key: key.to_string(),
value: value.to_string(),
});
}
pub fn entries(&self) -> Vec<(String, String)> {
self.lines
.iter()
.filter_map(|line| match line {
Line::Entry { key, value } => Some((key.clone(), value.clone())),
Line::Raw(_) => None,
})
.collect()
}
pub fn remove(&mut self, key: &str) -> bool {
let before = self.lines.len();
self.lines.retain(|line| match line {
Line::Entry { key: k, .. } => k != key,
Line::Raw(_) => true,
});
before != self.lines.len()
}
pub fn save(&self, path: &Path) -> miette::Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create {}", parent.display()))?;
let mut out = String::new();
for line in &self.lines {
match line {
Line::Raw(s) => out.push_str(s),
Line::Entry { key, value } => {
out.push_str(key);
out.push('=');
out.push_str(value);
}
}
out.push('\n');
}
let mut tmp = tempfile::NamedTempFile::new_in(parent)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create temp file in {}", parent.display()))?;
tmp.write_all(out.as_bytes())
.into_diagnostic()
.wrap_err("failed to write temp npmrc")?;
tmp.persist(path)
.map_err(|e| miette!("failed to persist {}: {}", path.display(), e.error))?;
Ok(())
}
}
pub fn resolve_registry(flag: Option<&str>, scope: Option<&str>) -> miette::Result<String> {
if let Some(r) = flag {
return Ok(normalize_registry_url_pub(r));
}
let cwd = crate::dirs::project_root_or_cwd().unwrap_or_else(|_| std::path::PathBuf::from("."));
let config = NpmConfig::load(&cwd);
if let Some(scope) = scope
&& let Some(url) = config.scoped_registries.get(scope)
{
return Ok(url.clone());
}
Ok(config.registry)
}
pub fn user_npmrc_path() -> miette::Result<PathBuf> {
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.ok_or_else(|| miette!("$HOME is not set; can't locate ~/.npmrc"))?;
Ok(home.join(".npmrc"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_replaces_existing_and_preserves_comments() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".npmrc");
std::fs::write(
&path,
"# top comment\n\
registry=https://registry.npmjs.org/\n\
//registry.npmjs.org/:_authToken=old\n\
; trailing\n",
)
.unwrap();
let mut edit = NpmrcEdit::load(&path).unwrap();
edit.set("//registry.npmjs.org/:_authToken", "new");
edit.save(&path).unwrap();
let after = std::fs::read_to_string(&path).unwrap();
assert!(after.contains("# top comment"));
assert!(after.contains("registry=https://registry.npmjs.org/"));
assert!(after.contains("//registry.npmjs.org/:_authToken=new"));
assert!(!after.contains("=old"));
assert!(after.contains("; trailing"));
}
#[test]
fn set_appends_when_absent() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".npmrc");
std::fs::write(&path, "registry=https://r.example.com/\n").unwrap();
let mut edit = NpmrcEdit::load(&path).unwrap();
edit.set("//r.example.com/:_authToken", "tok");
edit.save(&path).unwrap();
let after = std::fs::read_to_string(&path).unwrap();
assert!(after.contains("//r.example.com/:_authToken=tok"));
}
#[test]
fn remove_drops_matching_line() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".npmrc");
std::fs::write(
&path,
"registry=https://r.example.com/\n\
//r.example.com/:_authToken=tok\n",
)
.unwrap();
let mut edit = NpmrcEdit::load(&path).unwrap();
assert!(edit.remove("//r.example.com/:_authToken"));
edit.save(&path).unwrap();
let after = std::fs::read_to_string(&path).unwrap();
assert!(after.contains("registry=https://r.example.com/"));
assert!(!after.contains("_authToken"));
}
#[test]
fn remove_missing_key_is_false() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".npmrc");
std::fs::write(&path, "registry=https://r.example.com/\n").unwrap();
let mut edit = NpmrcEdit::load(&path).unwrap();
assert!(!edit.remove("//r.example.com/:_authToken"));
}
#[test]
fn load_nonexistent_is_empty() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing.npmrc");
let mut edit = NpmrcEdit::load(&path).unwrap();
edit.set("foo", "bar");
edit.save(&path).unwrap();
let after = std::fs::read_to_string(&path).unwrap();
assert_eq!(after, "foo=bar\n");
}
#[test]
fn registry_host_key_strips_scheme() {
assert_eq!(
registry_host_key("https://registry.npmjs.org/"),
"//registry.npmjs.org/"
);
assert_eq!(
registry_host_key("http://localhost:4873/"),
"//localhost:4873/"
);
}
}