Complete local claw-first CLI and config surface alignment

This commit is contained in:
Yeachan-Heo
2026-04-05 18:11:25 +00:00
parent 55d9f1da56
commit f43375f067
4 changed files with 124 additions and 173 deletions

View File

@@ -129,8 +129,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
CliAction::Status {
model,
permission_mode,
} => print_status_snapshot(&model, permission_mode)?,
CliAction::Sandbox => print_sandbox_status_snapshot()?,
output_format,
} => print_status_snapshot(&model, permission_mode, output_format)?,
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
CliAction::Prompt {
prompt,
model,
@@ -181,8 +182,11 @@ enum CliAction {
Status {
model: String,
permission_mode: PermissionMode,
output_format: CliOutputFormat,
},
Sandbox {
output_format: CliOutputFormat,
},
Sandbox,
Prompt {
prompt: String,
model: String,
@@ -365,7 +369,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
if let Some(action) = parse_local_help_action(&rest) {
return action;
}
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode_override) {
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode_override, output_format) {
return action;
}
@@ -435,6 +439,7 @@ fn parse_single_word_command_alias(
rest: &[String],
model: &str,
permission_mode_override: Option<PermissionMode>,
output_format: CliOutputFormat,
) -> Option<Result<CliAction, String>> {
if rest.len() != 1 {
return None;
@@ -446,8 +451,9 @@ fn parse_single_word_command_alias(
"status" => Some(Ok(CliAction::Status {
model: model.to_string(),
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
output_format,
})),
"sandbox" => Some(Ok(CliAction::Sandbox)),
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
"doctor" => Some(Ok(CliAction::Doctor)),
other => bare_slash_command_guidance(other).map(Err),
}
@@ -1862,13 +1868,7 @@ fn run_resume_command(
};
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(match output_format {
CliOutputFormat::Text => json!({
"kind": "mcp",
"message": handle_mcp_slash_command(args.as_deref(), &cwd)?,
}),
CliOutputFormat::Json => handle_mcp_slash_command_json(args.as_deref(), &cwd)?,
}),
message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?),
})
}
SlashCommand::Memory => Ok(ResumeCommandOutcome {
@@ -1912,15 +1912,7 @@ fn run_resume_command(
let cwd = env::current_dir()?;
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(match output_format {
CliOutputFormat::Text => json!({
"kind": "skills",
"message": handle_skills_slash_command(args.as_deref(), &cwd)?,
}),
CliOutputFormat::Json => {
handle_skills_slash_command_json(args.as_deref(), &cwd)?
}
}),
message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?),
})
}
SlashCommand::Doctor => Ok(ResumeCommandOutcome {
@@ -3136,7 +3128,7 @@ impl LiveCli {
CliOutputFormat::Text => println!("{}", handle_mcp_slash_command(args, &cwd)?),
CliOutputFormat::Json => println!(
"{}",
serialize_json_output(&handle_mcp_slash_command_json(args, &cwd)?)?
serde_json::to_string_pretty(&handle_mcp_slash_command_json(args, &cwd)?)?
),
}
Ok(())
@@ -3151,7 +3143,7 @@ impl LiveCli {
CliOutputFormat::Text => println!("{}", handle_skills_slash_command(args, &cwd)?),
CliOutputFormat::Json => println!(
"{}",
serialize_json_output(&handle_skills_slash_command_json(args, &cwd)?)?
serde_json::to_string_pretty(&handle_skills_slash_command_json(args, &cwd)?)?
),
}
Ok(())
@@ -3659,22 +3651,68 @@ fn render_repl_help() -> String {
fn print_status_snapshot(
model: &str,
permission_mode: PermissionMode,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
format_status_report(
model,
StatusUsage {
message_count: 0,
turns: 0,
latest: TokenUsage::default(),
cumulative: TokenUsage::default(),
estimated_tokens: 0,
},
permission_mode.as_str(),
&status_context(None)?,
)
);
let usage = StatusUsage {
message_count: 0,
turns: 0,
latest: TokenUsage::default(),
cumulative: TokenUsage::default(),
estimated_tokens: 0,
};
let context = status_context(None)?;
match output_format {
CliOutputFormat::Text => println!(
"{}",
format_status_report(model, usage, permission_mode.as_str(), &context)
),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "status",
"model": model,
"permission_mode": permission_mode.as_str(),
"usage": {
"messages": usage.message_count,
"turns": usage.turns,
"latest_total": usage.latest.total_tokens(),
"cumulative_input": usage.cumulative.input_tokens,
"cumulative_output": usage.cumulative.output_tokens,
"cumulative_total": usage.cumulative.total_tokens(),
"estimated_tokens": usage.estimated_tokens,
},
"workspace": {
"cwd": context.cwd,
"project_root": context.project_root,
"git_branch": context.git_branch,
"git_state": context.git_summary.headline(),
"changed_files": context.git_summary.changed_files,
"staged_files": context.git_summary.staged_files,
"unstaged_files": context.git_summary.unstaged_files,
"untracked_files": context.git_summary.untracked_files,
"session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()),
"loaded_config_files": context.loaded_config_files,
"discovered_config_files": context.discovered_config_files,
"memory_file_count": context.memory_file_count,
},
"sandbox": {
"enabled": context.sandbox_status.enabled,
"active": context.sandbox_status.active,
"supported": context.sandbox_status.supported,
"in_container": context.sandbox_status.in_container,
"requested_namespace": context.sandbox_status.requested.namespace_restrictions,
"active_namespace": context.sandbox_status.namespace_active,
"requested_network": context.sandbox_status.requested.network_isolation,
"active_network": context.sandbox_status.network_active,
"filesystem_mode": context.sandbox_status.filesystem_mode.as_str(),
"filesystem_active": context.sandbox_status.filesystem_active,
"allowed_mounts": context.sandbox_status.allowed_mounts,
"markers": context.sandbox_status.container_markers,
"fallback_reason": context.sandbox_status.fallback_reason,
}
}))?
),
}
Ok(())
}
@@ -3838,16 +3876,37 @@ fn format_commit_skipped_report() -> String {
.to_string()
}
fn print_sandbox_status_snapshot() -> Result<(), Box<dyn std::error::Error>> {
fn print_sandbox_status_snapshot(
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
let runtime_config = loader
.load()
.unwrap_or_else(|_| runtime::RuntimeConfig::empty());
println!(
"{}",
format_sandbox_report(&resolve_sandbox_status(runtime_config.sandbox(), &cwd))
);
let status = resolve_sandbox_status(runtime_config.sandbox(), &cwd);
match output_format {
CliOutputFormat::Text => println!("{}", format_sandbox_report(&status)),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "sandbox",
"enabled": status.enabled,
"active": status.active,
"supported": status.supported,
"in_container": status.in_container,
"requested_namespace": status.requested.namespace_restrictions,
"active_namespace": status.namespace_active,
"requested_network": status.requested.network_isolation,
"active_network": status.network_active,
"filesystem_mode": status.filesystem_mode.as_str(),
"filesystem_active": status.filesystem_active,
"allowed_mounts": status.allowed_mounts,
"markers": status.container_markers,
"fallback_reason": status.fallback_reason,
}))?
),
}
Ok(())
}
@@ -6605,11 +6664,14 @@ mod tests {
CliAction::Status {
model: DEFAULT_MODEL.to_string(),
permission_mode: PermissionMode::DangerFullAccess,
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["sandbox".to_string()]).expect("sandbox should parse"),
CliAction::Sandbox
CliAction::Sandbox {
output_format: CliOutputFormat::Text,
}
);
}

