Skip to main content

degenbot_cli_core/
path.rs

1//! The `path` command arms (ADR-051 D6;).
2//!
3//! Ports `cli/path.py`: both verbs are thin clients of a LIVE bot's
4//! `OperatorServer` over the JSON-lines wire protocol in
5//! [`crate::operator`]. The socket is resolved through the same cascade the
6//! rest of the console uses (`--socket` > `DEGENBOT_OPERATOR_SOCKET` > the
7//! `~/.config/degenbot/operator.sock` plugin default); no local pool or
8//! database work happens here.
9//!
10//! - `path add` parses each `FAMILY:ADDRESS[:HASH]` hop
11//!   ([`crate::operator::parse_hop_token`]) and sends `add_path` with an
12//!   optional direction bit applied to every hop.
13//! - `path discover` sends `discover` with an optional bound.
14
15use crate::context::CliContext;
16use crate::error::CliError;
17use crate::operator::{
18    parse_hop_token, resolve_socket, send_request, PathDirection, WireRequest, WireResponse,
19};
20use crate::prompt::PromptPlan;
21use crate::report::PathReport;
22
23/// The `path` command group.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum PathCommand {
26    /// `path add`: enqueue one specific path into the live registration
27    /// pipeline.
28    Add {
29        /// The operator socket path override (`--socket`).
30        socket: Option<String>,
31        /// The raw `FAMILY:ADDRESS[:HASH]` hop tokens, in path order.
32        hops: Vec<String>,
33        /// The direction bit applied to every hop, or `None` to auto-resolve.
34        direction: Option<PathDirection>,
35    },
36    /// `path discover`: one bounded on-demand discovery sweep.
37    Discover {
38        /// The operator socket path override (`--socket`).
39        socket: Option<String>,
40        /// The maximum number of paths to process, or `None` for the default.
41        bound: Option<u64>,
42    },
43}
44
45impl PathCommand {
46    /// Neither path arm prompts.
47    #[must_use]
48    pub const fn prompt_plan(&self, _ctx: &CliContext<'_>) -> PromptPlan {
49        PromptPlan::None
50    }
51}
52
53/// Execute a `path` command.
54///
55/// # Errors
56///
57/// [`CliError::OperatorHygiene`] for a malformed/unknown hop-family token or
58/// an empty hop list (raised before any socket work);
59/// [`CliError::OperatorRefused`] for a `{"ok": false}` host reply;
60/// [`CliError::OperatorProtocol`] for an unreachable socket or a malformed
61/// response.
62pub(crate) fn execute(command: &PathCommand, ctx: &CliContext<'_>) -> Result<PathReport, CliError> {
63    match command {
64        PathCommand::Add {
65            socket,
66            hops,
67            direction,
68        } => {
69            if hops.is_empty() {
70                return Err(CliError::OperatorHygiene(
71                    "--hop is required: an add_path needs at least one hop".to_string(),
72                ));
73            }
74            let steps = hops
75                .iter()
76                .map(String::as_str)
77                .map(parse_hop_token)
78                .collect::<Result<Vec<_>, _>>()?;
79            let directions = direction
80                .as_ref()
81                .map(|direction| vec![direction.is_zfo(); steps.len()]);
82            let request = WireRequest::AddPath { steps, directions };
83            match call(ctx, socket.as_deref(), &request)? {
84                WireResponse::Ok { detail, .. } => Ok(PathReport::Added { detail }),
85                WireResponse::Err { error } => Err(CliError::OperatorRefused(error)),
86            }
87        }
88        PathCommand::Discover { socket, bound } => {
89            let request = WireRequest::Discover { bound: *bound };
90            match call(ctx, socket.as_deref(), &request)? {
91                WireResponse::Ok { detail, .. } => Ok(PathReport::Discovered { detail }),
92                WireResponse::Err { error } => Err(CliError::OperatorRefused(error)),
93            }
94        }
95    }
96}
97
98/// Send one path request and return the host's ok frame (or its refusal).
99fn call(
100    ctx: &CliContext<'_>,
101    socket: Option<&str>,
102    request: &WireRequest,
103) -> Result<WireResponse, CliError> {
104    let socket = resolve_socket(ctx.env(), socket);
105    send_request(&socket, request)
106}