//! CLI view over the unified Maestro config at `~/.config/maestro/config.toml`. //! //! Reads `[shared].registry_url` (registry HTTP base) and `[cli].cookie` //! (persisted `maestro_session` JWT). All other sections in the file are //! preserved on write. use std::path::{Path, PathBuf}; use maestro_config::MaestroConfigFile; pub use maestro_config::CONFIG_PATH_ENV; /// `~/.config/maestro/config.toml` (XDG-aware). pub fn default_config_path() -> Result { Ok(maestro_config::default_config_path()?) } /// `$MAESTRO_CONFIG` if set, else [`default_config_path`]. pub fn resolve_config_path() -> Result { Ok(maestro_config::resolve_config_path()?) } #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error(transparent)] Inner(#[from] maestro_config::ConfigError), #[error( "no registry URL configured — pass --registry-url, set \ MAESTRO_REGISTRY_URL, or run `maestro set-registry `" )] NoRegistryUrl, } /// Resolved CLI config — the subset of the unified file that this binary /// actually consumes. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CliConfig { pub registry_url: Option, pub cookie: Option, } impl CliConfig { /// Load `[shared].registry_url` + `[cli].cookie` from the unified file. /// Returns an empty config if the file is missing — the CLI tolerates a /// missing file so `maestro set-registry ` can bootstrap it. pub fn load(path: &Path) -> Result { let file = MaestroConfigFile::load_or_default(path)?; Ok(Self { registry_url: file.shared.registry_url, cookie: file.cli.cookie, }) } /// Persist `[shared].registry_url`. Read-modify-write — preserves /// any other sections already in the file (`[daemon]`, `[registry]`, /// existing `[cli].cookie`). pub fn save_registry_url(path: &Path, url: &str) -> Result<(), ConfigError> { let mut file = MaestroConfigFile::load_or_default(path)?; file.shared.registry_url = Some(url.to_string()); file.save(path)?; Ok(()) } /// Persist `[cli].cookie`. Same read-modify-write semantics as /// [`save_registry_url`]; in particular this does not touch /// `[shared].registry_url`. pub fn save_cookie(path: &Path, cookie: &str) -> Result<(), ConfigError> { let mut file = MaestroConfigFile::load_or_default(path)?; file.cli.cookie = Some(cookie.to_string()); file.save(path)?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use maestro_config::SharedSection; fn write_file(path: &Path, body: &str) { std::fs::write(path, body).unwrap(); } #[test] fn missing_file_returns_empty() { let dir = tempfile::tempdir().unwrap(); let cfg = CliConfig::load(&dir.path().join("config.toml")).unwrap(); assert_eq!(cfg, CliConfig::default()); } #[test] fn load_reads_shared_registry_url_and_cli_cookie() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.toml"); write_file( &path, r#" [shared] registry_url = "https://r.example" [cli] cookie = "eyJ..." "#, ); let cfg = CliConfig::load(&path).unwrap(); assert_eq!(cfg.registry_url.as_deref(), Some("https://r.example")); assert_eq!(cfg.cookie.as_deref(), Some("eyJ...")); } #[test] fn save_registry_url_preserves_other_sections() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.toml"); write_file( &path, r#" [shared] token = "abc" [daemon] daemon_url = "ws://laptop:7070" "#, ); CliConfig::save_registry_url(&path, "https://new.example/").unwrap(); let after = MaestroConfigFile::load(&path).unwrap(); assert_eq!( after.shared.registry_url.as_deref(), Some("https://new.example/") ); assert_eq!(after.shared.token.as_deref(), Some("abc")); assert_eq!(after.daemon.daemon_url.as_deref(), Some("ws://laptop:7070")); } #[test] fn save_cookie_does_not_touch_registry_url() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.toml"); write_file( &path, r#" [shared] registry_url = "https://r.example" "#, ); CliConfig::save_cookie(&path, "eyJ.new").unwrap(); let after = MaestroConfigFile::load(&path).unwrap(); assert_eq!( after.shared.registry_url.as_deref(), Some("https://r.example") ); assert_eq!(after.cli.cookie.as_deref(), Some("eyJ.new")); } #[test] fn save_creates_file_if_missing() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("nested/config.toml"); CliConfig::save_registry_url(&path, "https://x").unwrap(); assert!(path.exists()); let after = MaestroConfigFile::load(&path).unwrap(); assert_eq!( after, MaestroConfigFile { shared: SharedSection { registry_url: Some("https://x".into()), token: None, }, ..Default::default() } ); } }