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}