Skip to main content

ralph/cli/
config.rs

1//! `ralph config ...` command group: Clap types and handler.
2
3use anyhow::Result;
4use clap::{Args, Subcommand, ValueEnum};
5
6use crate::{
7    agent, agent::profiles::BUILTIN_QUICK, agent::profiles::BUILTIN_THOROUGH, config, contracts,
8};
9
10/// Output format for `config show` command.
11#[derive(Debug, Clone, Copy, Default, ValueEnum)]
12pub enum ConfigShowFormat {
13    /// YAML output (human-readable, default).
14    #[default]
15    #[value(alias = "text", alias = "yml")]
16    Yaml,
17
18    /// JSON output for scripting and tooling.
19    Json,
20}
21
22/// Arguments for the `ralph config show` command.
23#[derive(Args, Debug, Clone, Copy)]
24pub struct ConfigShowArgs {
25    /// Output format.
26    #[arg(long, value_enum, default_value = "yaml")]
27    pub format: ConfigShowFormat,
28}
29
30pub fn handle_config(cmd: ConfigCommand) -> Result<()> {
31    match cmd {
32        ConfigCommand::Show(args) => {
33            let resolved = config::resolve_from_cwd()?;
34            match args.format {
35                ConfigShowFormat::Json => {
36                    let rendered = serde_json::to_string_pretty(&resolved.config)?;
37                    println!("{rendered}");
38                }
39                ConfigShowFormat::Yaml => {
40                    let rendered = serde_yaml::to_string(&resolved.config)?;
41                    print!("{rendered}");
42                }
43            }
44        }
45        ConfigCommand::Paths => {
46            let resolved = config::resolve_from_cwd()?;
47            println!("repo_root: {}", resolved.repo_root.display());
48            println!("queue: {}", resolved.queue_path.display());
49            println!("done: {}", resolved.done_path.display());
50            if let Some(path) = resolved.global_config_path.as_ref() {
51                println!("global_config: {}", path.display());
52            } else {
53                println!("global_config: (unavailable)");
54            }
55            if let Some(path) = resolved.project_config_path.as_ref() {
56                println!("project_config: {}", path.display());
57            } else {
58                println!("project_config: (unavailable)");
59            }
60        }
61        ConfigCommand::Schema => {
62            let schema = schemars::schema_for!(contracts::Config);
63            println!("{}", serde_json::to_string_pretty(&schema)?);
64        }
65        ConfigCommand::Profiles(profiles_args) => {
66            handle_profiles(profiles_args)?;
67        }
68    }
69    Ok(())
70}
71
72fn handle_profiles(args: ConfigProfilesArgs) -> Result<()> {
73    let resolved = config::resolve_from_cwd()?;
74
75    match args.command {
76        ConfigProfilesCommand::List => {
77            let names = agent::all_profile_names(resolved.config.profiles.as_ref());
78
79            if names.is_empty() {
80                println!("No profiles configured.");
81                println!("Built-in profiles: quick, thorough");
82                return Ok(());
83            }
84
85            println!("Available profiles:");
86            for name in names {
87                let is_builtin = name == BUILTIN_QUICK || name == BUILTIN_THOROUGH;
88                let marker = if is_builtin { " (built-in)" } else { "" };
89
90                // Get effective patch for this profile
91                if let Some(patch) =
92                    agent::resolve_profile_patch(&name, resolved.config.profiles.as_ref())
93                {
94                    let details = format_profile_summary(&patch);
95                    println!("  {}{} - {}", name, marker, details);
96                } else {
97                    println!("  {}{}", name, marker);
98                }
99            }
100        }
101        ConfigProfilesCommand::Show { name } => {
102            let name = name.trim();
103            if name.is_empty() {
104                anyhow::bail!("Profile name cannot be empty");
105            }
106
107            match agent::resolve_profile_patch(name, resolved.config.profiles.as_ref()) {
108                Some(patch) => {
109                    println!("Profile: {}", name);
110                    let is_builtin = name == BUILTIN_QUICK || name == BUILTIN_THOROUGH;
111                    if is_builtin {
112                        println!("Source: built-in");
113                    } else if resolved
114                        .config
115                        .profiles
116                        .as_ref()
117                        .is_some_and(|p| p.contains_key(name))
118                    {
119                        println!("Source: config");
120                    }
121                    println!();
122                    let rendered = serde_yaml::to_string(&patch)?;
123                    print!("{}", rendered);
124                }
125                None => {
126                    let names = agent::all_profile_names(resolved.config.profiles.as_ref());
127                    anyhow::bail!(
128                        "Unknown profile: {name:?}. Available profiles: {}",
129                        names.into_iter().collect::<Vec<_>>().join(", ")
130                    );
131                }
132            }
133        }
134    }
135    Ok(())
136}
137
138/// Format a profile patch as a summary string.
139fn format_profile_summary(patch: &contracts::AgentConfig) -> String {
140    let mut parts = Vec::new();
141
142    if let Some(runner) = &patch.runner {
143        parts.push(format!("runner={}", runner.as_str()));
144    }
145    if let Some(model) = &patch.model {
146        parts.push(format!("model={}", model.as_str()));
147    }
148    if let Some(phases) = patch.phases {
149        parts.push(format!("phases={}", phases));
150    }
151    if let Some(effort) = &patch.reasoning_effort {
152        parts.push(format!("effort={}", format_reasoning_effort(*effort)));
153    }
154
155    if parts.is_empty() {
156        "no overrides".to_string()
157    } else {
158        parts.join(", ")
159    }
160}
161
162fn format_reasoning_effort(effort: contracts::ReasoningEffort) -> &'static str {
163    match effort {
164        contracts::ReasoningEffort::Low => "low",
165        contracts::ReasoningEffort::Medium => "medium",
166        contracts::ReasoningEffort::High => "high",
167        contracts::ReasoningEffort::XHigh => "xhigh",
168    }
169}
170
171#[derive(Args)]
172#[command(
173    about = "Inspect and manage Ralph configuration",
174    after_long_help = "Examples:\n  ralph config show\n  ralph config show --format json\n  ralph config paths\n  ralph config schema\n  ralph config profiles list\n  ralph config profiles show quick"
175)]
176pub struct ConfigArgs {
177    #[command(subcommand)]
178    pub command: ConfigCommand,
179}
180
181#[derive(Subcommand)]
182pub enum ConfigCommand {
183    /// Show the resolved Ralph configuration.
184    #[command(
185        after_long_help = "Examples:\n  ralph config show\n  ralph config show --format json\n  ralph config show --format yaml"
186    )]
187    Show(ConfigShowArgs),
188    /// Print paths to the queue, done archive, and config files.
189    #[command(after_long_help = "Example:\n  ralph config paths")]
190    Paths,
191    /// Print the JSON schema for the configuration.
192    #[command(after_long_help = "Example:\n  ralph config schema")]
193    Schema,
194    /// List and inspect configuration profiles.
195    #[command(
196        after_long_help = "Examples:\n  ralph config profiles list\n  ralph config profiles show quick\n  ralph config profiles show thorough"
197    )]
198    Profiles(ConfigProfilesArgs),
199}
200
201/// Arguments for the `ralph config profiles` command.
202#[derive(Args)]
203pub struct ConfigProfilesArgs {
204    #[command(subcommand)]
205    pub command: ConfigProfilesCommand,
206}
207
208/// Subcommands for `ralph config profiles`.
209#[derive(Subcommand)]
210pub enum ConfigProfilesCommand {
211    /// List available profiles (built-in + configured).
212    List,
213    /// Show one profile (effective patch), falling back to built-ins.
214    Show { name: String },
215}