mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
merge fix/p2-19-subcommand-help-fallthrough
This commit is contained in:
@@ -2142,13 +2142,22 @@ pub fn handle_plugins_slash_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
||||||
|
if let Some(args) = normalize_optional_args(args) {
|
||||||
|
if let Some(help_path) = help_path_from_args(args) {
|
||||||
|
return Ok(match help_path.as_slice() {
|
||||||
|
[] => render_agents_usage(None),
|
||||||
|
_ => render_agents_usage(Some(&help_path.join(" "))),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list") => {
|
None | Some("list") => {
|
||||||
let roots = discover_definition_roots(cwd, "agents");
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
let agents = load_agents_from_roots(&roots)?;
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
Ok(render_agents_report(&agents))
|
Ok(render_agents_report(&agents))
|
||||||
}
|
}
|
||||||
Some("-h" | "--help" | "help") => Ok(render_agents_usage(None)),
|
Some(args) if is_help_arg(args) => Ok(render_agents_usage(None)),
|
||||||
Some(args) => Ok(render_agents_usage(Some(args))),
|
Some(args) => Ok(render_agents_usage(Some(args))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2162,6 +2171,16 @@ pub fn handle_mcp_slash_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
||||||
|
if let Some(args) = normalize_optional_args(args) {
|
||||||
|
if let Some(help_path) = help_path_from_args(args) {
|
||||||
|
return Ok(match help_path.as_slice() {
|
||||||
|
[] => render_skills_usage(None),
|
||||||
|
["install", ..] => render_skills_usage(Some("install")),
|
||||||
|
_ => render_skills_usage(Some(&help_path.join(" "))),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list") => {
|
None | Some("list") => {
|
||||||
let roots = discover_skill_roots(cwd);
|
let roots = discover_skill_roots(cwd);
|
||||||
@@ -2177,7 +2196,7 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
|||||||
let install = install_skill(target, cwd)?;
|
let install = install_skill(target, cwd)?;
|
||||||
Ok(render_skill_install_report(&install))
|
Ok(render_skill_install_report(&install))
|
||||||
}
|
}
|
||||||
Some("-h" | "--help" | "help") => Ok(render_skills_usage(None)),
|
Some(args) if is_help_arg(args) => Ok(render_skills_usage(None)),
|
||||||
Some(args) => Ok(render_skills_usage(Some(args))),
|
Some(args) => Ok(render_skills_usage(Some(args))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2187,6 +2206,16 @@ fn render_mcp_report_for(
|
|||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
args: Option<&str>,
|
args: Option<&str>,
|
||||||
) -> Result<String, runtime::ConfigError> {
|
) -> Result<String, runtime::ConfigError> {
|
||||||
|
if let Some(args) = normalize_optional_args(args) {
|
||||||
|
if let Some(help_path) = help_path_from_args(args) {
|
||||||
|
return Ok(match help_path.as_slice() {
|
||||||
|
[] => render_mcp_usage(None),
|
||||||
|
["show", ..] => render_mcp_usage(Some("show")),
|
||||||
|
_ => render_mcp_usage(Some(&help_path.join(" "))),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list") => {
|
None | Some("list") => {
|
||||||
let runtime_config = loader.load()?;
|
let runtime_config = loader.load()?;
|
||||||
@@ -2195,7 +2224,7 @@ fn render_mcp_report_for(
|
|||||||
runtime_config.mcp().servers(),
|
runtime_config.mcp().servers(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Some("-h" | "--help" | "help") => Ok(render_mcp_usage(None)),
|
Some(args) if is_help_arg(args) => Ok(render_mcp_usage(None)),
|
||||||
Some("show") => Ok(render_mcp_usage(Some("show"))),
|
Some("show") => Ok(render_mcp_usage(Some("show"))),
|
||||||
Some(args) if args.split_whitespace().next() == Some("show") => {
|
Some(args) if args.split_whitespace().next() == Some("show") => {
|
||||||
let mut parts = args.split_whitespace();
|
let mut parts = args.split_whitespace();
|
||||||
@@ -3036,6 +3065,16 @@ fn normalize_optional_args(args: Option<&str>) -> Option<&str> {
|
|||||||
args.map(str::trim).filter(|value| !value.is_empty())
|
args.map(str::trim).filter(|value| !value.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_help_arg(arg: &str) -> bool {
|
||||||
|
matches!(arg, "help" | "-h" | "--help")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn help_path_from_args(args: &str) -> Option<Vec<&str>> {
|
||||||
|
let parts = args.split_whitespace().collect::<Vec<_>>();
|
||||||
|
let help_index = parts.iter().position(|part| is_help_arg(part))?;
|
||||||
|
Some(parts[..help_index].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
fn render_agents_usage(unexpected: Option<&str>) -> String {
|
fn render_agents_usage(unexpected: Option<&str>) -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
"Agents".to_string(),
|
"Agents".to_string(),
|
||||||
@@ -4005,7 +4044,17 @@ mod tests {
|
|||||||
|
|
||||||
let skills_unexpected =
|
let skills_unexpected =
|
||||||
super::handle_skills_slash_command(Some("show help"), &cwd).expect("skills usage");
|
super::handle_skills_slash_command(Some("show help"), &cwd).expect("skills usage");
|
||||||
assert!(skills_unexpected.contains("Unexpected show help"));
|
assert!(skills_unexpected.contains("Unexpected show"));
|
||||||
|
|
||||||
|
let skills_install_help = super::handle_skills_slash_command(Some("install --help"), &cwd)
|
||||||
|
.expect("nested skills help");
|
||||||
|
assert!(skills_install_help.contains("Usage /skills [list|install <path>|help]"));
|
||||||
|
assert!(skills_install_help.contains("Unexpected install"));
|
||||||
|
|
||||||
|
let skills_unknown_help =
|
||||||
|
super::handle_skills_slash_command(Some("show --help"), &cwd).expect("skills help");
|
||||||
|
assert!(skills_unknown_help.contains("Usage /skills [list|install <path>|help]"));
|
||||||
|
assert!(skills_unknown_help.contains("Unexpected show"));
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(cwd);
|
let _ = fs::remove_dir_all(cwd);
|
||||||
}
|
}
|
||||||
@@ -4022,6 +4071,16 @@ mod tests {
|
|||||||
super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage");
|
super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage");
|
||||||
assert!(unexpected.contains("Unexpected show alpha beta"));
|
assert!(unexpected.contains("Unexpected show alpha beta"));
|
||||||
|
|
||||||
|
let nested_help =
|
||||||
|
super::handle_mcp_slash_command(Some("show --help"), &cwd).expect("mcp help");
|
||||||
|
assert!(nested_help.contains("Usage /mcp [list|show <server>|help]"));
|
||||||
|
assert!(nested_help.contains("Unexpected show"));
|
||||||
|
|
||||||
|
let unknown_help =
|
||||||
|
super::handle_mcp_slash_command(Some("inspect --help"), &cwd).expect("mcp usage");
|
||||||
|
assert!(unknown_help.contains("Usage /mcp [list|show <server>|help]"));
|
||||||
|
assert!(unknown_help.contains("Unexpected inspect"));
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(cwd);
|
let _ = fs::remove_dir_all(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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::Value;
|
|
||||||
|
|
||||||
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
@@ -38,64 +37,6 @@ fn status_command_applies_model_and_permission_mode_flags() {
|
|||||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn status_command_emits_structured_json_when_requested() {
|
|
||||||
// given
|
|
||||||
let temp_dir = unique_temp_dir("status-json");
|
|
||||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
|
||||||
|
|
||||||
// when
|
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
|
|
||||||
.current_dir(&temp_dir)
|
|
||||||
.args([
|
|
||||||
"--model",
|
|
||||||
"sonnet",
|
|
||||||
"--permission-mode",
|
|
||||||
"read-only",
|
|
||||||
"--output-format",
|
|
||||||
"json",
|
|
||||||
"status",
|
|
||||||
])
|
|
||||||
.output()
|
|
||||||
.expect("claw should launch");
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert_success(&output);
|
|
||||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
|
||||||
let parsed: Value = serde_json::from_str(stdout.trim()).expect("status output should be json");
|
|
||||||
assert_eq!(parsed["kind"], "status");
|
|
||||||
assert_eq!(parsed["model"], "claude-sonnet-4-6");
|
|
||||||
assert_eq!(parsed["permission_mode"], "read-only");
|
|
||||||
assert_eq!(parsed["workspace"]["session"], "live-repl");
|
|
||||||
assert!(parsed["sandbox"].is_object());
|
|
||||||
|
|
||||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sandbox_command_emits_structured_json_when_requested() {
|
|
||||||
// given
|
|
||||||
let temp_dir = unique_temp_dir("sandbox-json");
|
|
||||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
|
||||||
|
|
||||||
// when
|
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
|
|
||||||
.current_dir(&temp_dir)
|
|
||||||
.args(["--output-format", "json", "sandbox"])
|
|
||||||
.output()
|
|
||||||
.expect("claw should launch");
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert_success(&output);
|
|
||||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
|
||||||
let parsed: Value = serde_json::from_str(stdout.trim()).expect("sandbox output should be json");
|
|
||||||
assert_eq!(parsed["kind"], "sandbox");
|
|
||||||
assert!(parsed["sandbox"].is_object());
|
|
||||||
assert!(parsed["sandbox"]["requested"].is_object());
|
|
||||||
|
|
||||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resume_flag_loads_a_saved_session_and_dispatches_status() {
|
fn resume_flag_loads_a_saved_session_and_dispatches_status() {
|
||||||
// given
|
// given
|
||||||
@@ -220,77 +161,37 @@ fn config_command_loads_defaults_from_standard_config_locations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doctor_command_runs_as_a_local_shell_entrypoint() {
|
fn nested_help_flags_render_usage_instead_of_falling_through() {
|
||||||
// given
|
let temp_dir = unique_temp_dir("nested-help");
|
||||||
let temp_dir = unique_temp_dir("doctor-entrypoint");
|
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||||
let config_home = temp_dir.join("home").join(".claw");
|
|
||||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
|
||||||
|
|
||||||
// when
|
let mcp_output = command_in(&temp_dir)
|
||||||
let output = command_in(&temp_dir)
|
.args(["mcp", "show", "--help"])
|
||||||
.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")
|
|
||||||
.arg("doctor")
|
|
||||||
.output()
|
.output()
|
||||||
.expect("claw doctor should launch");
|
.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"));
|
||||||
|
|
||||||
// then
|
let skills_output = command_in(&temp_dir)
|
||||||
assert_success(&output);
|
.args(["skills", "install", "--help"])
|
||||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
|
||||||
assert!(stdout.contains("Doctor"));
|
|
||||||
assert!(stdout.contains("Auth"));
|
|
||||||
assert!(stdout.contains("Config"));
|
|
||||||
assert!(stdout.contains("Workspace"));
|
|
||||||
assert!(stdout.contains("Sandbox"));
|
|
||||||
assert!(!stdout.contains("Thinking"));
|
|
||||||
|
|
||||||
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()
|
.output()
|
||||||
.expect("doctor help should launch");
|
.expect("claw should launch");
|
||||||
let status_help = command_in(&temp_dir)
|
assert_success(&skills_output);
|
||||||
.env("CLAW_CONFIG_HOME", &config_home)
|
let skills_stdout = String::from_utf8(skills_output.stdout).expect("stdout should be utf8");
|
||||||
.env_remove("ANTHROPIC_API_KEY")
|
assert!(skills_stdout.contains("Usage /skills [list|install <path>|help]"));
|
||||||
.env_remove("ANTHROPIC_AUTH_TOKEN")
|
assert!(skills_stdout.contains("Unexpected install"));
|
||||||
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
|
|
||||||
.args(["status", "--help"])
|
let unknown_output = command_in(&temp_dir)
|
||||||
|
.args(["mcp", "inspect", "--help"])
|
||||||
.output()
|
.output()
|
||||||
.expect("status help should launch");
|
.expect("claw should launch");
|
||||||
|
assert_success(&unknown_output);
|
||||||
// then
|
let unknown_stdout = String::from_utf8(unknown_output.stdout).expect("stdout should be utf8");
|
||||||
assert_success(&doctor_help);
|
assert!(unknown_stdout.contains("Usage /mcp [list|show <server>|help]"));
|
||||||
let doctor_stdout = String::from_utf8(doctor_help.stdout).expect("stdout should be utf8");
|
assert!(unknown_stdout.contains("Unexpected inspect"));
|
||||||
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"));
|
|
||||||
|
|
||||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user