ref:main
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("not logged in — run `anvil auth login` first")]
NotLoggedIn,
#[error("config I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("config parse error: {0}")]
Parse(#[from] serde_json::Error),
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Config {
pub server_url: Option<String>,
pub token: Option<String>,
/// Default org/repo for commands (set via `anvil repo set-default`)
pub default_repo: Option<String>,
}
impl Config {
pub fn path() -> PathBuf {
let dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("anvil");
dir.join("config.json")
}
pub fn load() -> Result<Self, ConfigError> {
let path = Self::path();
let mut cfg: Self = if path.exists() {
let contents = std::fs::read_to_string(&path)?;
serde_json::from_str(&contents)?
} else {
Self::default()
};
// Env vars take precedence over the config file so CI can inject
// credentials without writing to disk.
if let Ok(url) = std::env::var("ANVIL_SERVER_URL") {
if !url.is_empty() {
cfg.server_url = Some(url);
}
}
if let Ok(token) = std::env::var("ANVIL_TOKEN") {
if !token.is_empty() {
cfg.token = Some(token);
}
}
Ok(cfg)
}
pub fn save(&self) -> Result<(), ConfigError> {
let path = Self::path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let contents = serde_json::to_string_pretty(self)?;
std::fs::write(&path, contents)?;
Ok(())
}
pub fn server_url(&self) -> Result<&str, ConfigError> {
self.server_url.as_deref().ok_or(ConfigError::NotLoggedIn)
}
pub fn token(&self) -> Result<&str, ConfigError> {
self.token.as_deref().ok_or(ConfigError::NotLoggedIn)
}
}
/// Resolve org/repo from explicit arg, --repo flag, or git remote.
pub fn resolve_repo(explicit: Option<&str>) -> Result<(String, String), ConfigError> {
// 1. Explicit argument
if let Some(r) = explicit {
return parse_org_repo(r);
}
// 2. Try git remote
if let Ok(remote) = detect_from_git_remote() {
return Ok(remote);
}
// 3. Fall back to config default
let cfg = Config::load()?;
if let Some(ref default) = cfg.default_repo {
return parse_org_repo(default);
}
Err(ConfigError::NotLoggedIn)
}
fn parse_org_repo(s: &str) -> Result<(String, String), ConfigError> {
let parts: Vec<&str> = s.splitn(2, '/').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
return Err(ConfigError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid repo format '{s}' — expected 'org/repo'"),
)));
}
Ok((parts[0].to_string(), parts[1].to_string()))
}
/// Try to detect org/repo from git remote origin URL.
fn detect_from_git_remote() -> Result<(String, String), ConfigError> {
let output = std::process::Command::new("git")
.args(["remote", "get-url", "origin"])
.output()
.map_err(ConfigError::Io)?;
if !output.status.success() {
return Err(ConfigError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no git remote found",
)));
}
let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
parse_remote_url(&url)
}
fn parse_remote_url(url: &str) -> Result<(String, String), ConfigError> {
// SSH: git@host:org/repo.git or ssh://git@host/org/repo.git
// HTTPS: https://host/org/repo.git
let path = if let Some(colon_idx) = url.find(':') {
if url.starts_with("ssh://") || url.starts_with("http://") || url.starts_with("https://") {
// URL format — extract path after host
url::Url::parse(url)
.ok()
.and_then(|u| {
let p = u.path().trim_start_matches('/').to_string();
if p.is_empty() {
None
} else {
Some(p)
}
})
.ok_or_else(|| {
ConfigError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("cannot parse remote URL: {url}"),
))
})?
} else {
// SCP-style: git@host:org/repo.git
url[colon_idx + 1..].to_string()
}
} else {
return Err(ConfigError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("cannot parse remote URL: {url}"),
)));
};
let path = path.trim_end_matches(".git");
parse_org_repo(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_remote_urls() {
assert_eq!(
parse_remote_url("git@anvil.fangorn.io:chaos/myrepo.git").unwrap(),
("chaos".into(), "myrepo".into())
);
assert_eq!(
parse_remote_url("https://anvil.fangorn.io/chaos/myrepo.git").unwrap(),
("chaos".into(), "myrepo".into())
);
assert_eq!(
parse_remote_url("ssh://git@anvil.fangorn.io/chaos/myrepo").unwrap(),
("chaos".into(), "myrepo".into())
);
}
}