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