opencode_cloud_core/config/
mod.rs1pub mod paths;
7pub mod schema;
8
9use std::fs::{self, File};
10use std::io::{Read, Write};
11use std::path::PathBuf;
12
13use anyhow::{Context, Result};
14use jsonc_parser::parse_to_serde_value;
15
16pub use paths::{get_config_dir, get_config_path, get_data_dir, get_pid_path};
17pub use schema::Config;
18
19pub fn ensure_config_dir() -> Result<PathBuf> {
24 let config_dir =
25 get_config_dir().ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
26
27 if !config_dir.exists() {
28 fs::create_dir_all(&config_dir).with_context(|| {
29 format!(
30 "Failed to create config directory: {}",
31 config_dir.display()
32 )
33 })?;
34 tracing::info!("Created config directory: {}", config_dir.display());
35 }
36
37 Ok(config_dir)
38}
39
40pub fn ensure_data_dir() -> Result<PathBuf> {
45 let data_dir =
46 get_data_dir().ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))?;
47
48 if !data_dir.exists() {
49 fs::create_dir_all(&data_dir)
50 .with_context(|| format!("Failed to create data directory: {}", data_dir.display()))?;
51 tracing::info!("Created data directory: {}", data_dir.display());
52 }
53
54 Ok(data_dir)
55}
56
57pub fn load_config() -> Result<Config> {
63 let config_path =
64 get_config_path().ok_or_else(|| anyhow::anyhow!("Could not determine config file path"))?;
65
66 if !config_path.exists() {
67 tracing::info!(
69 "Config file not found, creating default at: {}",
70 config_path.display()
71 );
72 let config = Config::default();
73 save_config(&config)?;
74 return Ok(config);
75 }
76
77 let mut file = File::open(&config_path)
79 .with_context(|| format!("Failed to open config file: {}", config_path.display()))?;
80
81 let mut contents = String::new();
82 file.read_to_string(&mut contents)
83 .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
84
85 let parsed_value = parse_to_serde_value(&contents, &Default::default())
87 .map_err(|e| anyhow::anyhow!("Invalid JSONC in config file: {}", e))?
88 .ok_or_else(|| anyhow::anyhow!("Config file is empty"))?;
89
90 let config: Config = serde_json::from_value(parsed_value).with_context(|| {
92 format!(
93 "Invalid configuration in {}. Check for unknown fields or invalid values.",
94 config_path.display()
95 )
96 })?;
97
98 Ok(config)
99}
100
101pub fn save_config(config: &Config) -> Result<()> {
106 ensure_config_dir()?;
107
108 let config_path =
109 get_config_path().ok_or_else(|| anyhow::anyhow!("Could not determine config file path"))?;
110
111 if config_path.exists() {
113 let backup_path = config_path.with_extension("json.bak");
114 fs::copy(&config_path, &backup_path)
115 .with_context(|| format!("Failed to create backup at: {}", backup_path.display()))?;
116 tracing::debug!("Created config backup: {}", backup_path.display());
117 }
118
119 let json = serde_json::to_string_pretty(config).context("Failed to serialize configuration")?;
121
122 let mut file = File::create(&config_path)
124 .with_context(|| format!("Failed to create config file: {}", config_path.display()))?;
125
126 file.write_all(json.as_bytes())
127 .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
128
129 tracing::debug!("Saved config to: {}", config_path.display());
130
131 Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_path_resolution_returns_values() {
140 assert!(get_config_dir().is_some());
142 assert!(get_data_dir().is_some());
143 assert!(get_config_path().is_some());
144 assert!(get_pid_path().is_some());
145 }
146
147 #[test]
148 fn test_paths_end_with_expected_names() {
149 let config_dir = get_config_dir().unwrap();
150 assert!(config_dir.ends_with("opencode-cloud"));
151
152 let data_dir = get_data_dir().unwrap();
153 assert!(data_dir.ends_with("opencode-cloud"));
154
155 let config_path = get_config_path().unwrap();
156 assert!(config_path.ends_with("config.json"));
157
158 let pid_path = get_pid_path().unwrap();
159 assert!(pid_path.ends_with("opencode-cloud.pid"));
160 }
161
162 }