Skip to main content

zwire_host/
lib.rs

1//! `zwire-host` — one small self-contained binary that exposes the local
2//! machine to any app over a JSON message protocol.
3//!
4//! Originally the Chrome native-messaging host for the [`zwire`] HUD, it is now a
5//! **universal local endpoint**: system stats, a namespaced key/value store, a
6//! filesystem crawler, subprocess exec, clipboard/notify/open, and multiplexed
7//! PTY terminals — reachable from a browser extension *and* from tmux, emacs,
8//! desktop apps, plugins, and any language.
9//!
10//! # Transports
11//! * **Native messaging** (default): `u32`-length-prefixed JSON on stdio, for
12//!   Chrome. Just run the binary with no recognised subcommand.
13//! * **Socket daemon**: `zwire-host serve` listens on a Unix socket speaking
14//!   newline-delimited JSON — the lingua franca every tool already has.
15//! * **Client**: `zwire-host call '{"cmd":"hostinfo"}'` sends one request and
16//!   prints the reply frames.
17//!
18//! Both transports feed the same [`session::Session`] dispatcher, so every
19//! capability is reachable from every client.
20//!
21//! [`zwire`]: https://github.com/MenkeTechnologies/zwire
22pub mod api;
23pub mod bus;
24pub mod exec;
25pub mod fsops;
26pub mod jobs;
27pub mod osops;
28pub mod peer;
29#[cfg(feature = "sysinfo-caps")]
30pub mod procs;
31pub mod proto;
32#[cfg(feature = "pty")]
33pub mod pty;
34pub mod session;
35pub mod store;
36#[cfg(feature = "sysinfo-caps")]
37pub mod sysmon;
38pub mod transport;
39pub mod watch;
40
41// Re-export the handful of types a dependent binary needs to embed the host.
42// This lets sibling hosts (e.g. `zpwrchrome-host`) pull this crate in and reuse
43// the dispatcher + transports directly:
44//
45// ```no_run
46// // in another crate's main.rs, with `zwire-host` as a dependency:
47// fn main() {
48//     // zero-config: full native-messaging + `serve`/`call` behaviour
49//     zwire_host::run(std::env::args().skip(1).collect());
50// }
51// ```
52//
53// Or drive the dispatcher yourself over any transport:
54//
55// ```no_run
56// use zwire_host::{Peer, Session};
57// let out = Peer::ndjson(Box::new(std::io::stdout()));
58// let mut sess = Session::new();
59// sess.handle(&out, &serde_json::json!({"cmd": "hostinfo"}));
60// ```
61pub use proto::{Framing, Out, Peer};
62pub use session::{caps, Session};
63
64use std::io::Read;
65use std::path::PathBuf;
66
67/// Crate version, surfaced in `hello`/`hostinfo` replies.
68pub const VERSION: &str = env!("CARGO_PKG_VERSION");
69
70/// Where the socket daemon listens by default. `$ZWIRE_HOST_SOCK` overrides on
71/// every platform. Otherwise:
72///   * Windows — a per-user named pipe `\\.\pipe\zwire-host-<user>`.
73///   * Unix — `$XDG_RUNTIME_DIR/zwire-host.sock`, else `~/.zwire/host.sock`.
74///
75/// On Windows the returned path's *leaf* is used as the pipe's namespaced name;
76/// the directory portion is ignored.
77pub fn default_socket() -> PathBuf {
78    if let Ok(p) = std::env::var("ZWIRE_HOST_SOCK") {
79        return PathBuf::from(p);
80    }
81    #[cfg(windows)]
82    {
83        let user = std::env::var("USERNAME").unwrap_or_else(|_| "user".into());
84        let safe: String = user
85            .chars()
86            .filter(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
87            .collect();
88        let name = if safe.is_empty() { "user".into() } else { safe };
89        PathBuf::from(format!("zwire-host-{name}"))
90    }
91    #[cfg(not(windows))]
92    {
93        if let Ok(rt) = std::env::var("XDG_RUNTIME_DIR") {
94            if !rt.is_empty() {
95                return PathBuf::from(rt).join("zwire-host.sock");
96            }
97        }
98        store::app_dir("zwire").join("host.sock")
99    }
100}
101
102/// Cyberpunk `--help` wordmark (ANSI-Shadow "ZWIRE-HOST"), cyan→magenta→red.
103const BANNER: &str = concat!(
104    "\x1b[36m███████╗██╗    ██╗██╗██████╗ ███████╗    ██╗  ██╗ ██████╗ ███████╗████████╗\x1b[0m\n",
105    "\x1b[36m╚══███╔╝██║    ██║██║██╔══██╗██╔════╝    ██║  ██║██╔═══██╗██╔════╝╚══██╔══╝\x1b[0m\n",
106    "\x1b[35m  ███╔╝ ██║ █╗ ██║██║██████╔╝█████╗█████╗███████║██║   ██║███████╗   ██║   \x1b[0m\n",
107    "\x1b[35m ███╔╝  ██║███╗██║██║██╔══██╗██╔══╝╚════╝██╔══██║██║   ██║╚════██║   ██║   \x1b[0m\n",
108    "\x1b[31m███████╗╚███╔███╔╝██║██║  ██║███████╗    ██║  ██║╚██████╔╝███████║   ██║   \x1b[0m\n",
109    "\x1b[31m╚══════╝ ╚══╝╚══╝ ╚═╝╚═╝  ╚═╝╚══════╝    ╚═╝  ╚═╝ ╚═════╝ ╚══════╝   ╚═╝   \x1b[0m\n",
110);
111
112/// Static body of the `--help` screen (a plain string literal, so the JSON
113/// braces in the EXAMPLES section stay literal).
114const HELP_BODY: &str = "  \x1b[35m>> UNIVERSAL LOCAL HOST // FULL SPECTRUM <<\x1b[0m\n\n  universal local host — system stats · fs · exec · pty · kv · os\n\n\x1b[33m  USAGE:\x1b[0m zwire-host [MODE] [OPTIONS]\n\n\x1b[36m  ── MODES ─────────────────────────────────────────────────────\x1b[0m\n  zwire-host                     \x1b[32m//\x1b[0m native-messaging on stdio (Chrome default)\n  zwire-host serve               \x1b[32m//\x1b[0m run the NDJSON socket daemon\n  zwire-host call '<json>'       \x1b[32m//\x1b[0m send one request to the daemon, print reply\n  zwire-host call                \x1b[32m//\x1b[0m ...reading the request JSON from stdin\n  zwire-host call --stream '...' \x1b[32m//\x1b[0m keep printing frames (sysinfo/pty streams)\n  zwire-host version | help      \x1b[32m//\x1b[0m print version / this help\n\n\x1b[36m  ── OPTIONS ───────────────────────────────────────────────────\x1b[0m\n  -s, --socket <path>            \x1b[32m//\x1b[0m socket ($ZWIRE_HOST_SOCK / $XDG_RUNTIME_DIR / ~/.zwire)\n  -f, --stream                   \x1b[32m//\x1b[0m relay every reply frame instead of just the first\n      --tcp <addr>               \x1b[32m//\x1b[0m (serve) also listen for peers/remote clients on TCP\n      --token <tok>              \x1b[32m//\x1b[0m (serve) shared secret required of inbound TCP\n      --name <name>              \x1b[32m//\x1b[0m (serve) advertised peer name (default: hostname)\n      --peer <addr>              \x1b[32m//\x1b[0m (serve) dial a peer and keep it linked; repeatable\n  -h, --help                     \x1b[32m//\x1b[0m print this help\n  -V, --version                  \x1b[32m//\x1b[0m print version\n\n\x1b[36m  ── EXAMPLES ──────────────────────────────────────────────────\x1b[0m\n  zwire-host serve &\n  zwire-host call '{\"cmd\":\"hostinfo\"}'\n  zwire-host call '{\"cmd\":\"fs_walk\",\"path\":\"~/src\",\"ext\":\"rs\"}'\n  echo '{\"cmd\":\"exec\",\"program\":\"git\",\"args\":[\"status\"]}' | zwire-host call\n  zwire-host call --stream '{\"cmd\":\"sysinfo_start\"}'\n  zwire-host serve --tcp 0.0.0.0:7420 --token SECRET --peer other.local:7420\n";
115
116/// Build the styled `--help` / `-h` screen in the MenkeTechnologies house
117/// style (see `tp -h`): banner, a status box padded at runtime so its right
118/// border never drifts as VERSION grows, cyan section rules, green `//`.
119fn usage() -> String {
120    const BOX_W: usize = 72;
121    let status = format!(" STATUS: ONLINE  // SIGNAL: ████████░░ // v{VERSION}");
122    let space = " ".repeat(BOX_W.saturating_sub(status.chars().count()));
123    let rule = "─".repeat(BOX_W);
124    format!(
125        "\n{BANNER} \x1b[36m┌{rule}┐\x1b[0m\n \x1b[36m│\x1b[0m{status}{space}\x1b[36m│\x1b[0m\n \x1b[36m└{rule}┘\x1b[0m\n{HELP_BODY}\n\x1b[36m  ── SYSTEM ────────────────────────────────────────────────────\x1b[0m\n  \x1b[35mv{VERSION} \x1b[0m// \x1b[33m(c) Jacob Menke and contributors\x1b[0m\n  \x1b[35mOne pipe. One binary. The whole machine.\x1b[0m\n  \x1b[33m>>> JACK IN. ONE SOCKET. OWN YOUR MACHINE. <<<\x1b[0m\n \x1b[36m░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\x1b[0m\n"
126    )
127}
128
129/// Entry point: interpret `args` (everything after `argv[0]`) and run the chosen
130/// transport. Any unrecognised first token falls through to native-messaging
131/// mode, because Chrome launches the host with extension-origin arguments we
132/// must ignore.
133pub fn run(args: Vec<String>) {
134    // Pull optional flags out of the arg list.
135    let mut socket = default_socket();
136    let mut follow = false;
137    let mut tcp: Option<String> = None;
138    let mut token: Option<String> = None;
139    let mut name: Option<String> = None;
140    let mut peers: Vec<String> = Vec::new();
141    let mut positional: Vec<String> = Vec::new();
142    let mut it = args.into_iter();
143    while let Some(a) = it.next() {
144        match a.as_str() {
145            "--socket" | "-s" => {
146                if let Some(p) = it.next() {
147                    socket = PathBuf::from(p);
148                }
149            }
150            "--stream" | "--follow" | "-f" => follow = true,
151            "--tcp" => tcp = it.next(),
152            "--token" => token = it.next(),
153            "--name" => name = it.next(),
154            "--peer" => {
155                if let Some(p) = it.next() {
156                    peers.push(p);
157                }
158            }
159            _ => positional.push(a),
160        }
161    }
162
163    match positional.first().map(String::as_str) {
164        Some("serve") => transport::serve(transport::ServeConfig {
165            socket,
166            tcp,
167            token,
168            name,
169            peers,
170        }),
171        Some("call") => {
172            let rest = positional[1..].join(" ");
173            let request = if rest.trim().is_empty() {
174                let mut s = String::new();
175                let _ = std::io::stdin().read_to_string(&mut s);
176                s
177            } else {
178                rest
179            };
180            transport::call(&socket, &request, follow);
181        }
182        Some("version") | Some("--version") | Some("-V") => {
183            println!("zwire-host {VERSION}");
184        }
185        Some("help") | Some("--help") | Some("-h") => {
186            print!("{}", usage());
187        }
188        // `stdio`, no args, or Chrome's origin argument → native messaging.
189        _ => transport::stdio(),
190    }
191}