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