mirror of
https://github.com/ultraworkers/claw-code-parity.git
synced 2026-04-29 08:16:14 +02:00
feat(cli): add claw config show command
Expose the merged runtime settings through a direct CLI subcommand so users can inspect the effective configuration without entering the REPL. The command prints the merged config as pretty JSON and is covered by unit and end-to-end tests. Constraint: Must preserve existing config precedence and read the same merged runtime config used by the CLI Rejected: Reuse the text `/config` report output | user requested machine-readable JSON Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep `claw config show` aligned with `ConfigLoader` precedence rules if config discovery changes Tested: `cd rust && cargo build --workspace`; `cd rust && cargo test --workspace` Not-tested: Manual invocation against a real user config outside test fixtures
This commit is contained in:
@@ -122,6 +122,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<CliAction, String> {
|
||||
"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<PermissionMode>,
|
||||
) -> Option<Result<CliAction, String>> {
|
||||
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<String> {
|
||||
"dump-manifests"
|
||||
| "bootstrap-plan"
|
||||
| "agents"
|
||||
| "config"
|
||||
| "mcp"
|
||||
| "skills"
|
||||
| "system-prompt"
|
||||
@@ -701,6 +705,16 @@ fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
||||
Ok(CliAction::PrintSystemPrompt { cwd, date })
|
||||
}
|
||||
|
||||
fn parse_config_args(args: &[String]) -> Result<CliAction, String> {
|
||||
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<CliAction, String> {
|
||||
match args {
|
||||
[] => Err("Usage: claw branch delete".to_string()),
|
||||
@@ -3620,6 +3634,19 @@ fn print_sandbox_status_snapshot() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_config_json() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("{}", render_merged_runtime_config_json()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_merged_runtime_config_json() -> Result<String, Box<dyn std::error::Error>> {
|
||||
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<String, Box<dyn std::error::Error>> {
|
||||
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"));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user