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// ── Domain types ──────────────────────────────────────────────────────────────
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Repository {
10    pub id: String,
11    pub name: String,
12    /// Absolute path to the git repository on disk
13    pub path: String,
14    pub registered_at: u64,
15    pub workstreams: Vec<Workstream>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Workstream {
20    pub id: String,
21    pub name: String,
22    pub repo_id: String,
23    pub branch: String,
24    /// Absolute path: `$VEX_HOME/worktrees/<workstream_id>`
25    pub worktree_path: String,
26    /// Always `"vex-<workstream_id>"`
27    pub tmux_session: String,
28    pub status: WorkstreamStatus,
29    pub agents: Vec<Agent>,
30    pub created_at: u64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub enum WorkstreamStatus {
35    Idle,
36    Running,
37    Stopped,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Agent {
42    pub id: String,
43    pub workstream_id: String,
44    /// Window index in the tmux session
45    pub tmux_window: u32,
46    pub prompt: String,
47    pub status: AgentStatus,
48    pub exit_code: Option<i32>,
49    pub spawned_at: u64,
50    pub exited_at: Option<u64>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54pub enum AgentStatus {
55    Running,
56    Exited,
57    Failed,
58}
59
60// ── Wire types ────────────────────────────────────────────────────────────────
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type", content = "data")]
64pub enum Command {
65    // ── Existing ──────────────────────────────────────────────────────────────
66    Status,
67    Whoami,
68    PairCreate {
69        label: Option<String>,
70        /// Expiry in seconds from now
71        expire_secs: Option<u64>,
72    },
73    PairList,
74    PairRevoke {
75        id: String,
76    },
77    PairRevokeAll,
78
79    // ── Repos (LocalOnly) ─────────────────────────────────────────────────────
80    /// Register a git repository. Unix-socket only (LocalOnly on TCP).
81    RepoRegister {
82        path: String,
83    },
84    RepoList,
85    RepoUnregister {
86        repo_id: String,
87    },
88
89    // ── Workstreams ───────────────────────────────────────────────────────────
90    WorkstreamCreate {
91        repo_id: String,
92        name: String,
93        branch: String,
94    },
95    /// `repo_id = None` means all repos
96    WorkstreamList {
97        repo_id: Option<String>,
98    },
99    WorkstreamDelete {
100        workstream_id: String,
101    },
102
103    // ── Agents ────────────────────────────────────────────────────────────────
104    AgentSpawn {
105        workstream_id: String,
106        prompt: String,
107    },
108    /// Claim the caller's current tmux window as an agent (in-place conversion).
109    /// The daemon registers the agent and returns the command to exec.
110    AgentSpawnInPlace {
111        workstream_id: String,
112        /// The existing tmux window index (the caller's current pane)
113        tmux_window: u32,
114        /// Optional task description; `None` means run the agent interactively
115        prompt: Option<String>,
116    },
117    AgentKill {
118        agent_id: String,
119    },
120    AgentList {
121        workstream_id: String,
122    },
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(tag = "type", content = "data")]
127pub enum Response {
128    // ── Existing ──────────────────────────────────────────────────────────────
129    Pong,
130    Ok,
131    DaemonStatus(DaemonStatus),
132    ClientInfo(ClientInfo),
133    /// Returned after PairCreate; contains the plaintext secret (one-time)
134    Pair(PairPayload),
135    PairedClient(PairedClient),
136    PairedClients(Vec<PairedClient>),
137    /// Returned by PairRevoke / PairRevokeAll, carrying the revoked count.
138    Revoked(u32),
139    Error(VexProtoError),
140
141    // ── Repos ─────────────────────────────────────────────────────────────────
142    RepoRegistered(Repository),
143    RepoList(Vec<Repository>),
144    RepoUnregistered,
145
146    // ── Workstreams ───────────────────────────────────────────────────────────
147    WorkstreamCreated(Workstream),
148    /// Full tree: repos → workstreams → agents
149    WorkstreamList(Vec<Repository>),
150    WorkstreamDeleted,
151
152    // ── Agents ────────────────────────────────────────────────────────────────
153    AgentSpawned(Agent),
154    /// Returned by `AgentSpawnInPlace`; client should `exec` the given command.
155    AgentSpawnedInPlace {
156        agent: Agent,
157        /// Shell command string to exec (replaces the caller's current process)
158        exec_cmd: String,
159    },
160    AgentKilled,
161    AgentList(Vec<Agent>),
162}
163
164// ── Existing helper types ─────────────────────────────────────────────────────
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct DaemonStatus {
168    pub uptime_secs: u64,
169    pub connected_clients: u32,
170    pub version: String,
171}
172
173/// Returned by PairCreate — contains the plaintext secret for the new token.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct PairPayload {
176    pub token_id: String,
177    pub token_secret: String,
178    /// Optional TCP host for encoding into a QR pairing string
179    pub host: Option<String>,
180}
181
182impl PairPayload {
183    /// Returns the pairing string in `<token_id>:<token_secret>` format.
184    pub fn pairing_string(&self) -> String {
185        format!("{}:{}", self.token_id, self.token_secret)
186    }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PairedClient {
191    pub token_id: String,
192    pub label: Option<String>,
193    pub created_at: String,
194    pub expires_at: Option<String>,
195    pub last_seen: Option<String>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ClientInfo {
200    pub token_id: Option<String>,
201    pub is_local: bool,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case")]
206pub enum Transport {
207    Unix,
208    Tcp,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(tag = "code", content = "message")]
213pub enum VexProtoError {
214    Unauthorized,
215    LocalOnly,
216    NotFound,
217    Internal(String),
218}
219
220/// Sent by the client at the start of every TCP connection before any Command.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct AuthToken {
223    pub token_id: String,
224    /// Plaintext hex-encoded 32-byte secret
225    pub token_secret: String,
226}
227
228// ── Framing ───────────────────────────────────────────────────────────────────
229
230pub mod framing {
231    use serde::{Deserialize, Serialize};
232    use std::io;
233    use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
234
235    #[derive(Debug)]
236    pub enum VexFrameError {
237        Io(io::Error),
238        Json(serde_json::Error),
239    }
240
241    impl std::fmt::Display for VexFrameError {
242        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243            match self {
244                VexFrameError::Io(e) => write!(f, "IO error: {e}"),
245                VexFrameError::Json(e) => write!(f, "JSON error: {e}"),
246            }
247        }
248    }
249
250    impl std::error::Error for VexFrameError {}
251
252    impl From<io::Error> for VexFrameError {
253        fn from(e: io::Error) -> Self {
254            VexFrameError::Io(e)
255        }
256    }
257
258    impl From<serde_json::Error> for VexFrameError {
259        fn from(e: serde_json::Error) -> Self {
260            VexFrameError::Json(e)
261        }
262    }
263
264    /// Write a length-prefixed JSON frame to `w`.
265    pub async fn send<W, T>(w: &mut W, msg: &T) -> Result<(), VexFrameError>
266    where
267        W: AsyncWrite + Unpin,
268        T: Serialize,
269    {
270        let body = serde_json::to_vec(msg)?;
271        w.write_u32(body.len() as u32).await?;
272        w.write_all(&body).await?;
273        Ok(())
274    }
275
276    /// Read a length-prefixed JSON frame from `r`.
277    pub async fn recv<R, T>(r: &mut R) -> Result<T, VexFrameError>
278    where
279        R: AsyncRead + Unpin,
280        T: for<'de> Deserialize<'de>,
281    {
282        let len = r.read_u32().await?;
283        let mut buf = vec![0u8; len as usize];
284        r.read_exact(&mut buf).await?;
285        Ok(serde_json::from_slice(&buf)?)
286    }
287}