diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-04 15:46:31 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-04 16:51:10 +0300 |
| commit | 5d9212a55acf4db05b8c5cdf3fee3c56ac404cf7 (patch) | |
| tree | 956276a6b85a52e3a5e0703eb0e5ddf7bc5f9f3c | |
| parent | 15cab964f3494b77159caaea289e17e1596e0720 (diff) | |
feat: expose session snapshot through cli
| -rw-r--r-- | docs/next/website/src/content/docs/socket-api.mdx | 3 | ||||
| -rw-r--r-- | src/cli/api.rs | 16 | ||||
| -rw-r--r-- | src/cli/spec.rs | 3 | ||||
| -rw-r--r-- | src/main.rs | 2 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 51 |
5 files changed, 73 insertions, 2 deletions
diff --git a/docs/next/website/src/content/docs/socket-api.mdx b/docs/next/website/src/content/docs/socket-api.mdx index dcf32e0e..64c5ac4e 100644 --- a/docs/next/website/src/content/docs/socket-api.mdx +++ b/docs/next/website/src/content/docs/socket-api.mdx @@ -121,6 +121,9 @@ Call `session.snapshot` again after reconnecting or when the local cache may be stale. Attached worktree provenance is included on workspace records. Full repo worktree discovery remains `worktree.list`. +From the CLI, `herdr api snapshot` prints the live `session.snapshot` response +as JSON for clients and agents that want a simple bootstrap command. + Pane control methods use public pane ids such as `w1:p1`. Methods whose schema makes `pane_id` optional use the server's active focused pane when it is omitted. `pane.move` always requires the source `pane_id`. diff --git a/src/cli/api.rs b/src/cli/api.rs index e931f4f8..418072e2 100644 --- a/src/cli/api.rs +++ b/src/cli/api.rs @@ -1,5 +1,7 @@ const API_SCHEMA_JSON: &str = include_str!("../../docs/next/api/herdr-api.schema.json"); +use crate::api::schema::{EmptyParams, Method, Request}; + pub(super) fn run_api_command(args: &[String]) -> std::io::Result<i32> { let Some(subcommand) = args.first().map(String::as_str) else { print_api_help(); @@ -8,6 +10,7 @@ pub(super) fn run_api_command(args: &[String]) -> std::io::Result<i32> { match subcommand { "schema" => api_schema(&args[1..]), + "snapshot" => api_snapshot(&args[1..]), "help" | "--help" | "-h" => { print_api_help(); Ok(0) @@ -50,6 +53,18 @@ fn api_schema(args: &[String]) -> std::io::Result<i32> { Ok(0) } +fn api_snapshot(args: &[String]) -> std::io::Result<i32> { + if !args.is_empty() { + eprintln!("usage: herdr api snapshot"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:api:snapshot".into(), + method: Method::SessionSnapshot(EmptyParams::default()), + })?) +} + fn write_schema_file(path: &std::path::Path) -> std::io::Result<()> { std::fs::write(path, API_SCHEMA_JSON) } @@ -83,6 +98,7 @@ fn schema_summary_text() -> std::io::Result<String> { fn print_api_help() { eprintln!("herdr api commands:"); + eprintln!(" herdr api snapshot"); eprintln!(" herdr api schema [--json | --output PATH]"); } diff --git a/src/cli/spec.rs b/src/cli/spec.rs index 71ecd9cc..088bc15e 100644 --- a/src/cli/spec.rs +++ b/src/cli/spec.rs @@ -128,7 +128,8 @@ fn server_command() -> Command { fn api_command() -> Command { Command::new("api") - .about("Inspect socket API metadata") + .about("Inspect socket API metadata and live runtime state") + .subcommand(Command::new("snapshot").about("Print the live session snapshot")) .subcommand( Command::new("schema") .about("Print or write the bundled API schema") diff --git a/src/main.rs b/src/main.rs index 1876c8e5..04ff4894 100644 --- a/src/main.rs +++ b/src/main.rs @@ -546,7 +546,7 @@ fn main() -> io::Result<()> { ), ( "herdr api <subcommand>", - "Inspect the bundled socket API schema", + "Inspect socket API metadata and live runtime state", ), ( "herdr workspace <subcommand>", diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index 41e924ee..c1bf27c0 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -1173,6 +1173,57 @@ fn api_schema_json_prints_bundled_schema() { } #[test] +fn api_snapshot_prints_live_session_snapshot() { + let base = unique_test_dir(); + fs::create_dir_all(&base).unwrap(); + let socket_path = base.join("herdr.sock"); + let listener = UnixListener::bind(&socket_path).unwrap(); + + let server = thread::spawn({ + let socket_path = socket_path.clone(); + move || { + listener.set_nonblocking(true).unwrap(); + let deadline = Instant::now() + Duration::from_millis(700); + while Instant::now() < deadline { + match listener.accept() { + Ok((mut stream, _)) => { + let mut line = String::new(); + let mut reader = BufReader::new(stream.try_clone().unwrap()); + reader.read_line(&mut line).unwrap(); + let request: serde_json::Value = serde_json::from_str(&line).unwrap(); + assert_eq!(request["method"], "session.snapshot"); + assert_eq!(request["id"], "cli:api:snapshot"); + + let response = serde_json::json!({ + "id": "cli:api:snapshot", + "result": { + "type": "ok", + "marker": "snapshot-passthrough" + } + }); + writeln!(stream, "{response}").unwrap(); + stream.flush().unwrap(); + let _ = fs::remove_file(socket_path); + return; + } + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(10)); + } + Err(err) => panic!("accept failed: {err}"), + } + } + panic!("CLI did not connect to fake API socket"); + } + }); + + let value = run_cli_json(&socket_path, &["api", "snapshot"]); + + assert_eq!(value["result"]["marker"], "snapshot-passthrough"); + server.join().unwrap(); + cleanup_test_base(&base); +} + +#[test] fn api_schema_output_writes_bundled_schema_to_file() { let base = unique_test_dir(); fs::create_dir_all(&base).unwrap(); |
