feat(#2017): add hivectl agents list verb showing agent status + technical state
This commit is contained in:
parent
4407967a2f
commit
70d1cdc859
5 changed files with 186 additions and 0 deletions
|
|
@ -504,6 +504,16 @@ const DEFAULT_HOST_SOCKET: &str = "/run/hyperhive/host.sock";
|
|||
|
||||
#[derive(Subcommand)]
|
||||
enum AgentsCmd {
|
||||
/// Show all managed agents with their status (running / needs-login /
|
||||
/// needs-update) and technical state (deployed sha, parent, pending
|
||||
/// reminders). The host roster overview; reuses the dashboard's
|
||||
/// per-agent aggregation. Requires the daemon running.
|
||||
List {
|
||||
/// Emit the raw JSON rows instead of the padded table (for
|
||||
/// scripting). The table is the default human-readable shape.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Stop and start a single agent container without rebuilding config.
|
||||
/// Useful for "kick the container" when the process is stuck or the
|
||||
/// container needs a clean restart without changing the NixOS config.
|
||||
|
|
@ -577,6 +587,7 @@ async fn main() -> Result<()> {
|
|||
GatewayCmd::ListUsers { file } => gateway_list_users(&file),
|
||||
},
|
||||
Cmd::Agents { cmd } => match cmd {
|
||||
AgentsCmd::List { json } => agents_list(&socket, json).await,
|
||||
AgentsCmd::Restart { name } => agents_restart(&socket, &name).await,
|
||||
AgentsCmd::RestartAll => agents_restart_all(&socket).await,
|
||||
},
|
||||
|
|
@ -1374,6 +1385,82 @@ async fn agents_restart(socket: &Path, name: &str) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/// `hivectl agents list` — fetch the per-agent status roster from the
|
||||
/// daemon (`HostRequest::AgentStatus`) and render it as a padded table,
|
||||
/// or the raw JSON rows with `--json`. Reuses the dashboard's
|
||||
/// `ContainerView` aggregation, so the CLI and the web UI never drift.
|
||||
async fn agents_list(socket: &Path, json: bool) -> Result<()> {
|
||||
let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::AgentStatus)
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
if !resp.ok {
|
||||
bail!(
|
||||
"agents list: {}",
|
||||
resp.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
let rows = resp.agent_statuses.unwrap_or_default();
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&rows)?);
|
||||
return Ok(());
|
||||
}
|
||||
if rows.is_empty() {
|
||||
println!("no managed agents found");
|
||||
return Ok(());
|
||||
}
|
||||
// STATUS collapses the health flags into one space-separated token so
|
||||
// the common case (`running`) stays short and anomalies stand out.
|
||||
let status_of = |r: &hive_sh4re::AgentStatusRow| -> String {
|
||||
let mut s = if r.running { "running" } else { "stopped" }.to_owned();
|
||||
if r.needs_login {
|
||||
s.push_str(" needs-login");
|
||||
}
|
||||
if r.needs_update {
|
||||
s.push_str(" needs-update");
|
||||
}
|
||||
s
|
||||
};
|
||||
let headers = ["NAME", "STATUS", "REV", "PARENT", "REMIND"];
|
||||
let table: Vec<[String; 5]> = rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
[
|
||||
r.name.clone(),
|
||||
status_of(r),
|
||||
r.deployed_sha.clone().unwrap_or_else(|| "-".to_owned()),
|
||||
r.parent.clone().unwrap_or_else(|| "-".to_owned()),
|
||||
if r.pending_reminders > 0 {
|
||||
r.pending_reminders.to_string()
|
||||
} else {
|
||||
"-".to_owned()
|
||||
},
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let mut widths = headers.map(str::len);
|
||||
for row in &table {
|
||||
for (i, cell) in row.iter().enumerate() {
|
||||
widths[i] = widths[i].max(cell.len());
|
||||
}
|
||||
}
|
||||
let fmt_row = |cells: &[String]| -> String {
|
||||
cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| format!("{c:<w$}", w = widths[i]))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
};
|
||||
let header_cells: Vec<String> = headers.iter().map(|h| (*h).to_owned()).collect();
|
||||
println!("{}", fmt_row(&header_cells));
|
||||
for row in &table {
|
||||
println!("{}", fmt_row(row));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn agents_restart_all(socket: &Path) -> Result<()> {
|
||||
let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::RestartAll)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -139,6 +139,22 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
|||
}
|
||||
HostRequest::Rebuild { name } => handle_rebuild(&coord, name).await?,
|
||||
HostRequest::List => HostResponse::list(lifecycle::list().await?),
|
||||
HostRequest::AgentStatus => {
|
||||
let rows = crate::container_view::build_all(&coord)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|v| hive_sh4re::AgentStatusRow {
|
||||
name: v.name,
|
||||
running: v.running,
|
||||
needs_update: v.needs_update,
|
||||
needs_login: v.needs_login,
|
||||
deployed_sha: v.deployed_sha,
|
||||
pending_reminders: v.pending_reminders,
|
||||
parent: v.parent,
|
||||
})
|
||||
.collect();
|
||||
HostResponse::agent_statuses(rows)
|
||||
}
|
||||
// The hive domain is injected into c0re's service env by
|
||||
// hive-c0re.nix (`HYPERHIVE_HIVE_DOMAIN`); surface it so the
|
||||
// operator CLI can fill in this hive's own identity.
|
||||
|
|
@ -246,6 +262,7 @@ async fn handle_restart_all() -> Result<HostResponse> {
|
|||
agents: Some(ok_agents),
|
||||
approvals: None,
|
||||
domain: None,
|
||||
agent_statuses: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -414,6 +431,7 @@ fn finish_lifecycle(ok_items: Vec<String>, errors: &[String]) -> HostResponse {
|
|||
agents: Some(ok_items),
|
||||
approvals: None,
|
||||
domain: None,
|
||||
agent_statuses: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue