diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index eb69e6e..ff7adae 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -122,6 +122,7 @@ fn run() -> Result<(), Box> { model, permission_mode, } => print_status_snapshot(&model, permission_mode)?, + CliAction::ConfigShow => print_config_json()?, CliAction::Sandbox => print_sandbox_status_snapshot()?, CliAction::Prompt { prompt, @@ -171,6 +172,7 @@ enum CliAction { model: String, permission_mode: PermissionMode, }, + ConfigShow, Sandbox, Prompt { prompt: String, @@ -356,6 +358,7 @@ fn parse_args(args: &[String]) -> Result { "agents" => Ok(CliAction::Agents { args: join_optional_args(&rest[1..]), }), + "config" => parse_config_args(&rest[1..]), "mcp" => Ok(CliAction::Mcp { args: join_optional_args(&rest[1..]), }), @@ -396,7 +399,7 @@ fn parse_single_word_command_alias( model: &str, permission_mode_override: Option, ) -> Option> { - if rest.len() != 1 || rest[0] == "branch" { + if rest.len() != 1 || matches!(rest[0].as_str(), "branch" | "config") { return None; } @@ -418,6 +421,7 @@ fn bare_slash_command_guidance(command_name: &str) -> Option { "dump-manifests" | "bootstrap-plan" | "agents" + | "config" | "mcp" | "skills" | "system-prompt" @@ -701,6 +705,16 @@ fn parse_system_prompt_args(args: &[String]) -> Result { Ok(CliAction::PrintSystemPrompt { cwd, date }) } +fn parse_config_args(args: &[String]) -> Result { + match args { + [] => Err("Usage: claw config show".to_string()), + [action] if action == "show" => Ok(CliAction::ConfigShow), + [action, ..] => Err(format!( + "unknown config action: {action}. Usage: claw config show" + )), + } +} + fn parse_branch_args(args: &[String]) -> Result { match args { [] => Err("Usage: claw branch delete".to_string()), @@ -3620,6 +3634,19 @@ fn print_sandbox_status_snapshot() -> Result<(), Box> { Ok(()) } +fn print_config_json() -> Result<(), Box> { + println!("{}", render_merged_runtime_config_json()?); + Ok(()) +} + +fn render_merged_runtime_config_json() -> Result> { + let cwd = env::current_dir()?; + let loader = ConfigLoader::default_for(&cwd); + let runtime_config = loader.load()?; + let parsed: serde_json::Value = serde_json::from_str(&runtime_config.as_json().render())?; + Ok(serde_json::to_string_pretty(&parsed)?) +} + fn render_config_report(section: Option<&str>) -> Result> { let cwd = env::current_dir()?; let loader = ConfigLoader::default_for(&cwd); @@ -5914,6 +5941,8 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> { out, " Show workspace status, origin/main freshness, active worktrees, and recent commits" )?; + writeln!(out, " claw config show")?; + writeln!(out, " Print the merged runtime config as JSON")?; writeln!(out, " claw sandbox")?; writeln!(out, " Show the current sandbox isolation snapshot")?; writeln!(out, " claw dump-manifests")?; @@ -5995,6 +6024,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> { out, " claw --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt" )?; + writeln!(out, " claw config show")?; writeln!(out, " claw branch delete")?; writeln!(out, " claw agents")?; writeln!(out, " claw mcp show my-server")?; @@ -6023,9 +6053,9 @@ mod tests { parse_args, parse_git_status_branch, parse_git_status_metadata_for, parse_git_workspace_summary, parse_git_worktrees, parse_recent_commits, permission_policy, print_help_to, push_output_block, render_config_report, render_diff_report, - render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage, - resolve_model_alias, resolve_session_reference, response_to_events, - resume_supported_slash_commands, run_resume_command, + render_diff_report_for, render_memory_report, render_merged_runtime_config_json, + render_repl_help, render_resume_usage, resolve_model_alias, resolve_session_reference, + response_to_events, resume_supported_slash_commands, run_resume_command, slash_command_completion_candidates_with_sessions, status_context, validate_no_args, write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitBranchFreshness, GitCommitEntry, GitWorkspaceSummary, GitWorktreeEntry, InternalPromptProgressEvent, @@ -6441,6 +6471,18 @@ mod tests { assert!(unknown_error.contains("Usage: claw branch delete")); } + #[test] + fn parses_config_show_subcommand() { + assert_eq!( + parse_args(&["config".to_string(), "show".to_string()]) + .expect("config show should parse"), + CliAction::ConfigShow + ); + + let error = parse_args(&["config".to_string()]).expect_err("missing action should fail"); + assert!(error.contains("Usage: claw config show")); + } + #[test] fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode() { let _guard = env_lock(); @@ -7111,6 +7153,16 @@ mod tests { assert!(plugins_report.contains("Merged section: plugins")); } + #[test] + fn merged_runtime_config_json_renders_pretty_valid_json() { + let rendered = + render_merged_runtime_config_json().expect("runtime config json should render"); + let parsed: serde_json::Value = + serde_json::from_str(&rendered).expect("runtime config json should parse"); + assert!(parsed.is_object()); + assert!(rendered.starts_with("{\n") || rendered == "{}"); + } + #[test] fn memory_report_uses_sectioned_layout() { let report = render_memory_report().expect("memory report should render"); @@ -7441,6 +7493,7 @@ UU conflicted.rs", let mut help = Vec::new(); print_help_to(&mut help).expect("help should render"); let help = String::from_utf8(help).expect("help should be utf8"); + assert!(help.contains("claw config show")); assert!(help.contains("claw branch delete")); assert!(help.contains("claw --resume [SESSION.jsonl|session-id|latest]")); assert!(help.contains("Use `latest` with --resume, /resume, or /session switch")); diff --git a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs index 9d574c4..56d2515 100644 --- a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs +++ b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs @@ -5,6 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use runtime::Session; +use serde_json::json; static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); @@ -160,6 +161,52 @@ fn config_command_loads_defaults_from_standard_config_locations() { fs::remove_dir_all(temp_dir).expect("cleanup temp dir"); } +#[test] +fn config_show_command_prints_merged_runtime_config_as_json() { + // given + let temp_dir = unique_temp_dir("config-show"); + let config_home = temp_dir.join("home").join(".claw"); + fs::create_dir_all(temp_dir.join(".claw")).expect("project config dir should exist"); + fs::create_dir_all(&config_home).expect("home config dir should exist"); + + fs::write( + config_home.join("settings.json"), + r#"{"model":"haiku","sandbox":{"enabled":true}}"#, + ) + .expect("write user settings"); + fs::write( + temp_dir.join(".claw.json"), + r#"{"permissions":{"allow":["git status"]},"sandbox":{"networkIsolation":true}}"#, + ) + .expect("write project settings"); + fs::write( + temp_dir.join(".claw").join("settings.local.json"), + r#"{"model":"opus","sandbox":{"filesystemMode":"workspace-only"}}"#, + ) + .expect("write local settings"); + + // when + let output = command_in(&temp_dir) + .env("CLAW_CONFIG_HOME", &config_home) + .args(["config", "show"]) + .output() + .expect("claw should launch"); + + // then + assert_success(&output); + let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("config show output should be valid json"); + assert_eq!(parsed["model"], "opus"); + assert_eq!(parsed["permissions"]["allow"], json!(["git status"])); + assert_eq!(parsed["sandbox"]["enabled"], true); + assert_eq!(parsed["sandbox"]["networkIsolation"], true); + assert_eq!(parsed["sandbox"]["filesystemMode"], "workspace-only"); + assert!(stdout.starts_with("{\n")); + + fs::remove_dir_all(temp_dir).expect("cleanup temp dir"); +} + fn command_in(cwd: &Path) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); command.current_dir(cwd);