Skip to main content

moltbook_cli/
config.rs

1//! Configuration management for the Moltbook CLI.
2//!
3//! This module handles loading and saving the agent's credentials (API key and agent name)
4//! to a local configuration file, typically located at `~/.config/moltbook/credentials.json`.
5//! It also enforces secure file permissions (0600) on Unix-like systems.
6
7use crate::api::error::ApiError;
8use dirs::home_dir;
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::PathBuf;
12
13/// The default configuration directory relative to the user's home.
14const CONFIG_DIR: &str = ".config/moltbook";
15/// The filename for storing agent credentials.
16const CONFIG_FILE: &str = "credentials.json";
17
18/// Represents the CLI configuration and credentials.
19#[derive(Serialize, Deserialize, Debug)]
20pub struct Config {
21    /// The Moltbook API key used for authentication.
22    pub api_key: String,
23    /// The name of the AI agent associated with this key.
24    pub agent_name: String,
25}
26
27impl Config {
28    /// Loads the configuration from the disk.
29    ///
30    /// # Errors
31    ///
32    /// Returns an `ApiError::ConfigError` if:
33    /// - The configuration file does not exist.
34    /// - The file cannot be read or parsed as valid JSON.
35    pub fn load() -> Result<Self, ApiError> {
36        let config_path = Self::get_config_path()?;
37
38        if !config_path.exists() {
39            return Err(ApiError::ConfigError(format!(
40                "Config file not found at: {}\nPlease create it with your API key.",
41                config_path.display()
42            )));
43        }
44
45        let content = fs::read_to_string(&config_path)
46            .map_err(|e| ApiError::ConfigError(format!("Failed to read config: {}", e)))?;
47
48        let config: Config = serde_json::from_str(&content)
49            .map_err(|e| ApiError::ConfigError(format!("Failed to parse config: {}", e)))?;
50
51        Ok(config)
52    }
53
54    /// Resolves the path to the configuration file.
55    ///
56    /// Priority:
57    /// 1. `MOLTBOOK_CONFIG_DIR` environment variable.
58    /// 2. Default `~/.config/moltbook/credentials.json` path.
59    fn get_config_path() -> Result<PathBuf, ApiError> {
60        if let Ok(config_dir) = std::env::var("MOLTBOOK_CONFIG_DIR") {
61            return Ok(PathBuf::from(config_dir).join(CONFIG_FILE));
62        }
63
64        let home = home_dir().ok_or_else(|| {
65            ApiError::ConfigError("Could not determine home directory".to_string())
66        })?;
67
68        Ok(home.join(CONFIG_DIR).join(CONFIG_FILE))
69    }
70
71    /// Saves the current configuration to disk.
72    ///
73    /// On Unix systems, this method strictly enforces `0600` permissions
74    /// to protect the API key from unauthorized local access.
75    pub fn save(&self) -> Result<(), ApiError> {
76        let config_path = Self::get_config_path()?;
77        let config_dir = config_path.parent().unwrap();
78
79        if !config_dir.exists() {
80            fs::create_dir_all(config_dir).map_err(|e| {
81                ApiError::ConfigError(format!("Failed to create config dir: {}", e))
82            })?;
83        }
84
85        let content = serde_json::to_string_pretty(self)
86            .map_err(|e| ApiError::ConfigError(format!("Failed to serialize config: {}", e)))?;
87
88        fs::write(&config_path, content)
89            .map_err(|e| ApiError::ConfigError(format!("Failed to write config: {}", e)))?;
90
91        #[cfg(unix)]
92        {
93            use std::os::unix::fs::PermissionsExt;
94            let mut perms = fs::metadata(&config_path)
95                .map_err(|e| ApiError::ConfigError(format!("Failed to get metadata: {}", e)))?
96                .permissions();
97            perms.set_mode(0o600);
98            fs::set_permissions(&config_path, perms)
99                .map_err(|e| ApiError::ConfigError(format!("Failed to set permissions: {}", e)))?;
100        }
101
102        Ok(())
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_config_deserialization() {
112        let json = r#"{"api_key": "test_key", "agent_name": "test_agent"}"#;
113        let config: Config = serde_json::from_str(json).unwrap();
114        assert_eq!(config.api_key, "test_key");
115        assert_eq!(config.agent_name, "test_agent");
116    }
117
118    #[test]
119    fn test_missing_fields() {
120        let json = r#"{"api_key": "test_key"}"#;
121        let result: Result<Config, _> = serde_json::from_str(json);
122        assert!(result.is_err());
123    }
124}