kai-tool 0.1.26

CLI helpers for AI coding, Codex credentials, and git worktree management.
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};

#[derive(Debug, Clone)]
pub struct RuntimePaths {
    pub credentials_home: PathBuf,
    pub codex_home: PathBuf,
    active_auth: Option<PathBuf>,
}

impl RuntimePaths {
    pub fn from_env() -> Result<Self> {
        let home = capulus::paths::home_dir()
            .context("could not determine the current user's home directory")?;
        Self::new(
            env_path("KAI_CREDENTIALS_HOME")
                .unwrap_or_else(|| home.join(".kai").join("credentials")),
            env_path("CODEX_HOME").unwrap_or_else(|| home.join(".codex")),
        )
    }

    pub fn new(credentials_home: PathBuf, codex_home: PathBuf) -> Result<Self> {
        Ok(Self {
            credentials_home: absolute(&credentials_home)
                .context("could not resolve KAI_CREDENTIALS_HOME")?,
            codex_home: absolute(&codex_home).context("could not resolve CODEX_HOME")?,
            active_auth: None,
        })
    }

    pub fn active_auth(&self) -> PathBuf {
        self.active_auth
            .clone()
            .unwrap_or_else(|| self.codex_home.join("auth.json"))
    }

    pub fn codex_config(&self) -> PathBuf {
        self.codex_home.join("config.toml")
    }

    pub fn read_codex_config(&self) -> Result<Option<toml::Table>> {
        let path = self.codex_config();
        let contents = match fs::read_to_string(&path) {
            Ok(contents) => contents,
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(err) => {
                return Err(err).with_context(|| format!("could not read {}", path.display()));
            }
        };
        toml::from_str(&contents)
            .with_context(|| format!("could not parse {}", path.display()))
            .map(Some)
    }

    pub fn profiles_dir(&self) -> PathBuf {
        self.credentials_home.join("profiles")
    }

    pub fn state_file(&self) -> PathBuf {
        self.credentials_home.join("state.json")
    }
}

fn env_path(name: &str) -> Option<PathBuf> {
    env::var_os(name)
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
}

fn absolute(path: &Path) -> Result<PathBuf> {
    if path.as_os_str().is_empty() {
        bail!("path cannot be empty");
    }
    std::path::absolute(path).map_err(Into::into)
}