1use anyhow::Result;
4use clap::{Args, Subcommand, ValueEnum};
5
6use crate::{agent, config, contracts};
7
8#[derive(Debug, Clone, Copy, Default, ValueEnum)]
10pub enum ConfigShowFormat {
11 #[default]
13 #[value(alias = "text", alias = "yml")]
14 Yaml,
15
16 Json,
18}
19
20#[derive(Args, Debug, Clone, Copy)]
22pub struct ConfigShowArgs {
23 #[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
136fn 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 #[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 #[command(after_long_help = "Example:\n ralph config paths")]
188 Paths,
189 #[command(after_long_help = "Example:\n ralph config schema")]
191 Schema,
192 #[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#[derive(Args)]
201pub struct ConfigProfilesArgs {
202 #[command(subcommand)]
203 pub command: ConfigProfilesCommand,
204}
205
206#[derive(Subcommand)]
208pub enum ConfigProfilesCommand {
209 List,
211 Show { name: String },
213}