1use 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#[derive(Debug, Clone, Copy, Default, ValueEnum)]
12pub enum ConfigShowFormat {
13 #[default]
15 #[value(alias = "text", alias = "yml")]
16 Yaml,
17
18 Json,
20}
21
22#[derive(Args, Debug, Clone, Copy)]
24pub struct ConfigShowArgs {
25 #[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 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
138fn 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 #[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 #[command(after_long_help = "Example:\n ralph config paths")]
190 Paths,
191 #[command(after_long_help = "Example:\n ralph config schema")]
193 Schema,
194 #[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#[derive(Args)]
203pub struct ConfigProfilesArgs {
204 #[command(subcommand)]
205 pub command: ConfigProfilesCommand,
206}
207
208#[derive(Subcommand)]
210pub enum ConfigProfilesCommand {
211 List,
213 Show { name: String },
215}