lobe-cli 0.1.4

Lobe — local-first HTTP performance profiling CLI for developers. Spins up a capture proxy, records DNS/TCP/TLS/TTFB/download phases per request, and flags anomalies against grounded network baselines.
//! Read + write `~/.config/lobe/config.toml`. Currently stores the CLI auth
//! token and the URLs the CLI talks to (web for `lobe login` prompts, api
//! for uploads). All fields are optional / defaulted so a first-run CLI
//! doesn't need any config file to function.

use std::fs;
use std::path::PathBuf;

use lobe_core::error::{Result, TloxError};
use serde::{Deserialize, Serialize};

/// The default web URL the CLI prints in the `lobe login` flow. Users
/// override with `--web-url` while developing against localhost.
pub const DEFAULT_WEB_URL: &str = "https://getlobe.dev";

/// The default Convex site URL where `lobe capture --upload` POSTs sessions.
/// Overridden with `--api-url` for local Convex or a different deployment.
pub const DEFAULT_API_URL: &str = "https://ceaseless-beagle-861.convex.site";

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Config {
    #[serde(default)]
    pub auth: Auth,
    #[serde(default)]
    pub urls: Urls,
}

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Auth {
    /// `lobe_<48hex>` token minted from `/cli-auth` in the web app.
    pub token: Option<String>,
    /// Email of the user this token belongs to. Just for display in
    /// `lobe whoami` / login success messages.
    pub email: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Urls {
    pub web: String,
    pub api: String,
}

impl Default for Urls {
    fn default() -> Self {
        Self {
            web: DEFAULT_WEB_URL.to_string(),
            api: DEFAULT_API_URL.to_string(),
        }
    }
}

/// Path to the config file. `~/.config/lobe/config.toml` on unix.
pub fn config_path() -> Result<PathBuf> {
    let home = home::home_dir().ok_or_else(|| {
        TloxError::Io(std::io::Error::other(
            "could not determine home directory",
        ))
    })?;
    Ok(home.join(".config").join("lobe").join("config.toml"))
}

/// Load the config, returning defaults if the file doesn't exist yet.
pub fn load() -> Result<Config> {
    let path = config_path()?;
    if !path.exists() {
        return Ok(Config::default());
    }
    let contents = fs::read_to_string(&path)?;
    toml::from_str(&contents)
        .map_err(|e| TloxError::Io(std::io::Error::other(format!("config parse error: {e}"))))
}

/// Persist the config to disk. Creates the parent directory if needed and
/// chmods the file to 600 (owner read/write only) on unix — because the
/// file contains a bearer token.
pub fn save(config: &Config) -> Result<()> {
    let path = config_path()?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let contents = toml::to_string_pretty(config)
        .map_err(|e| TloxError::Io(std::io::Error::other(format!("config write error: {e}"))))?;
    fs::write(&path, contents)?;

    #[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(())
}