1use std::env;
2use std::path::PathBuf;
3
4use figment::{
5 providers::{Env, Format, Serialized, Toml},
6 Figment,
7};
8
9use crate::error::ConfigError;
10use crate::model::{Config, ResolvedConfig};
11use crate::paths::{discover_project_root, project_config_path, user_config_path};
12
13#[derive(Debug, Clone, Default)]
15pub struct LoadOptions {
16 pub cwd: Option<PathBuf>,
17 pub model: Option<String>,
18 pub provider: Option<String>,
19 pub language: Option<String>,
20}
21
22pub fn load(opts: &LoadOptions) -> Result<ResolvedConfig, ConfigError> {
24 let cwd = opts
25 .cwd
26 .clone()
27 .or_else(|| env::current_dir().ok())
28 .unwrap_or_else(|| PathBuf::from("."));
29
30 let user_config_path = user_config_path();
31 let project_root = discover_project_root(&cwd);
32 let project_config_path = project_root.as_ref().map(|root| project_config_path(root));
33
34 let mut figment = Figment::new().merge(Serialized::defaults(Config::default()));
35
36 if user_config_path.is_file() {
37 figment = figment.merge(Toml::file(&user_config_path));
38 }
39
40 if let Some(path) = &project_config_path {
41 if path.is_file() {
42 figment = figment.merge(Toml::file(path));
43 }
44 }
45
46 figment = figment.merge(Env::prefixed("CODEI_").split("__"));
48
49 let mut config: Config = figment
50 .extract()
51 .map_err(|e| ConfigError::Load(Box::new(e)))?;
52 apply_cli_overrides(&mut config, opts);
53
54 let resolved = ResolvedConfig {
55 config,
56 cwd,
57 project_root,
58 user_config_path,
59 project_config_path,
60 };
61 resolved.validate()?;
62 Ok(resolved)
63}
64
65fn apply_cli_overrides(config: &mut Config, opts: &LoadOptions) {
66 if let Some(model) = &opts.model {
67 config.defaults.model = model.clone();
68 }
69 if let Some(provider) = &opts.provider {
70 config.defaults.provider = provider.clone();
71 }
72 if let Some(language) = &opts.language {
73 config.defaults.language = language.clone();
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use std::fs;
81 use std::io::Write;
82 use std::path::Path;
83
84 use tempfile::TempDir;
85
86 fn write_config(dir: &Path, rel: &str, content: &str) {
87 let path = dir.join(rel);
88 if let Some(parent) = path.parent() {
89 fs::create_dir_all(parent).unwrap();
90 }
91 fs::File::create(path)
92 .unwrap()
93 .write_all(content.as_bytes())
94 .unwrap();
95 }
96
97 #[test]
98 fn loads_defaults_without_files() {
99 let dir = TempDir::new().unwrap();
100 let opts = LoadOptions {
101 cwd: Some(dir.path().to_path_buf()),
102 ..Default::default()
103 };
104 let resolved = load(&opts).unwrap();
105 assert!(!resolved.config.defaults.provider.is_empty());
106 assert!(resolved.config.defaults.max_tokens > 0);
107 }
108
109 #[test]
110 fn merges_project_config() {
111 let dir = TempDir::new().unwrap();
112 write_config(
113 dir.path(),
114 ".codei/config.toml",
115 r#"
116[defaults]
117model = "project-model"
118"#,
119 );
120
121 let opts = LoadOptions {
122 cwd: Some(dir.path().to_path_buf()),
123 ..Default::default()
124 };
125 let resolved = load(&opts).unwrap();
126 assert_eq!(resolved.config.defaults.model, "project-model");
127 assert_eq!(
128 resolved.project_root.as_deref(),
129 Some(dir.path().canonicalize().unwrap().as_path())
130 );
131 }
132
133 #[test]
134 fn cli_overrides_take_precedence() {
135 let dir = TempDir::new().unwrap();
136 write_config(
137 dir.path(),
138 ".codei/config.toml",
139 r#"
140[defaults]
141model = "project-model"
142"#,
143 );
144
145 let opts = LoadOptions {
146 cwd: Some(dir.path().to_path_buf()),
147 model: Some("cli-model".to_string()),
148 ..Default::default()
149 };
150 let resolved = load(&opts).unwrap();
151 assert_eq!(resolved.config.defaults.model, "cli-model");
152 }
153}