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