mirror of
https://github.com/ultraworkers/claw-code-parity.git
synced 2026-06-24 10:51:10 +00: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:
parent
4331fdeb6a
commit
30b87c3725
@ -122,6 +122,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
model,
|
model,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
} => print_status_snapshot(&model, permission_mode)?,
|
} => print_status_snapshot(&model, permission_mode)?,
|
||||||
|
CliAction::ConfigShow => print_config_json()?,
|
||||||
CliAction::Sandbox => print_sandbox_status_snapshot()?,
|
CliAction::Sandbox => print_sandbox_status_snapshot()?,
|
||||||
CliAction::Prompt {
|
CliAction::Prompt {
|
||||||
prompt,
|
prompt,
|
||||||
@ -171,6 +172,7 @@ enum CliAction {
|
|||||||
model: String,
|
model: String,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
},
|
},
|
||||||
|
ConfigShow,
|
||||||
Sandbox,
|
Sandbox,
|
||||||
Prompt {
|
Prompt {
|
||||||
prompt: String,
|
prompt: String,
|
||||||
@ -356,6 +358,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
"agents" => Ok(CliAction::Agents {
|
"agents" => Ok(CliAction::Agents {
|
||||||
args: join_optional_args(&rest[1..]),
|
args: join_optional_args(&rest[1..]),
|
||||||
}),
|
}),
|
||||||
|
"config" => parse_config_args(&rest[1..]),
|
||||||
"mcp" => Ok(CliAction::Mcp {
|
"mcp" => Ok(CliAction::Mcp {
|
||||||
args: join_optional_args(&rest[1..]),
|
args: join_optional_args(&rest[1..]),
|
||||||
}),
|
}),
|
||||||
@ -396,7 +399,7 @@ fn parse_single_word_command_alias(
|
|||||||
model: &str,
|
model: &str,
|
||||||
permission_mode_override: Option<PermissionMode>,
|
permission_mode_override: Option<PermissionMode>,
|
||||||
) -> Option<Result<CliAction, String>> {
|
) -> Option<Result<CliAction, String>> {
|
||||||
if rest.len() != 1 || rest[0] == "branch" {
|
if rest.len() != 1 || matches!(rest[0].as_str(), "branch" | "config") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,6 +421,7 @@ fn bare_slash_command_guidance(command_name: &str) -> Option<String> {
|
|||||||
"dump-manifests"
|
"dump-manifests"
|
||||||
| "bootstrap-plan"
|
| "bootstrap-plan"
|
||||||
| "agents"
|
| "agents"
|
||||||
|
| "config"
|
||||||
| "mcp"
|
| "mcp"
|
||||||
| "skills"
|
| "skills"
|
||||||
| "system-prompt"
|
| "system-prompt"
|
||||||
@ -701,6 +705,16 @@ fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
Ok(CliAction::PrintSystemPrompt { cwd, date })
|
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> {
|
fn parse_branch_args(args: &[String]) -> Result<CliAction, String> {
|
||||||
match args {
|
match args {
|
||||||
[] => Err("Usage: claw branch delete".to_string()),
|
[] => Err("Usage: claw branch delete".to_string()),
|
||||||
@ -3620,6 +3634,19 @@ fn print_sandbox_status_snapshot() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
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>> {
|
fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
let loader = ConfigLoader::default_for(&cwd);
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
@ -5914,6 +5941,8 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
|||||||
out,
|
out,
|
||||||
" Show workspace status, origin/main freshness, active worktrees, and recent commits"
|
" 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, " claw sandbox")?;
|
||||||
writeln!(out, " Show the current sandbox isolation snapshot")?;
|
writeln!(out, " Show the current sandbox isolation snapshot")?;
|
||||||
writeln!(out, " claw dump-manifests")?;
|
writeln!(out, " claw dump-manifests")?;
|
||||||
@ -5995,6 +6024,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
|||||||
out,
|
out,
|
||||||
" claw --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt"
|
" claw --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt"
|
||||||
)?;
|
)?;
|
||||||
|
writeln!(out, " claw config show")?;
|
||||||
writeln!(out, " claw branch delete")?;
|
writeln!(out, " claw branch delete")?;
|
||||||
writeln!(out, " claw agents")?;
|
writeln!(out, " claw agents")?;
|
||||||
writeln!(out, " claw mcp show my-server")?;
|
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_args, parse_git_status_branch, parse_git_status_metadata_for,
|
||||||
parse_git_workspace_summary, parse_git_worktrees, parse_recent_commits, permission_policy,
|
parse_git_workspace_summary, parse_git_worktrees, parse_recent_commits, permission_policy,
|
||||||
print_help_to, push_output_block, render_config_report, render_diff_report,
|
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,
|
render_diff_report_for, render_memory_report, render_merged_runtime_config_json,
|
||||||
resolve_model_alias, resolve_session_reference, response_to_events,
|
render_repl_help, render_resume_usage, resolve_model_alias, resolve_session_reference,
|
||||||
resume_supported_slash_commands, run_resume_command,
|
response_to_events, resume_supported_slash_commands, run_resume_command,
|
||||||
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
||||||
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitBranchFreshness,
|
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitBranchFreshness,
|
||||||
GitCommitEntry, GitWorkspaceSummary, GitWorktreeEntry, InternalPromptProgressEvent,
|
GitCommitEntry, GitWorkspaceSummary, GitWorktreeEntry, InternalPromptProgressEvent,
|
||||||
@ -6441,6 +6471,18 @@ mod tests {
|
|||||||
assert!(unknown_error.contains("Usage: claw branch delete"));
|
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]
|
#[test]
|
||||||
fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode() {
|
fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode() {
|
||||||
let _guard = env_lock();
|
let _guard = env_lock();
|
||||||
@ -7111,6 +7153,16 @@ mod tests {
|
|||||||
assert!(plugins_report.contains("Merged section: plugins"));
|
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]
|
#[test]
|
||||||
fn memory_report_uses_sectioned_layout() {
|
fn memory_report_uses_sectioned_layout() {
|
||||||
let report = render_memory_report().expect("memory report should render");
|
let report = render_memory_report().expect("memory report should render");
|
||||||
@ -7441,6 +7493,7 @@ UU conflicted.rs",
|
|||||||
let mut help = Vec::new();
|
let mut help = Vec::new();
|
||||||
print_help_to(&mut help).expect("help should render");
|
print_help_to(&mut help).expect("help should render");
|
||||||
let help = String::from_utf8(help).expect("help should be utf8");
|
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 branch delete"));
|
||||||
assert!(help.contains("claw --resume [SESSION.jsonl|session-id|latest]"));
|
assert!(help.contains("claw --resume [SESSION.jsonl|session-id|latest]"));
|
||||||
assert!(help.contains("Use `latest` with --resume, /resume, or /session switch"));
|
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 std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use runtime::Session;
|
use runtime::Session;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
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");
|
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 {
|
fn command_in(cwd: &Path) -> Command {
|
||||||
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
|
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
|
||||||
command.current_dir(cwd);
|
command.current_dir(cwd);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user