zeroski 0.0.1

zero.ski CLI — @-protocol dispatch, streaming chat, and trace feed for the Zero runtime
//! Config resolution. Precedence: CLI flag > env var > ~/.zeroski/config.toml > default.
//!
//! The config file is plain TOML with two optional fields:
//!
//! ```toml
//! base = "https://zero.ski"
//! key  = "zk_live_..."       # API key minted from /api/keys
//! ```
//!
//! Env vars mirror the fields: `ZEROSKI_BASE`, `ZEROSKI_KEY`. A missing key
//! is fine — public read-only endpoints work without it; authed endpoints
//! will return a clear error from the server.

use std::{env, fs, path::PathBuf};

use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::Deserialize;

pub const DEFAULT_BASE: &str = "https://zero.ski";
pub const USER_AGENT: &str = concat!("zeroski/", env!("CARGO_PKG_VERSION"));

#[derive(Debug, Clone)]
pub struct Config {
    pub base: String,
    pub key: Option<String>,
}

#[derive(Debug, Default, Deserialize)]
struct FileConfig {
    base: Option<String>,
    key: Option<String>,
}

impl Config {
    pub fn resolve(base_flag: Option<String>, key_flag: Option<String>) -> Result<Self> {
        let file = load_file_config().unwrap_or_default();

        let base = base_flag
            .or_else(|| env::var("ZEROSKI_BASE").ok())
            .or(file.base)
            .unwrap_or_else(|| DEFAULT_BASE.to_string())
            .trim_end_matches('/')
            .to_string();

        let key = key_flag
            .or_else(|| env::var("ZEROSKI_KEY").ok())
            .or(file.key);

        Ok(Self { base, key })
    }

    pub fn config_path() -> Option<PathBuf> {
        ProjectDirs::from("ski", "zero", "zeroski")
            .map(|p| p.config_dir().join("config.toml"))
    }
}

fn load_file_config() -> Result<FileConfig> {
    let Some(path) = Config::config_path() else {
        return Ok(FileConfig::default());
    };
    if !path.exists() {
        return Ok(FileConfig::default());
    }
    let text = fs::read_to_string(&path)
        .with_context(|| format!("reading {}", path.display()))?;
    let parsed: FileConfig = toml::from_str(&text)
        .with_context(|| format!("parsing {}", path.display()))?;
    Ok(parsed)
}