Skip to main content

vibe_graph_ops/
config.rs

1//! Configuration for the operations layer.
2
3use std::path::PathBuf;
4
5use directories::ProjectDirs;
6use serde::{Deserialize, Serialize};
7
8use crate::error::{OpsError, OpsResult};
9
10/// Configuration for vibe-graph operations.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Config {
13    /// Maximum file size to include content (in KB).
14    #[serde(default = "default_max_content_size_kb")]
15    pub max_content_size_kb: u64,
16
17    /// GitHub username for API authentication.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub github_username: Option<String>,
20
21    /// GitHub token for API authentication.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub github_token: Option<String>,
24
25    /// Global cache directory for cloned repositories.
26    #[serde(default = "default_cache_dir")]
27    pub cache_dir: PathBuf,
28}
29
30fn default_max_content_size_kb() -> u64 {
31    100 // 100KB default
32}
33
34fn default_cache_dir() -> PathBuf {
35    ProjectDirs::from("com", "pinsky-three", "vibe-graph")
36        .map(|dirs| dirs.cache_dir().to_path_buf())
37        .unwrap_or_else(|| PathBuf::from(".vibe-graph-cache"))
38}
39
40impl Default for Config {
41    fn default() -> Self {
42        Self {
43            max_content_size_kb: default_max_content_size_kb(),
44            github_username: std::env::var("GITHUB_USERNAME").ok(),
45            github_token: std::env::var("GITHUB_TOKEN").ok(),
46            cache_dir: default_cache_dir(),
47        }
48    }
49}
50
51impl Config {
52    /// Load configuration from disk with environment overrides.
53    pub fn load() -> OpsResult<Self> {
54        // Try to load from config file
55        let config = if let Some(path) = Self::config_file_path() {
56            if path.exists() {
57                let contents = std::fs::read_to_string(&path)?;
58                serde_json::from_str(&contents)?
59            } else {
60                Self::default()
61            }
62        } else {
63            Self::default()
64        };
65
66        // Override with environment variables
67        let config = Self {
68            github_username: std::env::var("GITHUB_USERNAME")
69                .ok()
70                .or(config.github_username),
71            github_token: std::env::var("GITHUB_TOKEN").ok().or(config.github_token),
72            ..config
73        };
74
75        Ok(config)
76    }
77
78    /// Save configuration to disk.
79    pub fn save(&self) -> OpsResult<()> {
80        if let Some(path) = Self::config_file_path() {
81            if let Some(parent) = path.parent() {
82                std::fs::create_dir_all(parent)?;
83            }
84            let contents = serde_json::to_string_pretty(self)?;
85            std::fs::write(&path, contents)?;
86        }
87        Ok(())
88    }
89
90    /// Get the path to the configuration file.
91    pub fn config_file_path() -> Option<PathBuf> {
92        ProjectDirs::from("com", "pinsky-three", "vibe-graph")
93            .map(|dirs| dirs.config_dir().join("config.json"))
94    }
95
96    /// Check if GitHub credentials are configured.
97    pub fn has_github(&self) -> bool {
98        self.github_username.is_some() && self.github_token.is_some()
99    }
100
101    /// Validate that GitHub credentials are available.
102    pub fn validate_github(&self) -> OpsResult<()> {
103        if !self.has_github() {
104            return Err(OpsError::GitHubNotConfigured);
105        }
106        Ok(())
107    }
108
109    /// Get the cache directory for a specific GitHub organization.
110    pub fn org_cache_dir(&self, org: &str) -> PathBuf {
111        self.cache_dir.join(org)
112    }
113
114    /// Get a configuration value by key.
115    pub fn get(&self, key: &str) -> Option<String> {
116        match key {
117            "max_content_size_kb" => Some(self.max_content_size_kb.to_string()),
118            "github_username" => self.github_username.clone(),
119            "github_token" => self.github_token.as_ref().map(|_| "***".to_string()),
120            "cache_dir" => Some(self.cache_dir.display().to_string()),
121            _ => None,
122        }
123    }
124
125    /// Set a configuration value by key.
126    pub fn set(&mut self, key: &str, value: &str) -> OpsResult<()> {
127        match key {
128            "max_content_size_kb" => {
129                self.max_content_size_kb = value
130                    .parse()
131                    .map_err(|_| OpsError::Config(format!("Invalid number: {}", value)))?;
132            }
133            "github_username" => {
134                self.github_username = Some(value.to_string());
135            }
136            "github_token" => {
137                self.github_token = Some(value.to_string());
138            }
139            "cache_dir" => {
140                self.cache_dir = PathBuf::from(value);
141            }
142            _ => {
143                return Err(OpsError::Config(format!("Unknown config key: {}", key)));
144            }
145        }
146        Ok(())
147    }
148}