1use anyhow::{Context, Result};
4use clap::{Args, Subcommand, ValueEnum};
5use std::env;
6
7use crate::{agent, config, contracts};
8
9#[derive(Debug, Clone, Copy, Default, ValueEnum)]
11pub enum ConfigShowFormat {
12 #[default]
14 #[value(alias = "text", alias = "yml")]
15 Yaml,
16
17 Json,
19}
20
21#[derive(Args, Debug, Clone, Copy)]
23pub struct ConfigShowArgs {
24 #[arg(long, value_enum, default_value = "yaml")]
26 pub format: ConfigShowFormat,
27}
28
29pub fn handle_config(cmd: ConfigCommand) -> Result<()> {
30 match cmd {
31 ConfigCommand::Show(args) => {
32 let resolved = config::resolve_from_cwd()?;
33 match args.format {
34 ConfigShowFormat::Json => {
35 let rendered = serde_json::to_string_pretty(&resolved.config)?;
36 println!("{rendered}");
37 }
38 ConfigShowFormat::Yaml => {
39 let rendered = serde_yaml::to_string(&resolved.config)?;
40 print!("{rendered}");
41 }
42 }
43 }
44 ConfigCommand::Paths => {
45 let resolved = config::resolve_from_cwd()?;
46 println!("repo_root: {}", resolved.repo_root.display());
47 println!("queue: {}", resolved.queue_path.display());
48 println!("done: {}", resolved.done_path.display());
49 if let Some(path) = resolved.global_config_path.as_ref() {
50 println!("global_config: {}", path.display());
51 } else {
52 println!("global_config: (unavailable)");
53 }
54 if let Some(path) = resolved.project_config_path.as_ref() {
55 println!("project_config: {}", path.display());
56 } else {
57 println!("project_config: (unavailable)");
58 }
59 }
60 ConfigCommand::Schema => {
61 let schema = schemars::schema_for!(contracts::Config);
62 println!("{}", serde_json::to_string_pretty(&schema)?);
63 }
64 ConfigCommand::Profiles(profiles_args) => {
65 handle_profiles(profiles_args)?;
66 }
67 ConfigCommand::Trust(trust_args) => {
68 let cwd = env::current_dir().context("resolve current working directory")?;
69 let repo_root = config::find_repo_root(&cwd);
70 match trust_args.command {
71 ConfigTrustCommand::Init => {
72 config::initialize_repo_trust_file(&repo_root)?;
73 }
74 }
75 }
76 }
77 Ok(())
78}
79
80fn handle_profiles(args: ConfigProfilesArgs) -> Result<()> {
81 let resolved = config::resolve_from_cwd()?;
82
83 match args.command {
84 ConfigProfilesCommand::List => {
85 let names = agent::all_profile_names(resolved.config.profiles.as_ref());
86
87 if names.is_empty() {
88 println!("No profiles configured.");
89 println!(
90 "Define profiles under the `profiles` key in .ralph/config.jsonc or ~/.config/ralph/config.jsonc."
91 );
92 return Ok(());
93 }
94
95 println!("Available profiles:");
96 for name in names {
97 if let Some(patch) =
98 agent::resolve_profile_patch(&name, resolved.config.profiles.as_ref())
99 {
100 let details = format_profile_summary(&patch);
101 println!(" {} - {}", name, details);
102 } else {
103 println!(" {}", name);
104 }
105 }
106 }
107 ConfigProfilesCommand::Show { name } => {
108 let name = name.trim();
109 if name.is_empty() {
110 anyhow::bail!("Profile name cannot be empty");
111 }
112
113 match agent::resolve_profile_patch(name, resolved.config.profiles.as_ref()) {
114 Some(patch) => {
115 println!("Profile: {}", name);
116 if resolved
117 .config
118 .profiles
119 .as_ref()
120 .is_some_and(|p| p.contains_key(name))
121 {
122 println!("Source: config");
123 }
124 println!();
125 let rendered = serde_yaml::to_string(&patch)?;
126 print!("{}", rendered);
127 }
128 None => {
129 let names = agent::all_profile_names(resolved.config.profiles.as_ref());
130 if names.is_empty() {
131 anyhow::bail!(
132 "Unknown profile: {name:?}. No profiles are configured. Define profiles under the `profiles` key in .ralph/config.jsonc or ~/.config/ralph/config.jsonc."
133 );
134 }
135 anyhow::bail!(
136 "Unknown profile: {name:?}. Available configured profiles: {}",
137 names.into_iter().collect::<Vec<_>>().join(", ")
138 );
139 }
140 }
141 }
142 }
143 Ok(())
144}
145
146fn format_profile_summary(patch: &contracts::AgentConfig) -> String {
148 let mut parts = Vec::new();
149
150 if let Some(runner) = &patch.runner {
151 parts.push(format!("runner={}", runner.as_str()));
152 }
153 if let Some(model) = &patch.model {
154 parts.push(format!("model={}", model.as_str()));
155 }
156 if let Some(phases) = patch.phases {
157 parts.push(format!("phases={}", phases));
158 }
159 if let Some(effort) = &patch.reasoning_effort {
160 parts.push(format!("effort={}", format_reasoning_effort(*effort)));
161 }
162 if let Some(mode) = &patch.claude_permission_mode {
163 parts.push(format!(
164 "claude_permission_mode={}",
165 format_claude_permission_mode(*mode)
166 ));
167 }
168 if let Some(mode) = patch.effective_approval_mode() {
169 parts.push(format!(
170 "approval_mode={}",
171 format_runner_approval_mode(mode)
172 ));
173 }
174 if let Some(mode) = patch.effective_git_publish_mode() {
175 parts.push(format!("git_publish_mode={}", mode.as_str()));
176 }
177
178 if parts.is_empty() {
179 "no overrides".to_string()
180 } else {
181 parts.join(", ")
182 }
183}
184
185fn format_claude_permission_mode(mode: contracts::ClaudePermissionMode) -> &'static str {
186 match mode {
187 contracts::ClaudePermissionMode::AcceptEdits => "accept_edits",
188 contracts::ClaudePermissionMode::BypassPermissions => "bypass_permissions",
189 }
190}
191
192fn format_runner_approval_mode(mode: contracts::RunnerApprovalMode) -> &'static str {
193 match mode {
194 contracts::RunnerApprovalMode::Default => "default",
195 contracts::RunnerApprovalMode::AutoEdits => "auto_edits",
196 contracts::RunnerApprovalMode::Yolo => "yolo",
197 contracts::RunnerApprovalMode::Safe => "safe",
198 }
199}
200
201fn format_reasoning_effort(effort: contracts::ReasoningEffort) -> &'static str {
202 match effort {
203 contracts::ReasoningEffort::Low => "low",
204 contracts::ReasoningEffort::Medium => "medium",
205 contracts::ReasoningEffort::High => "high",
206 contracts::ReasoningEffort::XHigh => "xhigh",
207 }
208}
209
210#[derive(Args, Debug)]
212pub struct ConfigTrustArgs {
213 #[command(subcommand)]
214 pub command: ConfigTrustCommand,
215}
216
217#[derive(Subcommand, Debug, Clone, Copy)]
218pub enum ConfigTrustCommand {
219 Init,
221}
222
223#[derive(Args)]
224#[command(
225 about = "Inspect and manage Ralph configuration",
226 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 trust init\n ralph config profiles list\n ralph config profiles show fast-local"
227)]
228pub struct ConfigArgs {
229 #[command(subcommand)]
230 pub command: ConfigCommand,
231}
232
233#[derive(Subcommand)]
234pub enum ConfigCommand {
235 #[command(
237 after_long_help = "Examples:\n ralph config show\n ralph config show --format json\n ralph config show --format yaml"
238 )]
239 Show(ConfigShowArgs),
240 #[command(after_long_help = "Example:\n ralph config paths")]
242 Paths,
243 #[command(after_long_help = "Example:\n ralph config schema")]
245 Schema,
246 #[command(
248 after_long_help = "Examples:\n ralph config profiles list\n ralph config profiles show fast-local\n ralph config profiles show deep-review"
249 )]
250 Profiles(ConfigProfilesArgs),
251 #[command(after_long_help = "Examples:\n ralph config trust init")]
253 Trust(ConfigTrustArgs),
254}
255
256#[derive(Args)]
258pub struct ConfigProfilesArgs {
259 #[command(subcommand)]
260 pub command: ConfigProfilesCommand,
261}
262
263#[derive(Subcommand)]
265pub enum ConfigProfilesCommand {
266 List,
268 Show { name: String },
270}
271
272#[cfg(test)]
273mod profile_summary_tests {
274 use super::*;
275 use crate::agent;
276
277 #[test]
278 fn builtin_safe_profile_summary_includes_safety_and_publish() {
279 let patch = agent::resolve_profile_patch("safe", None).expect("builtin safe");
280 let summary = format_profile_summary(&patch);
281 assert!(summary.contains("approval_mode=safe"), "{summary}");
282 assert!(
283 summary.contains("claude_permission_mode=accept_edits"),
284 "{summary}"
285 );
286 assert!(summary.contains("git_publish_mode=off"), "{summary}");
287 }
288
289 #[test]
290 fn builtin_power_user_profile_summary_includes_safety_and_publish() {
291 let patch = agent::resolve_profile_patch("power-user", None).expect("builtin power-user");
292 let summary = format_profile_summary(&patch);
293 assert!(summary.contains("approval_mode=yolo"), "{summary}");
294 assert!(
295 summary.contains("claude_permission_mode=bypass_permissions"),
296 "{summary}"
297 );
298 assert!(
299 summary.contains("git_publish_mode=commit_and_push"),
300 "{summary}"
301 );
302 }
303}