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}