diff options
Diffstat (limited to 'src/cli')
| -rw-r--r-- | src/cli/agent.rs | 216 | ||||
| -rw-r--r-- | src/cli/server.rs | 83 |
2 files changed, 299 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" diff --git a/src/cli/server.rs b/src/cli/server.rs index 0102b2d4..f6cf7e54 100644 --- a/src/cli/server.rs +++ b/src/cli/server.rs @@ -10,6 +10,8 @@ pub(super) fn run_server_command(args: &[String]) -> std::io::Result<Option<i32> "live-handoff" => server_live_handoff(&args[1..]).map(Some), "--handoff-import" => Ok(None), "reload-config" => server_reload_config(&args[1..]).map(Some), + "agent-manifests" => server_agent_manifests(&args[1..]).map(Some), + "reload-agent-manifests" => server_reload_agent_manifests(&args[1..]).map(Some), "help" | "--help" | "-h" => { print_server_help(); Ok(Some(0)) @@ -42,6 +44,85 @@ fn server_reload_config(args: &[String]) -> std::io::Result<i32> { })?) } +fn server_agent_manifests(args: &[String]) -> std::io::Result<i32> { + let json = match args { + [] => false, + [flag] if flag == "--json" => true, + _ => { + eprintln!("usage: herdr server agent-manifests [--json]"); + return Ok(2); + } + }; + + let response = super::send_request(&Request { + id: "cli:server:agent-manifests".into(), + method: Method::ServerAgentManifests(EmptyParams::default()), + })?; + if json || response.get("error").is_some() { + return super::print_response(&response); + } + + print_agent_manifest_status(&response); + Ok(0) +} + +fn server_reload_agent_manifests(args: &[String]) -> std::io::Result<i32> { + if !args.is_empty() { + eprintln!("usage: herdr server reload-agent-manifests"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:server:reload-agent-manifests".into(), + method: Method::ServerReloadAgentManifests(EmptyParams::default()), + })?) +} + +fn print_agent_manifest_status(response: &serde_json::Value) { + let result = &response["result"]; + let last_check = result["last_check_unix"] + .as_u64() + .map(|value| value.to_string()) + .unwrap_or_else(|| "never".to_string()); + let last_result = result["last_result"].as_str().unwrap_or("not checked"); + println!("last check: {last_check}"); + println!("result: {last_result}"); + println!(); + + let Some(manifests) = result["manifests"].as_array() else { + return; + }; + for manifest in manifests { + let agent = manifest["agent"].as_str().unwrap_or("-"); + let source = manifest["source_kind"].as_str().unwrap_or("-"); + let active_version = manifest["active_version"].as_str().unwrap_or("-"); + let remote_version = manifest["cached_remote_version"].as_str().unwrap_or("-"); + let remote_result = manifest["remote_update_result"] + .as_str() + .unwrap_or("not checked"); + let local_override_shadowing_remote = manifest["local_override_shadowing_remote"] + .as_bool() + .unwrap_or(false); + let marker = if local_override_shadowing_remote { + "!" + } else if manifest["remote_update_error"].as_str().is_some() { + "x" + } else { + " " + }; + println!( + "{marker} {agent:<9} {source:<14} active {active_version:<14} remote {remote_version:<14} {remote_result}" + ); + if let Some(error) = manifest["remote_update_error"].as_str() { + println!(" {error}"); + } else if local_override_shadowing_remote { + println!(" local override shadows cached remote rules"); + } else if let Some(warning) = manifest["warning"].as_str() { + println!(" {warning}"); + } + } +} + fn server_live_handoff(args: &[String]) -> std::io::Result<i32> { let Some(params) = parse_live_handoff_params(args) else { eprintln!( @@ -105,6 +186,8 @@ fn print_server_help() { eprintln!(" herdr server stop stop the running server via the API socket"); eprintln!(" herdr server live-handoff hand off live panes to a new local server"); eprintln!(" herdr server reload-config reload config.toml in the running server"); + eprintln!(" herdr server agent-manifests [--json] show agent detection manifest status"); + eprintln!(" herdr server reload-agent-manifests reload agent detection manifests in the running server"); } #[cfg(test)] |
