Skip to main content

ralph_workflow/app/
config_init.rs

1//! Configuration loading and agent registry initialization.
2//!
3//! This module handles:
4//! - Loading configuration from the unified config file (~/.config/ralph-workflow.toml)
5//! - Applying environment variable and CLI overrides
6//! - Selecting default agents from fallback chains
7//! - Loading agent registry data from unified config
8//! - Fetching and caching OpenCode API catalog for dynamic provider/model resolution
9//!
10//! # Dependency Injection
11//!
12//! The [`initialize_config_with`] function accepts both a [`CatalogLoader`] and a
13//! [`ConfigEnvironment`] for full dependency injection. This enables testing without
14//! network calls or environment variable dependencies.
15
16use crate::agents::opencode_api::{CatalogLoader, RealCatalogLoader};
17use crate::agents::{
18    global_agents_config_path, validation as agent_validation, AgentRegistry, AgentRole,
19    ConfigSource,
20};
21use crate::cli::{
22    apply_args_to_config, handle_extended_help, handle_generate_completion,
23    handle_init_global_with, handle_init_legacy, handle_init_prompt_with, handle_list_work_guides,
24    handle_smart_init_with, Args,
25};
26use crate::config::{
27    loader, unified_config_path, Config, ConfigEnvironment, RealConfigEnvironment, UnifiedConfig,
28};
29use crate::git_helpers::get_repo_root;
30use crate::logger::Colors;
31use crate::logger::Logger;
32use std::path::PathBuf;
33
34/// Result of configuration initialization.
35pub struct ConfigInitResult {
36    /// The loaded configuration with CLI args applied.
37    pub config: Config,
38    /// The agent registry with merged configs.
39    pub registry: AgentRegistry,
40    /// The resolved path to the unified config file (for diagnostics/errors).
41    pub config_path: PathBuf,
42    /// Sources from which agent configs were loaded.
43    pub config_sources: Vec<ConfigSource>,
44}
45
46/// Initializes configuration and agent registry.
47///
48/// This function performs the following steps:
49/// 1. Loads config from unified config file (~/.config/ralph-workflow.toml)
50/// 2. Applies environment variable overrides
51/// 3. Applies CLI arguments to config
52/// 4. Handles --list-work-guides, --init-prompt, --init/--init-global (unified), and --init-legacy if set
53/// 5. Loads agent registry from built-ins + unified config
54/// 6. Selects default agents from fallback chains
55///
56/// # Arguments
57///
58/// * `args` - The parsed CLI arguments
59/// * `colors` - Color configuration for output
60/// * `logger` - Logger for info/warning messages
61///
62/// # Returns
63///
64/// Returns `Ok(Some(result))` on success, `Ok(None)` if an early exit was triggered
65/// (e.g., --init, --init-prompt, --list-templates), or an error if initialization fails.
66pub fn initialize_config(
67    args: &Args,
68    colors: Colors,
69    logger: &Logger,
70) -> anyhow::Result<Option<ConfigInitResult>> {
71    initialize_config_with(
72        args,
73        colors,
74        logger,
75        &RealCatalogLoader,
76        &RealConfigEnvironment,
77    )
78}
79
80/// Initializes configuration and agent registry with a custom catalog loader.
81///
82/// This is the same as [`initialize_config`] but accepts a custom [`CatalogLoader`]
83/// for dependency injection. This is primarily useful for testing.
84#[deprecated(since = "0.6.0", note = "Use initialize_config_with instead")]
85pub fn initialize_config_with_loader<L: CatalogLoader>(
86    args: &Args,
87    colors: Colors,
88    logger: &Logger,
89    catalog_loader: &L,
90) -> anyhow::Result<Option<ConfigInitResult>> {
91    initialize_config_with(args, colors, logger, catalog_loader, &RealConfigEnvironment)
92}
93
94/// Initializes configuration and agent registry with full dependency injection.
95///
96/// This is the same as [`initialize_config`] but accepts both a [`CatalogLoader`]
97/// and a [`ConfigEnvironment`] for full dependency injection. This enables testing
98/// without network calls or environment variable dependencies.
99///
100/// # Arguments
101///
102/// * `args` - The parsed CLI arguments
103/// * `colors` - Color configuration for output
104/// * `logger` - Logger for info/warning messages
105/// * `catalog_loader` - Loader for the OpenCode API catalog
106/// * `path_resolver` - Resolver for configuration file paths
107///
108/// # Returns
109///
110/// Returns `Ok(Some(result))` on success, `Ok(None)` if an early exit was triggered
111/// (e.g., --init, --init-prompt, --list-templates), or an error if initialization fails.
112pub fn initialize_config_with<L: CatalogLoader, P: ConfigEnvironment>(
113    args: &Args,
114    colors: Colors,
115    logger: &Logger,
116    catalog_loader: &L,
117    path_resolver: &P,
118) -> anyhow::Result<Option<ConfigInitResult>> {
119    // Load configuration from unified config file (with env overrides)
120    // Uses the provided path_resolver for filesystem operations instead of std::fs directly
121    let (mut config, unified, warnings) =
122        loader::load_config_from_path_with_env(args.config.as_deref(), path_resolver);
123
124    // Display any deprecation warnings from config loading
125    for warning in warnings {
126        logger.warn(&warning);
127    }
128
129    let config_path = args
130        .config
131        .clone()
132        .or_else(unified_config_path)
133        .unwrap_or_else(|| PathBuf::from("~/.config/ralph-workflow.toml"));
134
135    // Set commit message from CLI
136    config = config.with_commit_msg(args.commit_msg.clone());
137
138    // Apply CLI arguments to config
139    apply_args_to_config(args, &mut config, colors);
140
141    // Handle --generate-completion flag: generate shell completion script and exit
142    if let Some(shell) = args.completion.generate_completion {
143        if handle_generate_completion(shell) {
144            return Ok(None);
145        }
146    }
147
148    // Handle --extended-help / --man flag: display extended help and exit.
149    // If combined with --list-work-guides, show both to reduce surprises.
150    if args.recovery.extended_help {
151        handle_extended_help();
152        if args.work_guide_list.list_work_guides {
153            println!();
154            handle_list_work_guides(colors);
155        }
156        return Ok(None);
157    }
158
159    // Handle --list-work-guides / --list-templates flag: display available Work Guides and exit
160    if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
161        return Ok(None);
162    }
163
164    // Handle --init-prompt flag: create PROMPT.md from template and exit
165    if let Some(ref template_name) = args.init_prompt {
166        if handle_init_prompt_with(
167            template_name,
168            args.unified_init.force_init,
169            colors,
170            path_resolver,
171        )? {
172            return Ok(None);
173        }
174    }
175
176    // Handle smart --init flag: intelligently determine what to initialize
177    if args.unified_init.init.is_some()
178        && handle_smart_init_with(
179            args.unified_init.init.as_deref(),
180            args.unified_init.force_init,
181            colors,
182            path_resolver,
183        )?
184    {
185        return Ok(None);
186    }
187
188    // Handle --init-config flag: explicit config creation and exit
189    if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
190        return Ok(None);
191    }
192
193    // Handle --init-global flag: create unified config if it doesn't exist and exit
194    if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
195        return Ok(None);
196    }
197
198    // Handle --init-legacy flag: legacy per-repo agents.toml creation and exit
199    if args.legacy_init.init_legacy {
200        let repo_root = get_repo_root().ok();
201        let legacy_path = repo_root.map_or_else(
202            || PathBuf::from(".agent/agents.toml"),
203            |root| root.join(".agent/agents.toml"),
204        );
205        if handle_init_legacy(colors, &legacy_path)? {
206            return Ok(None);
207        }
208    }
209
210    // Initialize agent registry with built-in defaults + unified config.
211    let (registry, config_sources) =
212        load_agent_registry(unified.as_ref(), config_path.as_path(), catalog_loader)?;
213
214    // Apply default agents from fallback chains
215    apply_default_agents(&mut config, &registry);
216
217    Ok(Some(ConfigInitResult {
218        config,
219        registry,
220        config_path,
221        config_sources,
222    }))
223}
224
225fn load_agent_registry<L: CatalogLoader>(
226    unified: Option<&UnifiedConfig>,
227    config_path: &std::path::Path,
228    catalog_loader: &L,
229) -> anyhow::Result<(AgentRegistry, Vec<ConfigSource>)> {
230    let mut registry = AgentRegistry::new().map_err(|e| {
231        anyhow::anyhow!("Failed to load built-in default agents config (examples/agents.toml): {e}")
232    })?;
233
234    let mut sources = Vec::new();
235
236    // Backwards compatibility: load legacy agent config files only when unified config
237    // isn't present (this matches the deprecation warning behavior in config loader).
238    if unified.is_none() {
239        if let Some(global_path) = global_agents_config_path() {
240            if global_path.exists() {
241                let loaded = registry.load_from_file(&global_path).map_err(|e| {
242                    anyhow::anyhow!(
243                        "Failed to load legacy global agent config {}: {}",
244                        global_path.display(),
245                        e
246                    )
247                })?;
248                sources.push(ConfigSource {
249                    path: global_path,
250                    agents_loaded: loaded,
251                });
252            }
253        }
254
255        let repo_root = get_repo_root().ok();
256        let project_path = repo_root.map_or_else(
257            || PathBuf::from(".agent/agents.toml"),
258            |root| root.join(".agent/agents.toml"),
259        );
260        if project_path.exists() {
261            let loaded = registry.load_from_file(&project_path).map_err(|e| {
262                anyhow::anyhow!(
263                    "Failed to load legacy per-repo agent config {}: {}",
264                    project_path.display(),
265                    e
266                )
267            })?;
268            sources.push(ConfigSource {
269                path: project_path,
270                agents_loaded: loaded,
271            });
272        }
273    }
274
275    if let Some(unified_cfg) = unified {
276        let loaded = registry.apply_unified_config(unified_cfg);
277        if loaded > 0 || unified_cfg.agent_chain.is_some() {
278            sources.push(ConfigSource {
279                path: config_path.to_path_buf(),
280                agents_loaded: loaded,
281            });
282        }
283    }
284
285    // Load OpenCode API catalog if there are any opencode/* references
286    setup_opencode_catalog(&mut registry, unified, catalog_loader)?;
287
288    Ok((registry, sources))
289}
290
291/// Setup OpenCode API catalog for dynamic provider/model resolution.
292///
293/// This function:
294/// 1. Checks if there are any `opencode/*` references in the configured agent chains
295/// 2. If yes, fetches/loads the cached OpenCode API catalog
296/// 3. Sets the catalog on the registry for dynamic agent resolution
297/// 4. Validates all opencode/* references and reports errors with suggestions
298fn setup_opencode_catalog<L: CatalogLoader>(
299    registry: &mut AgentRegistry,
300    unified: Option<&UnifiedConfig>,
301    catalog_loader: &L,
302) -> anyhow::Result<()> {
303    // Collect fallback config from unified config or registry defaults
304    let fallback = unified
305        .and_then(|u| u.agent_chain.as_ref())
306        .cloned()
307        .unwrap_or_else(|| registry.fallback_config().clone());
308
309    // Check if there are any opencode/* references
310    let opencode_refs = agent_validation::get_opencode_refs(&fallback);
311    if opencode_refs.is_empty() {
312        // No opencode references, skip catalog loading
313        return Ok(());
314    }
315
316    // Load the API catalog using the injected loader
317    let catalog = catalog_loader.load().map_err(|e| {
318        anyhow::anyhow!(
319            "Failed to load OpenCode API catalog. \
320            This is required for the following agent references: {opencode_refs:?}. \
321            Error: {e}"
322        )
323    })?;
324
325    // Set the catalog on the registry for dynamic resolution
326    registry.set_opencode_catalog(catalog.clone());
327
328    // Validate all opencode/* references
329    agent_validation::validate_opencode_agents(&fallback, &catalog)
330        .map_err(|e| anyhow::anyhow!("{e}"))?;
331
332    Ok(())
333}
334
335/// Applies default agent selection from fallback chains.
336///
337/// If no agent was explicitly selected via CLI/env/preset, uses the first entry
338/// from the `agent_chain` configuration.
339fn apply_default_agents(config: &mut Config, registry: &AgentRegistry) {
340    if config.developer_agent.is_none() {
341        config.developer_agent = registry
342            .fallback_config()
343            .get_fallbacks(AgentRole::Developer)
344            .first()
345            .cloned();
346    }
347    if config.reviewer_agent.is_none() {
348        config.reviewer_agent = registry
349            .fallback_config()
350            .get_fallbacks(AgentRole::Reviewer)
351            .first()
352            .cloned();
353    }
354}