aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-27 22:53:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-27 22:54:02 +0300
commit5a61ae2f9d89320c2e91d58fc3646b5a6a88f44a (patch)
tree5a627b659542cc3b881e50de1b57e10f1736a198
parentd7879fafe80933ce962f1e029992e47a4eafc69d (diff)
feat: add status command
-rw-r--r--AGENTS.md1
-rw-r--r--SOCKET_API.md18
-rw-r--r--src/api/mod.rs1
-rw-r--r--src/api/schema.rs2
-rw-r--r--src/cli.rs194
-rw-r--r--src/main.rs77
-rw-r--r--src/server/headless.rs1
-rw-r--r--tests/api_ping.rs1
-rw-r--r--tests/cli_wrapper.rs150
9 files changed, 408 insertions, 37 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 8e09e379..dcb2aeff 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -49,6 +49,7 @@ Unit tests live next to the code (`#[cfg(test)] mod tests`). If you add behavior
## Conventions
- Conventional commits, lowercase, no emojis.
+- Do not edit `CHANGELOG.md` during normal feature or fix work. Changelog entries are drafted and applied immediately before release.
- When a commit fully fixes a GitHub issue, include `fixes #<issue-number>` in the commit body so GitHub closes it on merge.
- Rust: no `unwrap()` in production code. `tracing` for logging. `#[allow]` only with a comment explaining why.
- Don't bypass checks. If tests fail, fix them before committing.
diff --git a/SOCKET_API.md b/SOCKET_API.md
index 3ee1cfcc..b9da2efb 100644
--- a/SOCKET_API.md
+++ b/SOCKET_API.md
@@ -50,7 +50,8 @@ successful responses look like:
"id": "req_1",
"result": {
"type": "pong",
- "version": "0.1.2"
+ "version": "0.1.2",
+ "protocol": 2
}
}
```
@@ -891,10 +892,20 @@ example pushed `pane.agent_status_changed` event:
`agent` in pushed events follows the same rules as `pane_info.agent`: it may be a built-in detected name, a custom hook-reported label, or omitted.
## cli wrappers
-these commands talk to the same local socket surface and are usually the easiest starting point for shell scripts and coding agents.
+these commands provide the shell-facing control surface. most command groups talk to the local socket; `status client` only inspects the local executable.
### command groups
+status commands:
+
+```text
+herdr status
+herdr status server
+herdr status client
+```
+
+`herdr -V` and `herdr --version` print the local executable version without contacting the server. `herdr status` compares that local executable with the running server when one is reachable.
+
workspace commands:
```text
@@ -939,6 +950,9 @@ herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [
### cli behavior notes
+- `status` prints local client version/protocol, running server version/protocol when reachable, socket path, compatibility, and whether a restart is needed
+- `status server` prints only the running server side; if no server is reachable it exits successfully and prints `status: not running`
+- `status client` prints only the local executable version/protocol and binary path without contacting the server
- `workspace create` focuses by default; pass `--no-focus` to keep focus where it is
- `workspace create` without `--label` keeps the default cwd-based workspace naming
- `workspace create --label` applies the custom workspace name immediately
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 33a3cd44..e97d94f6 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -304,6 +304,7 @@ fn handle_request(request: Request, api_tx: &ApiRequestSender) -> String {
id: request.id,
result: ResponseResult::Pong {
version: env!("CARGO_PKG_VERSION").into(),
+ protocol: crate::server::protocol::PROTOCOL_VERSION,
},
})
.unwrap_or_else(|_| {
diff --git a/src/api/schema.rs b/src/api/schema.rs
index f7665fd9..d3fd4682 100644
--- a/src/api/schema.rs
+++ b/src/api/schema.rs
@@ -428,6 +428,7 @@ pub struct ErrorBody {
pub enum ResponseResult {
Pong {
version: String,
+ protocol: u32,
},
WorkspaceInfo {
workspace: WorkspaceInfo,
@@ -940,6 +941,7 @@ mod tests {
id: "req_1".into(),
result: ResponseResult::Pong {
version: "0.1.2".into(),
+ protocol: 2,
},
};
diff --git a/src/cli.rs b/src/cli.rs
index 8421863a..52fba0bd 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -9,9 +9,9 @@ use crate::api::schema::{
AgentStatus, EmptyParams, IntegrationInstallParams, IntegrationTarget,
IntegrationUninstallParams, Method, OutputMatch, PaneListParams, PaneReadParams,
PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget,
- PaneWaitForOutputParams, ReadSource, Request, SplitDirection, Subscription, TabCreateParams,
- TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams,
- WorkspaceTarget,
+ PaneWaitForOutputParams, PingParams, ReadSource, Request, SplitDirection, Subscription,
+ TabCreateParams, TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams,
+ WorkspaceRenameParams, WorkspaceTarget,
};
pub enum CommandOutcome {
@@ -31,6 +31,7 @@ pub fn maybe_run(args: &[String]) -> std::io::Result<CommandOutcome> {
};
exit_code
}
+ "status" => run_status_command(&args[2..])?,
"workspace" => run_workspace_command(&args[2..])?,
"tab" => run_tab_command(&args[2..])?,
"pane" => run_pane_command(&args[2..])?,
@@ -61,6 +62,160 @@ fn run_server_command(args: &[String]) -> std::io::Result<Option<i32>> {
}
}
+fn run_status_command(args: &[String]) -> std::io::Result<i32> {
+ match args.first().map(|arg| arg.as_str()) {
+ None => print_full_status(),
+ Some("server") => {
+ if args.len() > 1 {
+ eprintln!("usage: herdr status server");
+ return Ok(2);
+ }
+ print_server_status()
+ }
+ Some("client") => {
+ if args.len() > 1 {
+ eprintln!("usage: herdr status client");
+ return Ok(2);
+ }
+ print_client_status();
+ Ok(0)
+ }
+ Some("help" | "--help" | "-h") => {
+ print_status_help();
+ Ok(0)
+ }
+ Some(_) => {
+ print_status_help();
+ Ok(2)
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+enum ServerRuntimeStatus {
+ Running {
+ version: Option<String>,
+ protocol: Option<u32>,
+ },
+ NotRunning,
+}
+
+fn print_full_status() -> std::io::Result<i32> {
+ let server = read_server_runtime_status()?;
+
+ println!("client:");
+ println!(" version: {}", env!("CARGO_PKG_VERSION"));
+ println!(" protocol: {}", crate::server::protocol::PROTOCOL_VERSION);
+ println!();
+ println!("server:");
+ print_server_status_body(&server, " ");
+ println!();
+ println!("update:");
+ println!(" restart_needed: {}", restart_needed_label(&server));
+
+ Ok(0)
+}
+
+fn print_server_status() -> std::io::Result<i32> {
+ let server = read_server_runtime_status()?;
+ print_server_status_body(&server, "");
+ Ok(0)
+}
+
+fn print_client_status() {
+ println!("version: {}", env!("CARGO_PKG_VERSION"));
+ println!("protocol: {}", crate::server::protocol::PROTOCOL_VERSION);
+ println!("binary: {}", current_exe_label());
+}
+
+fn print_server_status_body(server: &ServerRuntimeStatus, indent: &str) {
+ match server {
+ ServerRuntimeStatus::Running { version, protocol } => {
+ println!("{indent}status: running");
+ println!("{indent}version: {}", option_label(version.as_deref()));
+ println!("{indent}protocol: {}", protocol_label(*protocol));
+ println!("{indent}compatible: {}", compatibility_label(*protocol));
+ println!("{indent}socket: {}", api::socket_path().display());
+ }
+ ServerRuntimeStatus::NotRunning => {
+ println!("{indent}status: not running");
+ println!("{indent}socket: {}", api::socket_path().display());
+ }
+ }
+}
+
+fn read_server_runtime_status() -> std::io::Result<ServerRuntimeStatus> {
+ match send_request(&Request {
+ id: "cli:status:server".into(),
+ method: Method::Ping(PingParams::default()),
+ }) {
+ Ok(response) => {
+ if response.get("error").is_some() {
+ return Err(std::io::Error::other(format!(
+ "server status request failed: {}",
+ response
+ )));
+ }
+
+ let result = &response["result"];
+ Ok(ServerRuntimeStatus::Running {
+ version: result
+ .get("version")
+ .and_then(|value| value.as_str())
+ .map(str::to_owned),
+ protocol: result
+ .get("protocol")
+ .and_then(|value| value.as_u64())
+ .and_then(|value| u32::try_from(value).ok()),
+ })
+ }
+ Err(err) if server_not_running_error(&err) => Ok(ServerRuntimeStatus::NotRunning),
+ Err(err) => Err(err),
+ }
+}
+
+fn server_not_running_error(err: &std::io::Error) -> bool {
+ matches!(
+ err.kind(),
+ std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
+ )
+}
+
+fn option_label(value: Option<&str>) -> &str {
+ value.unwrap_or("unknown")
+}
+
+fn protocol_label(protocol: Option<u32>) -> String {
+ protocol
+ .map(|value| value.to_string())
+ .unwrap_or_else(|| "unknown".to_string())
+}
+
+fn compatibility_label(protocol: Option<u32>) -> &'static str {
+ match protocol {
+ Some(protocol) if protocol == crate::server::protocol::PROTOCOL_VERSION => "yes",
+ Some(_) => "no",
+ None => "unknown",
+ }
+}
+
+fn restart_needed_label(server: &ServerRuntimeStatus) -> &'static str {
+ match server {
+ ServerRuntimeStatus::Running { version, .. } => match version.as_deref() {
+ Some(version) if version == env!("CARGO_PKG_VERSION") => "no",
+ Some(_) => "yes",
+ None => "unknown",
+ },
+ ServerRuntimeStatus::NotRunning => "no",
+ }
+}
+
+fn current_exe_label() -> String {
+ std::env::current_exe()
+ .map(|path| path.display().to_string())
+ .unwrap_or_else(|err| format!("unknown ({err})"))
+}
+
fn run_workspace_command(args: &[String]) -> std::io::Result<i32> {
let Some(subcommand) = args.first().map(|arg| arg.as_str()) else {
print_workspace_help();
@@ -74,6 +229,10 @@ fn run_workspace_command(args: &[String]) -> std::io::Result<i32> {
"focus" => workspace_focus(&args[1..]),
"rename" => workspace_rename(&args[1..]),
"close" => workspace_close(&args[1..]),
+ "help" | "--help" | "-h" => {
+ print_workspace_help();
+ Ok(0)
+ }
_ => {
print_workspace_help();
Ok(2)
@@ -94,6 +253,10 @@ fn run_tab_command(args: &[String]) -> std::io::Result<i32> {
"focus" => tab_focus(&args[1..]),
"rename" => tab_rename(&args[1..]),
"close" => tab_close(&args[1..]),
+ "help" | "--help" | "-h" => {
+ print_tab_help();
+ Ok(0)
+ }
_ => {
print_tab_help();
Ok(2)
@@ -116,6 +279,10 @@ fn run_pane_command(args: &[String]) -> std::io::Result<i32> {
"send-text" => pane_send_text(&args[1..]),
"send-keys" => pane_send_keys(&args[1..]),
"run" => pane_run(&args[1..]),
+ "help" | "--help" | "-h" => {
+ print_pane_help();
+ Ok(0)
+ }
_ => {
print_pane_help();
Ok(2)
@@ -132,6 +299,10 @@ fn run_wait_command(args: &[String]) -> std::io::Result<i32> {
match subcommand {
"output" => wait_output(&args[1..]),
"agent-status" => wait_agent_status(&args[1..]),
+ "help" | "--help" | "-h" => {
+ print_wait_help();
+ Ok(0)
+ }
_ => {
print_wait_help();
Ok(2)
@@ -148,6 +319,10 @@ fn run_integration_command(args: &[String]) -> std::io::Result<i32> {
match subcommand {
"install" => integration_install(&args[1..]),
"uninstall" => integration_uninstall(&args[1..]),
+ "help" | "--help" | "-h" => {
+ print_integration_help();
+ Ok(0)
+ }
_ => {
print_integration_help();
Ok(2)
@@ -1053,10 +1228,17 @@ fn parse_u64_flag(flag: &str, value: &str) -> std::io::Result<u64> {
fn print_server_help() {
eprintln!("herdr server commands:");
eprintln!(" herdr server run as headless server");
- eprintln!(" herdr server stop stop the running server via the api socket");
+ eprintln!(" herdr server stop stop the running server via the API socket");
eprintln!(" herdr server reload-config reload config.toml in the running server");
}
+fn print_status_help() {
+ eprintln!("herdr status commands:");
+ eprintln!(" herdr status show local client and running server status");
+ eprintln!(" herdr status server show running server status");
+ eprintln!(" herdr status client show local client binary status");
+}
+
fn print_workspace_help() {
eprintln!("herdr workspace commands:");
eprintln!(" herdr workspace list");
@@ -1083,7 +1265,7 @@ fn print_pane_help() {
eprintln!("herdr pane commands:");
eprintln!(" herdr pane list [--workspace <workspace_id>]");
eprintln!(" herdr pane get <pane_id>");
- eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N]");
+ eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--raw]");
eprintln!(" herdr pane split <pane_id> --direction right|down [--cwd PATH] [--no-focus]");
eprintln!(" herdr pane close <pane_id>");
eprintln!(" herdr pane send-text <pane_id> <text>");
@@ -1093,7 +1275,7 @@ fn print_pane_help() {
fn print_wait_help() {
eprintln!("herdr wait commands:");
- eprintln!(" herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex]");
+ eprintln!(" herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex] [--raw]");
eprintln!(
" herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]"
);
diff --git a/src/main.rs b/src/main.rs
index 9b66198f..1d8439b9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -188,35 +188,61 @@ fn main() -> io::Result<()> {
if args.iter().any(|a| a == "--help" || a == "-h") {
println!("herdr — terminal workspace manager for AI coding agents");
println!();
- println!("Usage: herdr [options]");
- println!(" herdr update");
- println!(" herdr server stop");
- println!(" herdr server reload-config");
- println!(" herdr workspace <subcommand> ...");
- println!(" herdr tab <subcommand> ...");
- println!(" herdr pane <subcommand> ...");
- println!(" herdr wait <subcommand> ...");
- println!(" herdr integration <subcommand> ...");
+ println!("Usage: herdr [options] [command]");
println!();
- println!("Commands:");
- println!(" server Run as headless server (no terminal, persists after client disconnect)");
- println!(" server stop Stop the running server via the API socket");
- println!(" server reload-config Reload config.toml in the running server");
- println!(" client Connect to a running server as a thin client");
+ println!("Common commands:");
println!(
- " update Download and install the latest version (run outside herdr)"
+ " {:<32} {}",
+ "herdr", "Launch or attach to the persistent session"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr status [server|client]", "Show local client and running server status"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr update", "Download and install the latest version"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr server stop", "Stop the running server via the API socket"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr server reload-config", "Reload config.toml in the running server"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr workspace <subcommand>", "Workspace helpers over the socket API"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr tab <subcommand>", "Tab helpers over the socket API"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr pane <subcommand>", "Pane control helpers over the socket API"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr wait <subcommand>", "Blocking wait helpers over the socket API"
+ );
+ println!(
+ " {:<32} {}",
+ "herdr integration <subcommand>", "Manage built-in agent integrations"
+ );
+ println!();
+ println!("Advanced commands:");
+ println!(" {:<32} {}", "herdr server", "Run as headless server");
+ println!(
+ " {:<32} {}",
+ "herdr client", "Connect to a running server as a thin client"
);
- println!(" workspace workspace helpers over the socket api");
- println!(" tab tab helpers over the socket api");
- println!(" pane pane control helpers over the socket api");
- println!(" wait blocking wait helpers over the socket api");
- println!(" integration manage built-in agent integrations");
println!();
println!("Options:");
println!(" --no-session Run monolithically (no server/client, escape hatch)");
println!(" --default-config Print default configuration and exit");
println!(" --version, -V Print version and exit");
- println!(" --show-changelog Preview the current version's release notes");
println!(" --help, -h Show this help");
println!();
println!("Config: {}", config::config_path().display());
@@ -242,7 +268,6 @@ fn main() -> io::Result<()> {
"--version",
"-V",
"--default-config",
- "--show-changelog",
"--help",
"-h",
];
@@ -257,6 +282,7 @@ fn main() -> io::Result<()> {
"server",
"client",
"update",
+ "status",
"workspace",
"pane",
"wait",
@@ -304,7 +330,6 @@ fn main() -> io::Result<()> {
Err(err) => return Err(err),
};
- let show_changelog = args.iter().any(|a| a == "--show-changelog");
let in_tmux = std::env::var("TMUX").is_ok();
let original_hook = std::panic::take_hook();
@@ -359,11 +384,7 @@ fn main() -> io::Result<()> {
std::io::stdout().flush()?;
}
- let startup_release_notes = if show_changelog {
- crate::release_notes::load_preview_from_local_changelog(env!("CARGO_PKG_VERSION"))
- } else {
- crate::release_notes::load_pending_for_current_version()
- };
+ let startup_release_notes = crate::release_notes::load_pending_for_current_version();
let mut app = app::App::new(
config,
diff --git a/src/server/headless.rs b/src/server/headless.rs
index b37538df..cffffe6a 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -1948,7 +1948,6 @@ pub fn run_server() -> io::Result<()> {
};
let no_session = false; // Server always does session persistence.
- let _show_changelog = false;
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
diff --git a/tests/api_ping.rs b/tests/api_ping.rs
index b8de04f3..a0e2d33b 100644
--- a/tests/api_ping.rs
+++ b/tests/api_ping.rs
@@ -223,6 +223,7 @@ fn ping_over_socket_returns_version() {
assert_eq!(value["id"], "req_1");
assert_eq!(value["result"]["type"], "pong");
assert_eq!(value["result"]["version"], env!("CARGO_PKG_VERSION"));
+ assert_eq!(value["result"]["protocol"], 2);
cleanup_spawned_herdr(child, base);
}
diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs
index 46f199b1..caad5b39 100644
--- a/tests/cli_wrapper.rs
+++ b/tests/cli_wrapper.rs
@@ -462,6 +462,56 @@ fn pane_run_sends_one_send_input_request_with_enter_key() {
}
#[test]
+fn help_commands_exit_successfully() {
+ let help_cases: &[&[&str]] = &[
+ &["-h"],
+ &["--help"],
+ &["status", "-h"],
+ &["server", "-h"],
+ &["workspace", "-h"],
+ &["tab", "-h"],
+ &["pane", "-h"],
+ &["wait", "-h"],
+ &["integration", "-h"],
+ ];
+
+ for args in help_cases {
+ let output = Command::new(env!("CARGO_BIN_EXE_herdr"))
+ .args(*args)
+ .output()
+ .unwrap();
+ assert!(
+ output.status.success(),
+ "herdr {} failed: status={:?} stdout={} stderr={}",
+ args.join(" "),
+ output.status.code(),
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+}
+
+#[test]
+fn removed_show_changelog_flag_fails_before_nested_guard() {
+ let output = Command::new(env!("CARGO_BIN_EXE_herdr"))
+ .arg("--show-changelog")
+ .env("HERDR_ENV", "1")
+ .output()
+ .unwrap();
+
+ assert_eq!(output.status.code(), Some(1));
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ assert!(
+ stderr.contains("unknown option: --show-changelog"),
+ "stderr: {stderr}"
+ );
+ assert!(
+ !stderr.contains("nested herdr"),
+ "unknown flag should be rejected before nested guard: {stderr}"
+ );
+}
+
+#[test]
fn integration_commands_honor_socket_override_when_server_is_missing() {
let base = unique_test_dir();
let home_dir = base.join("home");
@@ -515,6 +565,106 @@ fn integration_commands_honor_socket_override_when_server_is_missing() {
}
#[test]
+fn status_commands_report_client_and_server_versions() {
+ let base = unique_test_dir();
+ let config_home = base.join("config");
+ let runtime_dir = base.join("runtime");
+ let socket_path = runtime_dir.join("herdr.sock");
+
+ let herdr = spawn_herdr(&config_home, &runtime_dir, &socket_path);
+ wait_for_socket(&socket_path, Duration::from_secs(5));
+
+ let full = run_cli(&socket_path, &["status"]);
+ assert!(
+ full.status.success(),
+ "stderr: {}",
+ String::from_utf8_lossy(&full.stderr)
+ );
+ let full_stdout = String::from_utf8_lossy(&full.stdout);
+ assert!(full_stdout.contains("client:\n"), "stdout: {full_stdout}");
+ assert!(
+ full_stdout.contains(&format!(" version: {}", env!("CARGO_PKG_VERSION"))),
+ "stdout: {full_stdout}"
+ );
+ assert!(
+ full_stdout.contains(" protocol: 2"),
+ "stdout: {full_stdout}"
+ );
+ assert!(full_stdout.contains("server:\n"), "stdout: {full_stdout}");
+ assert!(
+ full_stdout.contains(" status: running"),
+ "stdout: {full_stdout}"
+ );
+ assert!(
+ full_stdout.contains(" compatible: yes"),
+ "stdout: {full_stdout}"
+ );
+ assert!(
+ full_stdout.contains(" restart_needed: no"),
+ "stdout: {full_stdout}"
+ );
+ assert!(
+ full_stdout.contains(&socket_path.display().to_string()),
+ "stdout: {full_stdout}"
+ );
+
+ let server = run_cli(&socket_path, &["status", "server"]);
+ assert!(server.status.success());
+ let server_stdout = String::from_utf8_lossy(&server.stdout);
+ assert!(
+ server_stdout.contains("status: running"),
+ "stdout: {server_stdout}"
+ );
+ assert!(
+ server_stdout.contains(&format!("version: {}", env!("CARGO_PKG_VERSION"))),
+ "stdout: {server_stdout}"
+ );
+ assert!(
+ server_stdout.contains("protocol: 2"),
+ "stdout: {server_stdout}"
+ );
+
+ let client = run_cli(&socket_path, &["status", "client"]);
+ assert!(client.status.success());
+ let client_stdout = String::from_utf8_lossy(&client.stdout);
+ assert!(
+ client_stdout.contains(&format!("version: {}", env!("CARGO_PKG_VERSION"))),
+ "stdout: {client_stdout}"
+ );
+ assert!(
+ client_stdout.contains("protocol: 2"),
+ "stdout: {client_stdout}"
+ );
+ assert!(
+ client_stdout.contains("binary: "),
+ "stdout: {client_stdout}"
+ );
+
+ cleanup_spawned_herdr(herdr, base);
+}
+
+#[test]
+fn status_reports_not_running_when_server_socket_is_missing() {
+ let base = unique_test_dir();
+ let runtime_dir = base.join("runtime");
+ fs::create_dir_all(&runtime_dir).unwrap();
+ register_runtime_dir(&runtime_dir);
+ let socket_path = runtime_dir.join("missing.sock");
+
+ let status = run_cli(&socket_path, &["status"]);
+ assert!(status.status.success());
+ let stdout = String::from_utf8_lossy(&status.stdout);
+ assert!(stdout.contains(" status: not running"), "stdout: {stdout}");
+ assert!(stdout.contains(" restart_needed: no"), "stdout: {stdout}");
+ assert!(
+ stdout.contains(&socket_path.display().to_string()),
+ "stdout: {stdout}"
+ );
+
+ cleanup_test_base(&base);
+}
+
+#[test]
fn server_stop_command_shuts_down_running_server() {
let base = unique_test_dir();
let config_home = base.join("config");