mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-27 00:54:58 +08:00
feat: #146 — wire claw config and claw diff as standalone subcommands
## Problem `claw config` and `claw diff` are pure-local read-only introspection commands (config merges .claw.json + .claw/settings.json from disk; diff shells out to `git diff --cached` + `git diff`). Neither needs a session context, yet both rejected direct CLI invocation: $ claw config error: `claw config` is a slash command. Use `claw --resume SESSION.jsonl /config` ... $ claw diff error: `claw diff` is a slash command. ... This forced clawing operators to spin up a full session just to inspect static disk state, and broke natural pipelines like `claw config --output-format json | jq`. ## Root cause Sibling of #145: `SlashCommand::Config { section }` and `SlashCommand::Diff` had working renderers (`render_config_report`, `render_config_json`, `render_diff_report`, `render_diff_json_for`) exposed for resume sessions, but the top-level CLI parser in `parse_subcommand()` had no arms for them. Zero-arg `config`/`diff` hit `parse_single_word_command_alias`'s fallback to `bare_slash_command_guidance`, producing the misleading guidance. ## Changes ### rust/crates/rusty-claude-cli/src/main.rs - Added `CliAction::Config { section, output_format }` and `CliAction::Diff { output_format }` variants. - Added `"config"` / `"diff"` arms to the top-level parser in `parse_subcommand()`. `config` accepts an optional section name (env|hooks|model|plugins) matching SlashCommand::Config semantics. `diff` takes no positional args. Both reject extra trailing args with a clear error. - Added `"config" | "diff" => None` to `parse_single_word_command_alias` so bare invocations fall through to the new parser arms instead of the slash-guidance error. - Added dispatch in run() that calls existing renderers: text mode uses `render_config_report` / `render_diff_report`; JSON mode uses `render_config_json` / `render_diff_json_for` with `serde_json::to_string_pretty`. - Added 5 regression assertions in parse_args test covering: parse_args(["config"]), parse_args(["config", "env"]), parse_args(["config", "--output-format", "json"]), parse_args(["diff"]), parse_args(["diff", "--output-format", "json"]). ### ROADMAP.md Added Pinpoint #146 documenting the gap, verification, root cause, fix shape, and acceptance. Explicitly notes which other slash commands (`hooks`, `usage`, `context`, etc.) are NOT candidates because they are session-state-modifying. ## Live verification $ claw config # no config files Config Working directory /private/tmp/cd-146-verify Loaded files 0 Merged keys 0 Discovered files user missing ... project missing ... local missing ... Exit 0. $ claw config --output-format json { "cwd": "...", "files": [...], ... } $ claw diff # no git Diff Result no git repository Detail ... Exit 0. $ claw diff --output-format json # inside claw-code { "kind": "diff", "result": "changes", "staged": "", "unstaged": "diff --git ..." } Exit 0. ## Tests - rusty-claude-cli bin: 177 tests pass (5 new assertions in parse_args) - Full workspace green except pre-existing resume_latest flake (unrelated) ## Not changed `hooks`, `usage`, `context`, `tasks`, `theme`, `voice`, `rename`, `copy`, `color`, `effort`, `branch`, `rewind`, `ide`, `tag`, `output-style`, `add-dir` — all session-mutating or interactive-only; correctly remain slash-only. Closes ROADMAP #146.
This commit is contained in:
@@ -253,6 +253,37 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
CliAction::Acp { output_format } => print_acp_status(output_format)?,
|
||||
CliAction::State { output_format } => run_worker_state(output_format)?,
|
||||
CliAction::Init { output_format } => run_init(output_format)?,
|
||||
// #146: dispatch pure-local introspection. Text mode uses existing
|
||||
// render_config_report/render_diff_report; JSON mode uses the
|
||||
// corresponding _json helpers already exposed for resume sessions.
|
||||
CliAction::Config {
|
||||
section,
|
||||
output_format,
|
||||
} => {
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{}", render_config_report(section.as_deref())?);
|
||||
}
|
||||
CliOutputFormat::Json => {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&render_config_json(section.as_deref())?)?
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
CliAction::Diff { output_format } => match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{}", render_diff_report()?);
|
||||
}
|
||||
CliOutputFormat::Json => {
|
||||
let cwd = env::current_dir()?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&render_diff_json_for(&cwd)?)?
|
||||
);
|
||||
}
|
||||
},
|
||||
CliAction::Export {
|
||||
session_reference,
|
||||
output_path,
|
||||
@@ -349,6 +380,15 @@ enum CliAction {
|
||||
Init {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
// #146: `claw config` and `claw diff` are pure-local read-only
|
||||
// introspection commands; wire them as standalone CLI subcommands.
|
||||
Config {
|
||||
section: Option<String>,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Diff {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Export {
|
||||
session_reference: String,
|
||||
output_path: Option<PathBuf>,
|
||||
@@ -685,6 +725,38 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
// #146: `config` is pure-local read-only introspection (merges
|
||||
// `.claw.json` + `.claw/settings.json` from disk, no network, no
|
||||
// state mutation). Previously callers had to spin up a session with
|
||||
// `claw --resume SESSION.jsonl /config` to see their own config,
|
||||
// which is synthetic friction. Accepts an optional section name
|
||||
// (env|hooks|model|plugins) matching the slash command shape.
|
||||
"config" => {
|
||||
let tail = &rest[1..];
|
||||
let section = tail.first().cloned();
|
||||
if tail.len() > 1 {
|
||||
return Err(format!(
|
||||
"unexpected extra arguments after `claw config {}`: {}",
|
||||
tail[0],
|
||||
tail[1..].join(" ")
|
||||
));
|
||||
}
|
||||
Ok(CliAction::Config {
|
||||
section,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
// #146: `diff` is pure-local (shells out to `git diff --cached` +
|
||||
// `git diff`). No session needed to inspect the working tree.
|
||||
"diff" => {
|
||||
if rest.len() > 1 {
|
||||
return Err(format!(
|
||||
"unexpected extra arguments after `claw diff`: {}",
|
||||
rest[1..].join(" ")
|
||||
));
|
||||
}
|
||||
Ok(CliAction::Diff { output_format })
|
||||
}
|
||||
"skills" => {
|
||||
let args = join_optional_args(&rest[1..]);
|
||||
match classify_skills_slash_command(args.as_deref()) {
|
||||
@@ -843,6 +915,11 @@ fn parse_single_word_command_alias(
|
||||
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
|
||||
"doctor" => Some(Ok(CliAction::Doctor { output_format })),
|
||||
"state" => Some(Ok(CliAction::State { output_format })),
|
||||
// #146: let `config` and `diff` fall through to parse_subcommand
|
||||
// where they are wired as pure-local introspection, instead of
|
||||
// producing the "is a slash command" guidance. Zero-arg cases
|
||||
// reach parse_subcommand too via this None.
|
||||
"config" | "diff" => None,
|
||||
other => bare_slash_command_guidance(other).map(Err),
|
||||
}
|
||||
}
|
||||
@@ -9619,6 +9696,53 @@ mod tests {
|
||||
output_format: CliOutputFormat::Json,
|
||||
}
|
||||
);
|
||||
// #146: `config` and `diff` must parse as standalone CLI actions,
|
||||
// not fall through to the "is a slash command" error. Both are
|
||||
// pure-local read-only introspection.
|
||||
assert_eq!(
|
||||
parse_args(&["config".to_string()]).expect("config should parse"),
|
||||
CliAction::Config {
|
||||
section: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["config".to_string(), "env".to_string()])
|
||||
.expect("config env should parse"),
|
||||
CliAction::Config {
|
||||
section: Some("env".to_string()),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"config".to_string(),
|
||||
"--output-format".to_string(),
|
||||
"json".to_string(),
|
||||
])
|
||||
.expect("config --output-format json should parse"),
|
||||
CliAction::Config {
|
||||
section: None,
|
||||
output_format: CliOutputFormat::Json,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["diff".to_string()]).expect("diff should parse"),
|
||||
CliAction::Diff {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"diff".to_string(),
|
||||
"--output-format".to_string(),
|
||||
"json".to_string(),
|
||||
])
|
||||
.expect("diff --output-format json should parse"),
|
||||
CliAction::Diff {
|
||||
output_format: CliOutputFormat::Json,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user