use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Config {
pub host: String,
pub username: String,
pub token: String,
}
impl Config {
pub fn default_path() -> Result<PathBuf> {
if let Ok(dir) = std::env::var("RUNTIME_CONFIG_HOME") {
return Ok(PathBuf::from(dir).join("config.json"));
}
let base = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.ok()
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.ok_or_else(|| {
anyhow!("could not resolve config dir: set $HOME or $XDG_CONFIG_HOME")
})?;
Ok(base.join("runtime").join("config.json"))
}
pub fn load() -> Result<Self> {
let path = Self::default_path()?;
Self::load_from(&path)
}
pub fn load_from(path: &Path) -> Result<Self> {
let bytes = fs::read(path).with_context(|| {
format!(
"no Runtime config at {} — run `runtime auth login` first",
path.display()
)
})?;
let cfg: Config = serde_json::from_slice(&bytes)
.with_context(|| format!("invalid config JSON at {}", path.display()))?;
Ok(cfg)
}
pub fn save(&self) -> Result<PathBuf> {
let path = Self::default_path()?;
self.save_to(&path)?;
Ok(path)
}
pub fn save_to(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
}
let json = serde_json::to_vec_pretty(self)?;
fs::write(path, &json).with_context(|| format!("write {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(path, perms)?;
}
Ok(())
}
pub fn delete() -> Result<bool> {
let path = Self::default_path()?;
match fs::remove_file(&path) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e).with_context(|| format!("delete {}", path.display())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn save_then_load_round_trips() {
let tmp = tempfile::tempdir().expect("tmp");
let path = tmp.path().join("config.json");
let cfg = Config {
host: "https://runtime.example".into(),
username: "alice".into(),
token: "pat_xyz".into(),
};
cfg.save_to(&path).expect("save");
let loaded = Config::load_from(&path).expect("load");
assert_eq!(loaded, cfg);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
}
}
#[test]
fn load_missing_returns_helpful_error() {
let tmp = tempfile::tempdir().expect("tmp");
let path = tmp.path().join("nope.json");
let err = Config::load_from(&path).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("runtime auth login"), "{msg}");
}
}