feat(xy): CLI client commands

Replace bail!("not implemented") stubs with real RPC calls over the Unix
socket; add format::list_table for fixed-width list output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 12:02:09 +02:00
parent b434c636a6
commit c1f6225e26
2 changed files with 212 additions and 15 deletions
+27
View File
@@ -0,0 +1,27 @@
use xy_protocol::rpc::ServerSummary;
pub fn list_table(rows: &[ServerSummary]) -> String {
let mut out = String::new();
out.push_str("NAME STATE PID PORT UPTIME RESTARTS\n");
for r in rows {
let pid = r.pid.map(|p| p.to_string()).unwrap_or_else(|| "-".into());
let up = r
.uptime_secs
.map(|s| format!("{}s", s))
.unwrap_or_else(|| "-".into());
out.push_str(&format!(
"{:<20}{:<12}{:<8}{:<8}{:<10}{}\n",
r.name,
format!("{:?}", r.state).to_lowercase(),
pid,
r.port,
up,
r.restart_count
));
}
out
}
+185 -15
View File
@@ -1,24 +1,194 @@
use crate::paths::Paths;
use anyhow::{Result, bail};
use anyhow::Result;
use serde_json::json;
use xy_ipc::{Client, ClientError};
use xy_protocol::rpc::{
LogLine, LogStream, LogsParams, LogsSubscribed, ReloadResult, RestartResult, ServerSummary,
StartResult, StatusDetail, StopResult, methods, notifications,
};
pub async fn list(_p: Paths) -> Result<i32> {
bail!("not implemented")
mod format;
async fn connect(paths: &Paths) -> Result<Client> {
match Client::connect(&paths.socket).await {
Ok(c) => Ok(c),
Err(err) => {
eprintln!(
"xy: cannot reach daemon at {}: {err}",
paths.socket.display()
);
std::process::exit(2);
}
}
}
pub async fn status(_p: Paths, _name: String) -> Result<i32> {
bail!("not implemented")
fn rpc_to_exit(err: &ClientError) -> i32 {
match err {
ClientError::Unreachable(_) => 2,
ClientError::Rpc { .. } => 1,
_ => 1,
}
}
pub async fn start(_p: Paths, _all: bool, _name: Option<String>) -> Result<i32> {
bail!("not implemented")
pub async fn list(paths: Paths) -> Result<i32> {
let mut c = connect(&paths).await?;
let rows: Vec<ServerSummary> = match c.call_no_params(methods::LIST).await {
Ok(v) => v,
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
};
print!("{}", format::list_table(&rows));
Ok(0)
}
pub async fn stop(_p: Paths, _all: bool, _name: Option<String>) -> Result<i32> {
bail!("not implemented")
pub async fn status(paths: Paths, name: String) -> Result<i32> {
let mut c = connect(&paths).await?;
let d: StatusDetail = match c.call(methods::STATUS, &json!({"name": name})).await {
Ok(v) => v,
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
};
println!("{:#?}", d);
Ok(0)
}
pub async fn restart(_p: Paths, _all: bool, _name: Option<String>) -> Result<i32> {
bail!("not implemented")
fn name_or_all(all: bool, name: Option<String>) -> serde_json::Value {
if all {
json!({"all": true})
} else {
json!({"name": name.unwrap()})
}
}
pub async fn reload(_p: Paths) -> Result<i32> {
bail!("not implemented")
pub async fn start(paths: Paths, all: bool, name: Option<String>) -> Result<i32> {
let mut c = connect(&paths).await?;
let r: StartResult = match c.call(methods::START, &name_or_all(all, name)).await {
Ok(v) => v,
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
};
if !r.started.is_empty() {
println!("started: {}", r.started.join(", "));
}
if !r.already_running.is_empty() {
println!("already running: {}", r.already_running.join(", "));
}
Ok(0)
}
pub async fn logs(_p: Paths, _name: String, _tail: Option<u32>, _follow: bool) -> Result<i32> {
bail!("not implemented")
pub async fn stop(paths: Paths, all: bool, name: Option<String>) -> Result<i32> {
let mut c = connect(&paths).await?;
let r: StopResult = match c.call(methods::STOP, &name_or_all(all, name)).await {
Ok(v) => v,
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
};
if !r.stopped.is_empty() {
println!("stopped: {}", r.stopped.join(", "));
}
if !r.not_running.is_empty() {
println!("not running: {}", r.not_running.join(", "));
}
Ok(0)
}
pub async fn restart(paths: Paths, all: bool, name: Option<String>) -> Result<i32> {
let mut c = connect(&paths).await?;
let r: RestartResult = match c.call(methods::RESTART, &name_or_all(all, name)).await {
Ok(v) => v,
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
};
println!("restarted: {}", r.restarted.join(", "));
Ok(0)
}
pub async fn reload(paths: Paths) -> Result<i32> {
let mut c = connect(&paths).await?;
let r: ReloadResult = match c.call_no_params(methods::RELOAD).await {
Ok(v) => v,
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
};
println!("added: {}", r.added.join(", "));
println!("removed: {}", r.removed.join(", "));
println!("changed: {}", r.changed.join(", "));
println!("unchanged: {}", r.unchanged.join(", "));
Ok(0)
}
pub async fn logs(paths: Paths, name: String, tail: Option<u32>, follow: bool) -> Result<i32> {
let mut c = connect(&paths).await?;
let p = LogsParams {
name: name.clone(),
tail,
follow,
};
let _sub: LogsSubscribed = match c.call(methods::LOGS, &p).await {
Ok(v) => v,
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
};
loop {
match c.read_notification().await {
Ok(None) => return Ok(0),
Ok(Some(n)) => match n.method.as_str() {
notifications::LOG => {
if let Some(params) = n.params
&& let Ok(line) = serde_json::from_value::<LogLine>(params)
{
let tag = match line.stream {
LogStream::Stdout => "out",
LogStream::Stderr => "err",
};
println!("[{tag}] {}", line.line);
}
}
notifications::LOG_END => return Ok(0),
_ => {}
},
Err(err) => {
eprintln!("xy: {err}");
return Ok(rpc_to_exit(&err));
}
}
}
}