Skip to main content

vex_cli/proto/
mod.rs

1use serde::{Deserialize, Serialize};
2
3/// Default port vexd listens on for TLS TCP connections.
4pub const DEFAULT_TCP_PORT: u16 = 7422;
5
6/// Default port vexd listens on for HTTPS (HTTP API) connections.
7pub const DEFAULT_HTTP_PORT: u16 = 7423;
8
9// ── Wire types ────────────────────────────────────────────────────────────────
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type", content = "data")]
13pub enum Command {
14    Status,
15    Whoami,
16    PairCreate {
17        label: Option<String>,
18        /// Expiry in seconds from now
19        expire_secs: Option<u64>,
20    },
21    PairList,
22    PairRevoke {
23        id: String,
24    },
25    PairRevokeAll,
26    ProjectRegister {
27        name: String,
28        repo: String,
29        path: String,
30    },
31    ProjectUnregister {
32        name: String,
33    },
34    ProjectList,
35    WorkstreamCreate {
36        project_name: String,
37        name: String,
38    },
39    WorkstreamList {
40        project_name: String,
41    },
42    WorkstreamDelete {
43        project_name: String,
44        name: String,
45    },
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct WorkstreamInfo {
50    pub name: String,
51    pub project_name: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(tag = "type", content = "data")]
56pub enum Response {
57    Pong,
58    Ok,
59    DaemonStatus(DaemonStatus),
60    ClientInfo(ClientInfo),
61    /// Returned after PairCreate; contains the plaintext secret (one-time)
62    Pair(PairPayload),
63    PairedClient(PairedClient),
64    PairedClients(Vec<PairedClient>),
65    /// Returned by PairRevoke / PairRevokeAll, carrying the revoked count.
66    Revoked(u32),
67    Project(ProjectInfo),
68    Projects(Vec<ProjectInfo>),
69    Workstream(WorkstreamInfo),
70    Workstreams(Vec<WorkstreamInfo>),
71    Error(VexProtoError),
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct DaemonStatus {
76    pub uptime_secs: u64,
77    pub connected_clients: u32,
78    pub version: String,
79}
80
81/// Returned by PairCreate — contains the plaintext secret for the new token.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct PairPayload {
84    pub token_id: String,
85    pub token_secret: String,
86    /// Optional TCP host for encoding into a QR pairing string
87    pub host: Option<String>,
88}
89
90impl PairPayload {
91    /// Returns the pairing string in `<token_id>:<token_secret>` format.
92    pub fn pairing_string(&self) -> String {
93        format!("{}:{}", self.token_id, self.token_secret)
94    }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PairedClient {
99    pub token_id: String,
100    pub label: Option<String>,
101    pub created_at: String,
102    pub expires_at: Option<String>,
103    pub last_seen: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ProjectInfo {
108    pub name: String,
109    pub repo: String,
110    pub path: String,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ClientInfo {
115    pub token_id: Option<String>,
116    pub is_local: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum Transport {
122    Unix,
123    Tcp,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(tag = "code", content = "message")]
128pub enum VexProtoError {
129    Unauthorized,
130    LocalOnly,
131    NotFound,
132    Internal(String),
133}
134
135/// Sent by the client at the start of every TCP connection before any Command.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct AuthToken {
138    pub token_id: String,
139    /// Plaintext hex-encoded 32-byte secret
140    pub token_secret: String,
141}
142
143// ── Framing ───────────────────────────────────────────────────────────────────
144
145pub mod framing {
146    use serde::{Deserialize, Serialize};
147    use std::io;
148    use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
149
150    #[derive(Debug)]
151    pub enum VexFrameError {
152        Io(io::Error),
153        Json(serde_json::Error),
154    }
155
156    impl std::fmt::Display for VexFrameError {
157        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158            match self {
159                VexFrameError::Io(e) => write!(f, "IO error: {e}"),
160                VexFrameError::Json(e) => write!(f, "JSON error: {e}"),
161            }
162        }
163    }
164
165    impl std::error::Error for VexFrameError {}
166
167    impl From<io::Error> for VexFrameError {
168        fn from(e: io::Error) -> Self {
169            VexFrameError::Io(e)
170        }
171    }
172
173    impl From<serde_json::Error> for VexFrameError {
174        fn from(e: serde_json::Error) -> Self {
175            VexFrameError::Json(e)
176        }
177    }
178
179    /// Write a length-prefixed JSON frame to `w`.
180    pub async fn send<W, T>(w: &mut W, msg: &T) -> Result<(), VexFrameError>
181    where
182        W: AsyncWrite + Unpin,
183        T: Serialize,
184    {
185        let body = serde_json::to_vec(msg)?;
186        w.write_u32(body.len() as u32).await?;
187        w.write_all(&body).await?;
188        Ok(())
189    }
190
191    /// Read a length-prefixed JSON frame from `r`.
192    pub async fn recv<R, T>(r: &mut R) -> Result<T, VexFrameError>
193    where
194        R: AsyncRead + Unpin,
195        T: for<'de> Deserialize<'de>,
196    {
197        let len = r.read_u32().await?;
198        let mut buf = vec![0u8; len as usize];
199        r.read_exact(&mut buf).await?;
200        Ok(serde_json::from_slice(&buf)?)
201    }
202}