cufflink-cli 0.8.33

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Debug, Deserialize)]
pub struct ProjectConfig {
    #[serde(default)]
    pub service: ServiceConfig,
    #[serde(default)]
    pub environments: HashMap<String, EnvironmentConfig>,
    /// Directory containing Cufflink.toml (set after loading, not from TOML)
    #[serde(skip)]
    pub project_dir: PathBuf,
}

#[derive(Debug, Deserialize, Default)]
pub struct ServiceConfig {
    /// Service name (required for web mode, optional for Rust services which extract from manifest)
    pub name: Option<String>,
    /// Service mode: "crud", "wasm", "web" (defaults to Rust build if not specified)
    pub mode: Option<String>,
    /// Default environment name when --env is not specified
    pub default_env: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct EnvironmentConfig {
    pub api_url: String,
    pub tenant: String,
    /// Name of the environment variable that holds the API key for this env
    pub api_key_env: Option<String>,
    /// Inline API key (for local dev only — don't commit to version control)
    pub api_key: Option<String>,
    /// Keycloak base URL for login (e.g., "https://auth.staging.starmoire.xyz")
    pub keycloak_url: Option<String>,
    /// Keycloak realm name (e.g., "starmoire")
    pub keycloak_realm: Option<String>,
    /// Keycloak client ID for device auth (defaults to "cufflink-cli")
    pub keycloak_client_id: Option<String>,
    /// Non-secret config key-value pairs to sync on deploy
    #[serde(default)]
    pub config: HashMap<String, String>,
    /// Secret config mappings: CONFIG_KEY = "securestore_secret_name"
    #[serde(default)]
    pub secrets: HashMap<String, String>,
}

impl ProjectConfig {
    /// Search for Cufflink.toml starting from cwd, walking up to filesystem root.
    pub fn find_and_load() -> eyre::Result<Option<Self>> {
        let cwd = std::env::current_dir()?;
        let mut dir = cwd.as_path();
        loop {
            let candidate = dir.join("Cufflink.toml");
            if candidate.exists() {
                let content = std::fs::read_to_string(&candidate)?;
                let mut config: ProjectConfig = toml::from_str(&content)
                    .map_err(|e| eyre::eyre!("Failed to parse {}: {}", candidate.display(), e))?;
                config.project_dir = dir.to_path_buf();
                return Ok(Some(config));
            }
            match dir.parent() {
                Some(parent) => dir = parent,
                None => return Ok(None),
            }
        }
    }

    pub fn get_env(&self, name: &str) -> eyre::Result<&EnvironmentConfig> {
        self.environments.get(name).ok_or_else(|| {
            let available: Vec<&str> = self.environments.keys().map(|k| k.as_str()).collect();
            eyre::eyre!(
                "Environment '{}' not found in Cufflink.toml. Available: {:?}",
                name,
                available
            )
        })
    }

    pub fn resolve_env(&self, env_arg: Option<&str>) -> eyre::Result<Option<&EnvironmentConfig>> {
        let env_name = env_arg
            .map(|s| s.to_string())
            .or_else(|| self.service.default_env.clone());
        match env_name {
            Some(name) => Ok(Some(self.get_env(&name)?)),
            None => Ok(None),
        }
    }
}