mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-30 22:06:43 -04:00
Compare commits
6 Commits
f33c315c93
...
50e3fa3a83
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50e3fa3a83 | ||
|
|
a51b2105ed | ||
|
|
a3270db602 | ||
|
|
12f1f9a74e | ||
|
|
2678fa0af5 | ||
|
|
b9990bb27c |
9
USAGE.md
9
USAGE.md
@@ -43,6 +43,15 @@ cd rust
|
||||
/doctor
|
||||
```
|
||||
|
||||
Or run doctor directly with JSON output for scripting:
|
||||
|
||||
```bash
|
||||
cd rust
|
||||
./target/debug/claw doctor --output-format json
|
||||
```
|
||||
|
||||
**Note:** Diagnostic verbs (`doctor`, `status`, `sandbox`, `version`) support `--output-format json` for machine-readable output. Invalid suffix arguments (e.g., `--json`) are now rejected at parse time rather than falling through to prompt dispatch.
|
||||
|
||||
### Interactive REPL
|
||||
|
||||
```bash
|
||||
|
||||
@@ -8,6 +8,7 @@ use tokio::process::Command as TokioCommand;
|
||||
use tokio::runtime::Builder;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::lane_events::{LaneEvent, ShipMergeMethod, ShipProvenance};
|
||||
use crate::sandbox::{
|
||||
build_linux_sandbox_command, resolve_sandbox_status_for_request, FilesystemIsolationMode,
|
||||
SandboxConfig, SandboxStatus,
|
||||
@@ -102,11 +103,76 @@ pub fn execute_bash(input: BashCommandInput) -> io::Result<BashCommandOutput> {
|
||||
runtime.block_on(execute_bash_async(input, sandbox_status, cwd))
|
||||
}
|
||||
|
||||
/// Detect git push to main and emit ship provenance event
|
||||
fn detect_and_emit_ship_prepared(command: &str) {
|
||||
let trimmed = command.trim();
|
||||
// Simple detection: git push with main/master
|
||||
if trimmed.contains("git push") && (trimmed.contains("main") || trimmed.contains("master")) {
|
||||
// Emit ship.prepared event
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let provenance = ShipProvenance {
|
||||
source_branch: get_current_branch().unwrap_or_else(|| "unknown".to_string()),
|
||||
base_commit: get_head_commit().unwrap_or_default(),
|
||||
commit_count: 0, // Would need to calculate from range
|
||||
commit_range: "unknown..HEAD".to_string(),
|
||||
merge_method: ShipMergeMethod::DirectPush,
|
||||
actor: get_git_actor().unwrap_or_else(|| "unknown".to_string()),
|
||||
pr_number: None,
|
||||
};
|
||||
let _event = LaneEvent::ship_prepared(format!("{}", now), &provenance);
|
||||
// Log to stderr as interim routing before event stream integration
|
||||
eprintln!(
|
||||
"[ship.prepared] branch={} -> main, commits={}, actor={}",
|
||||
provenance.source_branch, provenance.commit_count, provenance.actor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_branch() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(["branch", "--show-current"])
|
||||
.output()
|
||||
.ok()?;
|
||||
if output.status.success() {
|
||||
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_head_commit() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.output()
|
||||
.ok()?;
|
||||
if output.status.success() {
|
||||
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_git_actor() -> Option<String> {
|
||||
let name = Command::new("git")
|
||||
.args(["config", "user.name"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())?;
|
||||
Some(name)
|
||||
}
|
||||
|
||||
async fn execute_bash_async(
|
||||
input: BashCommandInput,
|
||||
sandbox_status: SandboxStatus,
|
||||
cwd: std::path::PathBuf,
|
||||
) -> io::Result<BashCommandOutput> {
|
||||
// Detect and emit ship provenance for git push operations
|
||||
detect_and_emit_ship_prepared(&input.command);
|
||||
|
||||
let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true);
|
||||
|
||||
let output_result = if let Some(timeout_ms) = input.timeout {
|
||||
|
||||
@@ -447,11 +447,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| "missing value for --model".to_string())?;
|
||||
validate_model_syntax(value)?;
|
||||
model = resolve_model_alias_with_config(value);
|
||||
index += 2;
|
||||
}
|
||||
flag if flag.starts_with("--model=") => {
|
||||
model = resolve_model_alias_with_config(&flag[8..]);
|
||||
let value = &flag[8..];
|
||||
validate_model_syntax(value)?;
|
||||
model = resolve_model_alias_with_config(value);
|
||||
index += 1;
|
||||
}
|
||||
"--output-format" => {
|
||||
@@ -743,6 +746,31 @@ fn parse_single_word_command_alias(
|
||||
permission_mode_override: Option<PermissionMode>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Option<Result<CliAction, String>> {
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Diagnostic verbs (help, version, status, sandbox, doctor, state) accept only the verb itself
|
||||
// or --help / -h as a suffix. Any other suffix args are unrecognized.
|
||||
let verb = &rest[0];
|
||||
let is_diagnostic = matches!(
|
||||
verb.as_str(),
|
||||
"help" | "version" | "status" | "sandbox" | "doctor" | "state"
|
||||
);
|
||||
|
||||
if is_diagnostic && rest.len() > 1 {
|
||||
// Diagnostic verb with trailing args: reject unrecognized suffix
|
||||
if is_help_flag(&rest[1]) && rest.len() == 2 {
|
||||
// "doctor --help" is valid, routed to parse_local_help_action() instead
|
||||
return None;
|
||||
}
|
||||
// Unrecognized suffix like "--json"
|
||||
return Some(Err(format!(
|
||||
"unrecognized argument `{}` for subcommand `{}`",
|
||||
rest[1], verb
|
||||
)));
|
||||
}
|
||||
|
||||
if rest.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
@@ -1035,6 +1063,37 @@ fn resolve_model_alias_with_config(model: &str) -> String {
|
||||
resolve_model_alias(trimmed).to_string()
|
||||
}
|
||||
|
||||
/// Validate model syntax at parse time.
|
||||
/// Accepts: known aliases (opus, sonnet, haiku) or provider/model pattern.
|
||||
/// Rejects: empty, whitespace-only, strings with spaces, or invalid chars.
|
||||
fn validate_model_syntax(model: &str) -> Result<(), String> {
|
||||
let trimmed = model.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("model string cannot be empty".to_string());
|
||||
}
|
||||
// Known aliases are always valid
|
||||
match trimmed {
|
||||
"opus" | "sonnet" | "haiku" => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
// Check for spaces (malformed)
|
||||
if trimmed.contains(' ') {
|
||||
return Err(format!(
|
||||
"invalid model syntax: '{}' contains spaces. Use provider/model format or known alias",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
// Check provider/model format: provider_id/model_id
|
||||
let parts: Vec<&str> = trimmed.split('/').collect();
|
||||
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
|
||||
return Err(format!(
|
||||
"invalid model syntax: '{}'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config_alias_for_current_dir(alias: &str) -> Option<String> {
|
||||
if alias.is_empty() {
|
||||
return None;
|
||||
@@ -1508,10 +1567,7 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
|
||||
check_sandbox_health(&context.sandbox_status),
|
||||
check_system_health(&cwd, config.as_ref().ok()),
|
||||
],
|
||||
});
|
||||
// Run stale-base preflight check — emits warnings to stderr if branch is behind main
|
||||
run_stale_base_preflight(None);
|
||||
Ok(report)
|
||||
})
|
||||
}
|
||||
|
||||
fn run_doctor(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -5184,28 +5240,32 @@ fn sandbox_json_value(status: &runtime::SandboxStatus) -> serde_json::Value {
|
||||
fn render_help_topic(topic: LocalHelpTopic) -> String {
|
||||
match topic {
|
||||
LocalHelpTopic::Status => "Status
|
||||
Usage claw status
|
||||
Usage claw status [--output-format <format>]
|
||||
Purpose show the local workspace snapshot without entering the REPL
|
||||
Output model, permissions, git state, config files, and sandbox status
|
||||
Formats text (default), json
|
||||
Related /status · claw --resume latest /status"
|
||||
.to_string(),
|
||||
LocalHelpTopic::Sandbox => "Sandbox
|
||||
Usage claw sandbox
|
||||
Usage claw sandbox [--output-format <format>]
|
||||
Purpose inspect the resolved sandbox and isolation state for the current directory
|
||||
Output namespace, network, filesystem, and fallback details
|
||||
Formats text (default), json
|
||||
Related /sandbox · claw status"
|
||||
.to_string(),
|
||||
LocalHelpTopic::Doctor => "Doctor
|
||||
Usage claw doctor
|
||||
Usage claw doctor [--output-format <format>]
|
||||
Purpose diagnose local auth, config, workspace, sandbox, and build metadata
|
||||
Output local-only health report; no provider request or session resume required
|
||||
Formats text (default), json
|
||||
Related /doctor · claw --resume latest /doctor"
|
||||
.to_string(),
|
||||
LocalHelpTopic::Acp => "ACP / Zed
|
||||
Usage claw acp [serve]
|
||||
Usage claw acp [serve] [--output-format <format>]
|
||||
Aliases claw --acp · claw -acp
|
||||
Purpose explain the current editor-facing ACP/Zed launch contract without starting the runtime
|
||||
Status discoverability only; `serve` is a status alias and does not launch a daemon yet
|
||||
Formats text (default), json
|
||||
Related ROADMAP #64a (discoverability) · ROADMAP #76 (real ACP support) · claw --help"
|
||||
.to_string(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user