View File

@@ -192,12 +192,10 @@ fn doctor_command_runs_as_a_local_shell_entrypoint() {
#[test]
fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
// given
let temp_dir = unique_temp_dir("subcommand-help");
let config_home = temp_dir.join("home").join(".claw");
fs::create_dir_all(&config_home).expect("config home should exist");
// when
let doctor_help = command_in(&temp_dir)
.env("CLAW_CONFIG_HOME", &config_home)
.env_remove("ANTHROPIC_API_KEY")
@@ -215,7 +213,6 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
.output()
.expect("status help should launch");
// then
assert_success(&doctor_help);
let doctor_stdout = String::from_utf8(doctor_help.stdout).expect("stdout should be utf8");
assert!(doctor_stdout.contains("Usage claw doctor"));
@@ -236,121 +233,6 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn nested_help_flags_render_usage_instead_of_falling_through() {
let temp_dir = unique_temp_dir("nested-help");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
let mcp_output = command_in(&temp_dir)
.args(["mcp", "show", "--help"])
.output()
.expect("claw should launch");
assert_success(&mcp_output);
let mcp_stdout = String::from_utf8(mcp_output.stdout).expect("stdout should be utf8");
assert!(mcp_stdout.contains("Usage /mcp [list|show <server>|help]"));
assert!(mcp_stdout.contains("Unexpected show"));
assert!(!mcp_stdout.contains("server `--help` is not configured"));
let skills_output = command_in(&temp_dir)
.args(["skills", "install", "--help"])
.output()
.expect("claw should launch");
assert_success(&skills_output);
let skills_stdout = String::from_utf8(skills_output.stdout).expect("stdout should be utf8");
assert!(skills_stdout.contains("Usage /skills [list|install <path>|help]"));
assert!(skills_stdout.contains("Unexpected install"));
let unknown_output = command_in(&temp_dir)
.args(["mcp", "inspect", "--help"])
.output()
.expect("claw should launch");
assert_success(&unknown_output);
let unknown_stdout = String::from_utf8(unknown_output.stdout).expect("stdout should be utf8");
assert!(unknown_stdout.contains("Usage /mcp [list|show <server>|help]"));
assert!(unknown_stdout.contains("Unexpected inspect"));
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
// given
let temp_dir = unique_temp_dir("subcommand-help");
let config_home = temp_dir.join("home").join(".claw");
fs::create_dir_all(&config_home).expect("config home should exist");
// when
let doctor_help = command_in(&temp_dir)
.env("CLAW_CONFIG_HOME", &config_home)
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN")
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
.args(["doctor", "--help"])
.output()
.expect("doctor help should launch");
let status_help = command_in(&temp_dir)
.env("CLAW_CONFIG_HOME", &config_home)
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN")
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
.args(["status", "--help"])
.output()
.expect("status help should launch");
// then
assert_success(&doctor_help);
let doctor_stdout = String::from_utf8(doctor_help.stdout).expect("stdout should be utf8");
assert!(doctor_stdout.contains("Usage claw doctor"));
assert!(doctor_stdout.contains("local-only health report"));
assert!(!doctor_stdout.contains("Thinking"));
assert_success(&status_help);
let status_stdout = String::from_utf8(status_help.stdout).expect("stdout should be utf8");
assert!(status_stdout.contains("Usage claw status"));
assert!(status_stdout.contains("local workspace snapshot"));
assert!(!status_stdout.contains("Thinking"));
let doctor_stderr = String::from_utf8(doctor_help.stderr).expect("stderr should be utf8");
let status_stderr = String::from_utf8(status_help.stderr).expect("stderr should be utf8");
assert!(!doctor_stderr.contains("auth_unavailable"));
assert!(!status_stderr.contains("auth_unavailable"));
=======
fn nested_help_flags_render_usage_instead_of_falling_through() {
let temp_dir = unique_temp_dir("nested-help");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
let mcp_output = command_in(&temp_dir)
.args(["mcp", "show", "--help"])
.output()
.expect("claw should launch");
assert_success(&mcp_output);
let mcp_stdout = String::from_utf8(mcp_output.stdout).expect("stdout should be utf8");
assert!(mcp_stdout.contains("Usage /mcp [list|show <server>|help]"));
assert!(mcp_stdout.contains("Unexpected show"));
assert!(!mcp_stdout.contains("server `--help` is not configured"));
let skills_output = command_in(&temp_dir)
.args(["skills", "install", "--help"])
.output()
.expect("claw should launch");
assert_success(&skills_output);
let skills_stdout = String::from_utf8(skills_output.stdout).expect("stdout should be utf8");
assert!(skills_stdout.contains("Usage /skills [list|install <path>|help]"));
assert!(skills_stdout.contains("Unexpected install"));
let unknown_output = command_in(&temp_dir)
.args(["mcp", "inspect", "--help"])
.output()
.expect("claw should launch");
assert_success(&unknown_output);
let unknown_stdout = String::from_utf8(unknown_output.stdout).expect("stdout should be utf8");
assert!(unknown_stdout.contains("Usage /mcp [list|show <server>|help]"));
assert!(unknown_stdout.contains("Unexpected inspect"));
>>>>>>> 1f53d96 (Route nested CLI help requests to usage instead of operand fallthrough)
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);