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}