wsb 0.253.6349

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The control API: what a command line asks the daemon, over the HTTP server
//! it already runs.
//!
//! Mounted on the workbench's own listener rather than a second one. The
//! daemon serves HTTP already; a separate port would be a second thing to
//! bind, to record, to firewall and to explain.
//!
//! # Not for browsers
//!
//! These routes open worktrees and stop the daemon, so they refuse any
//! request a browser sent — anything carrying `Origin` or `Sec-Fetch-Site`.
//! This API belongs to the command line.
//!
//! # What authorises a request
//!
//! The machine's control token, which the daemon publishes in its 0600
//! rendezvous file: having read it is what says the caller is the account the
//! daemon runs as. `GET /api/daemon/info` and resolving a worktree need
//! nothing — a command line has to be able to ask whether a daemon is there
//! and which version it is before anything else, and none of that is a secret.

use serde::{Deserialize, Serialize};

/// Where the control API is mounted on the daemon's HTTP server.
pub const PREFIX: &str = "/api/daemon";

/// Routes, spelled once. Both ends use these constants.
pub mod route {
    /// `GET` — what this daemon is and what it holds.
    pub const INFO: &str = "/api/daemon/info";
    /// `POST` — which worktree a directory belongs to, opening it if this
    /// daemon has not. The first thing a command line asks.
    pub const RESOLVE: &str = "/api/daemon/resolve";
    /// `POST` — stop the daemon.
    pub const SHUTDOWN: &str = "/api/daemon/shutdown";
    /// The header carrying this machine's control secret.
    pub const CONTROL_HEADER: &str = "x-wsb-machine-control";
}

/// `POST /api/daemon/resolve` — a directory, in; the worktree it belongs to,
/// out.
///
/// A command line sends where it is standing; the daemon walks to the super
/// root the same way every other part of wsb does and answers with that
/// worktree. Two commands run in two submodules of one worktree get one
/// answer; a command run in another worktree of the same project gets that
/// other worktree — from the same daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolveRequest {
    pub cwd: String,
}

/// One worktree, as the daemon holds it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorktreeInfo {
    /// `<project>:<worktree>`, or `~` and a hash of the path outside `$PRJ`.
    pub id: String,
    /// Its `$PRJ` project; none outside `$PRJ`.
    pub project: Option<String>,
    /// `main`, the name prj gave it, or its folder's name outside `$PRJ`.
    pub name: String,
    /// Its super root on this machine.
    pub root: String,
    /// What a paired machine knows it by, `<family>@<name>`; none when it has
    /// nothing a peer could recognise it by.
    pub workspace: Option<String>,
    /// Why it has no continuity, when it has none.
    pub no_continuity: Option<String>,
}

/// `GET /api/daemon/info`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Info {
    /// The daemon's own version. A command line that finds a daemon built from
    /// a different wsb refuses it rather than speaking to it: the two share
    /// this API and the workspace database, and "probably compatible" is not a
    /// state worth having.
    pub version: String,
    pub pid: u32,
    /// The address the workbench — and this API — are served on. Loopback
    /// unless the daemon was started with `--listen`.
    ///
    /// Typed, not a string: "is this reachable from another machine" is a
    /// question about an address, and asking it by comparing text is how
    /// `::1` and `0:0:0:0:0:0:0:1` come to mean different things.
    pub http_address: std::net::IpAddr,
    /// Where the workbench — and this API — are served.
    pub http_port: u16,
    /// This machine's peer identity and name, so a command can report them
    /// without reading the identity file itself.
    pub machine_id: String,
    pub machine_name: String,
    /// The address the peer listener is bound to.
    ///
    /// Every interface by default, unlike the workbench: a peer connection is
    /// authenticated against a pinned identity and encrypted, and a listener
    /// only loopback can reach is one no peer can. Reported because "can the
    /// other machine reach me" is the first question pairing raises.
    pub peer_address: std::net::IpAddr,
    /// The port peers dial: zero when nothing listens — off, or refused.
    pub peer_port: u16,
    /// Why peers cannot reach this machine although listening was asked for:
    /// the port taken, the address not bindable.
    pub peer_refused: Option<String>,
    /// Every worktree this daemon has open. A machine daemon serves many, and
    /// which one a command means is a parameter rather than a property of the
    /// process.
    pub worktrees: Vec<WorktreeInfo>,
}

impl Info {
    /// One worktree by id, or `None` when this daemon does not have it open.
    pub fn worktree(&self, id: &str) -> Option<&WorktreeInfo> {
        self.worktrees.iter().find(|worktree| worktree.id == id)
    }
}

/// The body of every refusal, so a caller never has to read a status code and
/// guess what went wrong.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiError {
    pub error: String,
}