aboutsummaryrefslogtreecommitdiff
path: root/src/cli
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli')
-rw-r--r--src/cli/agent.rs216
-rw-r--r--src/cli/server.rs83
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)]