1use std::path::PathBuf;
4
5use directories::ProjectDirs;
6use serde::{Deserialize, Serialize};
7
8use crate::error::{OpsError, OpsResult};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Config {
13 #[serde(default = "default_max_content_size_kb")]
15 pub max_content_size_kb: u64,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub github_username: Option<String>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub github_token: Option<String>,
24
25 #[serde(default = "default_cache_dir")]
27 pub cache_dir: PathBuf,
28}
29
30fn default_max_content_size_kb() -> u64 {
31 100 }
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 pub fn load() -> OpsResult<Self> {
54 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 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 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 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 pub fn has_github(&self) -> bool {
98 self.github_username.is_some() && self.github_token.is_some()
99 }
100
101 pub fn validate_github(&self) -> OpsResult<()> {
103 if !self.has_github() {
104 return Err(OpsError::GitHubNotConfigured);
105 }
106 Ok(())
107 }
108
109 pub fn org_cache_dir(&self, org: &str) -> PathBuf {
111 self.cache_dir.join(org)
112 }
113
114 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 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}