Skip to main content

harmont_cli/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5const DEFAULT_API_URL: &str = "https://api.harmont.dev";
6
7/// Resolve the Harmont config dir (`~/.harmont/`).
8///
9/// # Errors
10///
11/// Returns an error if the user's home directory cannot be determined
12/// (the `dirs` crate's platform-specific lookup fails — typically only
13/// happens in restrictive sandboxes with no `HOME` / passwd entry).
14pub fn user_config_dir() -> Result<PathBuf> {
15    let home = dirs::home_dir().context("could not determine home directory")?;
16    Ok(home.join(".harmont"))
17}
18
19/// User preferences stored alongside the config.
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct Preferences {
22    /// Default output format ("human" or "json").
23    pub format: Option<String>,
24    /// Whether `hm build create` should auto-watch.
25    pub auto_watch: Option<bool>,
26}
27
28/// Persistent CLI configuration at `~/.harmont/config.toml`.
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct Config {
31    /// Base URL for the Harmont API.
32    pub api_url: Option<String>,
33    /// Currently active organization slug.
34    pub org: Option<String>,
35    /// User preferences.
36    #[serde(default)]
37    pub preferences: Preferences,
38}
39
40impl Config {
41    /// Returns the path to the config file (`~/.harmont/config.toml`).
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if [`user_config_dir`] fails (no home directory
46    /// available).
47    pub fn path() -> Result<PathBuf> {
48        Ok(user_config_dir()?.join("config.toml"))
49    }
50
51    /// Load configuration from disk, returning defaults if the file does not exist.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the config path cannot be resolved, the file
56    /// exists but cannot be read (permissions, I/O error), or the file
57    /// contents are not valid TOML matching the `Config` shape.
58    pub fn load() -> Result<Self> {
59        let path = Self::path()?;
60        if !path.exists() {
61            return Ok(Self::default());
62        }
63        let contents = std::fs::read_to_string(&path)
64            .with_context(|| format!("reading {}", path.display()))?;
65        let config: Self =
66            toml::from_str(&contents).with_context(|| format!("parsing {}", path.display()))?;
67        Ok(config)
68    }
69
70    /// Persist configuration to disk atomically, with the config directory
71    /// (`~/.harmont/`) restricted to 0o700 so adjacent credential
72    /// files are not exposed.
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the config path cannot be resolved, the
77    /// `Config` cannot be serialized to TOML (only happens for
78    /// non-string map keys, which `Config` does not have), or the
79    /// atomic write fails (out-of-space, permission denied, parent
80    /// directory cannot be created).
81    pub fn save(&self) -> Result<()> {
82        let path = Self::path()?;
83        let serialized = toml::to_string_pretty(self).context("serializing config")?;
84        crate::fs_util::write_atomic_restricted(&path, serialized.as_bytes(), 0o644, 0o700)
85            .with_context(|| format!("writing {}", path.display()))?;
86        Ok(())
87    }
88
89    /// Effective API URL (config value or default).
90    #[must_use]
91    pub fn api_url(&self) -> &str {
92        self.api_url.as_deref().unwrap_or(DEFAULT_API_URL)
93    }
94}