mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-07 00:24:50 +08:00
feat(commands): slash-command validation, help formatting, CLI wiring
- Add centralized validate_slash_command_input for all slash commands - Rich error messages and per-command help detail - Wire validation into CLI entrypoints in main.rs - Consistent /agents and /skills usage surface - Verified: cargo test -p commands 22 passed, integration test passed, clippy clean
This commit is contained in:
@@ -22,7 +22,8 @@ use api::{
|
||||
|
||||
use commands::{
|
||||
handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command,
|
||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs, SlashCommand,
|
||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
||||
validate_slash_command_input, SlashCommand,
|
||||
};
|
||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||
use init::initialize_repo;
|
||||
@@ -422,12 +423,12 @@ fn join_optional_args(args: &[String]) -> Option<String> {
|
||||
|
||||
fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
||||
let raw = rest.join(" ");
|
||||
match SlashCommand::parse(&raw) {
|
||||
Some(SlashCommand::Help) => Ok(CliAction::Help),
|
||||
Some(SlashCommand::Agents { args }) => Ok(CliAction::Agents { args }),
|
||||
Some(SlashCommand::Skills { args }) => Ok(CliAction::Skills { args }),
|
||||
Some(SlashCommand::Unknown(name)) => Err(format_unknown_direct_slash_command(&name)),
|
||||
Some(command) => Err({
|
||||
match validate_slash_command_input(&raw) {
|
||||
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help),
|
||||
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents { args }),
|
||||
Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills { args }),
|
||||
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
|
||||
Ok(Some(command)) => Err({
|
||||
let _ = command;
|
||||
format!(
|
||||
"slash command {command_name} is interactive-only. Start `claw` and run it there, or use `claw --resume SESSION.jsonl {command_name}` / `claw --resume {latest} {command_name}` when the command is marked [resume] in /help.",
|
||||
@@ -435,7 +436,8 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
||||
latest = LATEST_SESSION_REFERENCE,
|
||||
)
|
||||
}),
|
||||
None => Err(format!("unknown subcommand: {}", rest[0])),
|
||||
Ok(None) => Err(format!("unknown subcommand: {}", rest[0])),
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,9 +898,16 @@ fn resume_session(session_path: &Path, commands: &[String]) {
|
||||
|
||||
let mut session = session;
|
||||
for raw_command in commands {
|
||||
let Some(command) = SlashCommand::parse(raw_command) else {
|
||||
eprintln!("unsupported resumed command: {raw_command}");
|
||||
std::process::exit(2);
|
||||
let command = match validate_slash_command_input(raw_command) {
|
||||
Ok(Some(command)) => command,
|
||||
Ok(None) => {
|
||||
eprintln!("unsupported resumed command: {raw_command}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
match run_resume_command(&resolved_path, &session, &command) {
|
||||
Ok(ResumeCommandOutcome {
|
||||
@@ -1417,11 +1426,18 @@ fn run_repl(
|
||||
cli.persist_session()?;
|
||||
break;
|
||||
}
|
||||
if let Some(command) = SlashCommand::parse(&trimmed) {
|
||||
if cli.handle_repl_command(command)? {
|
||||
cli.persist_session()?;
|
||||
match validate_slash_command_input(&trimmed) {
|
||||
Ok(Some(command)) => {
|
||||
if cli.handle_repl_command(command)? {
|
||||
cli.persist_session()?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
editor.push_history(input);
|
||||
cli.run_turn(&trimmed)?;
|
||||
@@ -5246,6 +5262,23 @@ mod tests {
|
||||
assert!(error.contains("claw --resume SESSION.jsonl /status"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_slash_commands_surface_shared_validation_errors() {
|
||||
let compact_error = parse_args(&["/compact".to_string(), "now".to_string()])
|
||||
.expect_err("invalid /compact shape should be rejected");
|
||||
assert!(compact_error.contains("Unexpected arguments for /compact."));
|
||||
assert!(compact_error.contains("Usage /compact"));
|
||||
|
||||
let plugins_error = parse_args(&[
|
||||
"/plugins".to_string(),
|
||||
"list".to_string(),
|
||||
"extra".to_string(),
|
||||
])
|
||||
.expect_err("invalid /plugins list shape should be rejected");
|
||||
assert!(plugins_error.contains("Usage: /plugin list"));
|
||||
assert!(plugins_error.contains("Aliases /plugins, /marketplace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_unknown_slash_command_with_suggestions() {
|
||||
let report = format_unknown_slash_command_message("stats");
|
||||
|
||||
Reference in New Issue
Block a user