1mod registry;
17
18use crate::agents::opencode_api::{CatalogLoader, RealCatalogLoader};
19use crate::agents::ConfigSource;
20use crate::cli::{
21 apply_args_to_config, handle_check_config_with, handle_extended_help,
22 handle_generate_completion, handle_init_global_with, handle_init_local_config_with,
23 handle_list_work_guides, handle_smart_init_with, Args,
24};
25use crate::config::{
26 loader, unified_config_path, Config, ConfigEnvironment, RealConfigEnvironment,
27};
28use crate::logger::Colors;
29use crate::logger::Logger;
30use std::path::PathBuf;
31
32use crate::agents::AgentRegistry;
33use registry::{apply_default_agents, load_agent_registry, resolve_agent_config_source_path};
34
35pub struct ConfigInitResult {
37 pub config: Config,
39 pub registry: AgentRegistry,
41 pub config_path: PathBuf,
43 pub config_sources: Vec<ConfigSource>,
45 pub agent_resolution_sources: AgentResolutionSources,
47}
48
49#[derive(Debug, Clone)]
51pub struct AgentResolutionSources {
52 pub local_config_path: Option<PathBuf>,
54 pub global_config_path: Option<PathBuf>,
56 pub built_in_defaults: bool,
58}
59
60impl AgentResolutionSources {
61 #[must_use]
63 pub fn describe_searched_sources(&self) -> String {
64 let sources: Vec<String> = [
65 self.local_config_path
66 .as_ref()
67 .map(|path| format!("local config ({})", path.display())),
68 self.global_config_path
69 .as_ref()
70 .map(|path| format!("global config ({})", path.display())),
71 self.built_in_defaults
72 .then(|| "built-in defaults".to_string()),
73 ]
74 .into_iter()
75 .flatten()
76 .collect();
77
78 if sources.is_empty() {
79 "none".to_string()
80 } else {
81 sources.join(", ")
82 }
83 }
84}
85
86pub fn initialize_config(
111 args: &Args,
112 colors: Colors,
113 logger: &Logger,
114) -> anyhow::Result<Option<ConfigInitResult>> {
115 initialize_config_with(
116 args,
117 colors,
118 logger,
119 &RealCatalogLoader::default(),
120 &RealConfigEnvironment,
121 )
122}
123
124#[expect(clippy::print_stderr, reason = "CLI error output to user")]
136#[expect(clippy::print_stdout, reason = "CLI help output to user")]
137pub fn initialize_config_with<L: CatalogLoader, P: ConfigEnvironment>(
149 args: &Args,
150 colors: Colors,
151 logger: &Logger,
152 catalog_loader: &L,
153 path_resolver: &P,
154) -> anyhow::Result<Option<ConfigInitResult>> {
155 let (config, unified, warnings) =
158 match loader::load_config_from_path_with_env(args.config.as_deref(), path_resolver) {
159 Ok(result) => result,
160 Err(e) => {
161 eprintln!("{}", e.format_errors());
164 return Err(anyhow::anyhow!("Configuration validation failed"));
165 }
166 };
167
168 warnings.iter().for_each(|warning| logger.warn(warning));
170
171 let config_path = args
172 .config
173 .clone()
174 .or_else(unified_config_path)
175 .unwrap_or_else(|| PathBuf::from("~/.config/ralph-workflow.toml"));
176
177 let config = apply_args_to_config(args, config, colors);
179
180 if let Some(shell) = args.completion.generate_completion {
182 if handle_generate_completion(shell) {
183 return Ok(None);
184 }
185 }
186
187 if args.recovery.extended_help {
190 handle_extended_help();
191 if args.work_guide_list.list_work_guides {
192 println!();
193 let _ = handle_list_work_guides(colors);
194 }
195 return Ok(None);
196 }
197
198 if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
200 return Ok(None);
201 }
202
203 if args.unified_init.init.is_some()
205 && handle_smart_init_with(
206 args.unified_init.init.as_deref(),
207 args.unified_init.force_init,
208 colors,
209 path_resolver,
210 )?
211 {
212 return Ok(None);
213 }
214
215 if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
217 return Ok(None);
218 }
219
220 if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
222 return Ok(None);
223 }
224
225 if args.unified_init.init_local_config
227 && handle_init_local_config_with(colors, path_resolver, args.unified_init.force_init)?
228 {
229 return Ok(None);
230 }
231
232 if args.unified_init.check_config
234 && handle_check_config_with(colors, path_resolver, args.debug_verbosity.debug)?
235 {
236 return Ok(None);
237 }
238
239 let local_config_path = path_resolver.local_config_path();
240 let global_config_path = args
241 .config
242 .clone()
243 .or_else(|| path_resolver.unified_config_path());
244
245 let agent_resolution_sources = AgentResolutionSources {
246 local_config_path: if args.config.is_none() {
247 local_config_path.clone()
248 } else {
249 None
250 },
251 global_config_path,
252 built_in_defaults: true,
253 };
254
255 let config_source_path = resolve_agent_config_source_path(
257 config_path.as_path(),
258 args.config.as_deref(),
259 local_config_path.as_deref(),
260 path_resolver,
261 );
262 let (registry, config_sources) = load_agent_registry(
263 unified.as_ref(),
264 config_source_path.as_path(),
265 catalog_loader,
266 )?;
267
268 let config = apply_default_agents(&config, ®istry);
270
271 Ok(Some(ConfigInitResult {
272 config,
273 registry,
274 config_path,
275 config_sources,
276 agent_resolution_sources,
277 }))
278}
279
280#[cfg(test)]
281mod tests {
282 use super::{initialize_config_with, AgentResolutionSources};
283 use crate::agents::opencode_api::{
284 ApiCatalog, CacheError, CatalogLoader, DEFAULT_CACHE_TTL_SECONDS,
285 };
286 use crate::cli::Args;
287 use crate::config::MemoryConfigEnvironment;
288 use crate::logger::{Colors, Logger};
289 use clap::Parser;
290 use std::collections::HashMap;
291 use std::path::PathBuf;
292
293 struct StaticCatalogLoader;
294
295 impl CatalogLoader for StaticCatalogLoader {
296 fn load(&self) -> Result<ApiCatalog, CacheError> {
297 Ok(ApiCatalog {
298 providers: HashMap::new(),
299 models: HashMap::new(),
300 cached_at: None,
301 ttl_seconds: DEFAULT_CACHE_TTL_SECONDS,
302 })
303 }
304 }
305
306 #[test]
307 fn test_explicit_config_does_not_report_local_source() {
308 let args = Args::try_parse_from(["ralph", "--config", "/test/config/ralph-workflow.toml"])
309 .expect("args should parse");
310 let logger = Logger::new(Colors::new());
311 let env = MemoryConfigEnvironment::new()
312 .with_unified_config_path("/test/config/ralph-workflow.toml")
313 .with_local_config_path("/test/repo/.agent/ralph-workflow.toml")
314 .with_file(
315 "/test/repo/.agent/ralph-workflow.toml",
316 "[agent_chain]\ndeveloper = [\"codex\"]\n",
317 );
318
319 let result =
320 initialize_config_with(&args, Colors::new(), &logger, &StaticCatalogLoader, &env)
321 .expect("initialization should succeed")
322 .expect("normal execution should return config init result");
323
324 assert!(
325 result.config_sources.is_empty(),
326 "with explicit --config and no explicit file present, local config should not be consulted"
327 );
328 assert_eq!(result.agent_resolution_sources.local_config_path, None);
329 assert_eq!(
330 result.agent_resolution_sources.global_config_path,
331 Some(PathBuf::from("/test/config/ralph-workflow.toml"))
332 );
333 }
334
335 #[test]
336 fn test_agent_resolution_sources_include_local_when_no_explicit_config() {
337 let args = Args::try_parse_from(["ralph"]).expect("args should parse");
338 let logger = Logger::new(Colors::new());
339 let env = MemoryConfigEnvironment::new()
340 .with_unified_config_path("/test/config/ralph-workflow.toml")
341 .with_local_config_path("/test/repo/.agent/ralph-workflow.toml");
342
343 let result =
344 initialize_config_with(&args, Colors::new(), &logger, &StaticCatalogLoader, &env)
345 .expect("initialization should succeed")
346 .expect("normal execution should return config init result");
347
348 assert_eq!(
349 result.agent_resolution_sources.local_config_path,
350 Some(PathBuf::from("/test/repo/.agent/ralph-workflow.toml"))
351 );
352 assert_eq!(
353 result.agent_resolution_sources.global_config_path,
354 Some(PathBuf::from("/test/config/ralph-workflow.toml"))
355 );
356 assert!(result.agent_resolution_sources.built_in_defaults);
357 }
358
359 #[test]
360 fn test_agent_resolution_sources_exclude_local_with_explicit_config() {
361 let args = Args::try_parse_from(["ralph", "--config", "/custom/path.toml"])
362 .expect("args should parse");
363 let logger = Logger::new(Colors::new());
364 let env = MemoryConfigEnvironment::new()
365 .with_unified_config_path("/test/config/ralph-workflow.toml")
366 .with_local_config_path("/test/repo/.agent/ralph-workflow.toml");
367
368 let result =
369 initialize_config_with(&args, Colors::new(), &logger, &StaticCatalogLoader, &env)
370 .expect("initialization should succeed")
371 .expect("normal execution should return config init result");
372
373 assert_eq!(result.agent_resolution_sources.local_config_path, None);
374 assert_eq!(
375 result.agent_resolution_sources.global_config_path,
376 Some(PathBuf::from("/custom/path.toml"))
377 );
378 }
379
380 #[test]
381 fn test_agent_resolution_sources_description_omits_missing_sources() {
382 let sources = AgentResolutionSources {
383 local_config_path: None,
384 global_config_path: Some(PathBuf::from("/custom/path.toml")),
385 built_in_defaults: true,
386 };
387
388 assert_eq!(
389 sources.describe_searched_sources(),
390 "global config (/custom/path.toml), built-in defaults"
391 );
392 }
393}