diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-10 15:24:24 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-10 15:33:45 +0300 |
| commit | 36a1b7f86444871f0c38ff36510a6e3c2eec48c6 (patch) | |
| tree | 4a2d100eef7f26af7feaa93ce661423c3994c54e /src/cli/agent.rs | |
| parent | 8d13492926829c5cf0ea42c8613344cd65ff59fa (diff) | |
feat: distribute agent detection manifests
Diffstat (limited to 'src/cli/agent.rs')
| -rw-r--r-- | src/cli/agent.rs | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/src/cli/agent.rs b/src/cli/agent.rs index 5005eda1..24f1880d 100644 --- a/src/cli/agent.rs +++ b/src/cli/agent.rs @@ -19,6 +19,7 @@ pub(super) fn run_agent_command(args: &[String]) -> std::io::Result<i32> { "wait" => agent_wait(&args[1..]), "attach" => agent_attach(&args[1..]), "start" => agent_start(&args[1..]), + "explain" => agent_explain(&args[1..]), "help" | "--help" | "-h" => { print_agent_help(); Ok(0) @@ -30,6 +31,219 @@ pub(super) fn run_agent_command(args: &[String]) -> std::io::Result<i32> { } } +fn agent_explain(args: &[String]) -> std::io::Result<i32> { + let mut file = None; + let mut agent = None; + let mut json = false; + let mut target = None; + + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--file" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --file"); + return Ok(2); + }; + file = Some(value.clone()); + index += 2; + } + "--agent" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --agent"); + return Ok(2); + }; + agent = Some(value.clone()); + index += 2; + } + "--json" => { + json = true; + index += 1; + } + "--format" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --format"); + return Ok(2); + }; + match value.as_str() { + "json" => json = true, + "text" => json = false, + other => { + eprintln!("invalid --format: {other} (expected text or json)"); + return Ok(2); + } + } + index += 2; + } + "help" | "--help" | "-h" => { + eprintln!("usage: herdr agent explain <target> [--json]"); + eprintln!("usage: herdr agent explain --file PATH --agent LABEL [--json]"); + return Ok(0); + } + value if value.starts_with('-') => { + eprintln!("unknown option: {value}"); + return Ok(2); + } + value => { + if target.is_some() { + eprintln!("usage: herdr agent explain <target> [--json]"); + return Ok(2); + } + target = Some(value.to_string()); + index += 1; + } + } + } + + let explain = if let Some(path) = file { + if target.is_some() { + eprintln!("usage: herdr agent explain --file PATH --agent LABEL [--json]"); + return Ok(2); + } + let Some(agent_label) = agent else { + eprintln!("herdr agent explain --file requires --agent LABEL"); + return Ok(2); + }; + let content = std::fs::read_to_string(path)?; + crate::detect::manifest::explain_to_json_value(&crate::detect::manifest::explain_for_label( + &agent_label, + &content, + )) + } else { + let Some(target) = target else { + eprintln!("usage: herdr agent explain <target> [--json]"); + eprintln!("usage: herdr agent explain --file PATH --agent LABEL [--json]"); + return Ok(2); + }; + if agent.is_some() { + eprintln!("--agent is only valid with --file"); + return Ok(2); + } + + let response = super::send_request(&Request { + id: "cli:agent:explain".into(), + method: Method::AgentExplain(AgentTarget { + target: target.to_owned(), + }), + })?; + if response.get("error").is_some() { + eprintln!("{}", serde_json::to_string(&response).unwrap()); + return Ok(1); + } + response["result"]["explain"].clone() + }; + + if json { + println!("{explain}"); + } else { + print_agent_explain_text(&explain); + } + Ok(0) +} + +fn print_agent_explain_text(explain: &serde_json::Value) { + println!("agent: {}", explain["agent"].as_str().unwrap_or("unknown")); + println!("state: {}", explain["state"].as_str().unwrap_or("unknown")); + println!( + "screen_detection_skipped: {}", + explain["screen_detection_skipped"] + .as_bool() + .unwrap_or(false) + ); + if let Some(reason) = explain["screen_detection_skip_reason"].as_str() { + println!("screen_detection_skip_reason: {reason}"); + } + println!( + "manifest: {}", + explain["manifest_source"].as_str().unwrap_or("none") + ); + println!( + "manifest_version: {}", + explain["manifest_version"].as_str().unwrap_or("unknown") + ); + println!( + "cached_remote_version: {}", + explain["cached_remote_version"].as_str().unwrap_or("none") + ); + println!( + "local_override_shadowing_remote: {}", + explain["local_override_shadowing_remote"] + .as_bool() + .unwrap_or(false) + ); + if let Some(status) = explain["remote_update_status"].as_str() { + println!("remote_update_status: {status}"); + } + if let Some(error) = explain["remote_update_error"].as_str() { + println!("remote_update_error: {error}"); + } + if let Some(rule) = explain["matched_rule"].as_object() { + println!( + "matched_rule: {} priority={} region={} state={}", + rule.get("id") + .and_then(|value| value.as_str()) + .unwrap_or("-"), + rule.get("priority") + .and_then(|value| value.as_i64()) + .unwrap_or(0), + rule.get("region") + .and_then(|value| value.as_str()) + .unwrap_or("-"), + rule.get("state") + .and_then(|value| value.as_str()) + .unwrap_or("unknown") + ); + } else { + println!("matched_rule: none"); + } + println!( + "visible: idle={} blocker={} working={}", + explain["visible_idle"].as_bool().unwrap_or(false), + explain["visible_blocker"].as_bool().unwrap_or(false), + explain["visible_working"].as_bool().unwrap_or(false) + ); + if let Some(reason) = explain["fallback_reason"].as_str() { + println!("fallback_reason: {reason}"); + } + if let Some(reason) = explain["skipped_update_reason"].as_str() { + println!("skipped_update_reason: {reason}"); + } + if let Some(warning) = explain["warning"].as_str() { + println!("warning: {warning}"); + } + if let Some(evaluated_rules) = explain["evaluated_rules"] + .as_array() + .filter(|rules| !rules.is_empty()) + { + println!("evaluated_rules:"); + for rule in evaluated_rules { + println!( + " {} matched={} priority={} region={} state={}", + rule["id"].as_str().unwrap_or("-"), + rule["matched"].as_bool().unwrap_or(false), + rule["priority"].as_i64().unwrap_or(0), + rule["region"].as_str().unwrap_or("-"), + rule["state"].as_str().unwrap_or("unknown") + ); + let evidence = &rule["evidence"]; + println!( + " matchers: contains={:?} regex={:?} line_regex={:?} all={} any={} not={}", + evidence["contains"], + evidence["regex"], + evidence["line_regex"], + evidence["all_count"].as_u64().unwrap_or(0), + evidence["any_count"].as_u64().unwrap_or(0), + evidence["not_count"].as_u64().unwrap_or(0) + ); + println!( + " region: bytes={} preview={:?}", + evidence["region_bytes"].as_u64().unwrap_or(0), + evidence["region_preview"].as_str().unwrap_or("") + ); + } + } +} + fn agent_start(args: &[String]) -> std::io::Result<i32> { let Some(name) = args.first() else { eprintln!("usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); @@ -422,6 +636,8 @@ fn print_agent_help() { eprintln!(" herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]"); eprintln!(" herdr agent attach <target> [--takeover]"); eprintln!(" herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); + eprintln!(" herdr agent explain <target> [--json]"); + eprintln!(" herdr agent explain --file PATH --agent LABEL [--json]"); eprintln!(" targets accept terminal ids, unique agent names, detected/reported agent labels, and legacy pane ids"); eprintln!( " agent send writes literal text; use pane run when you want command text plus Enter" |
