1use serde::{Deserialize, Serialize};
2
3pub const DEFAULT_TCP_PORT: u16 = 7422;
5
6fn default_branch_fallback() -> String {
9 "main".to_string()
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Repository {
14 pub id: String,
15 pub name: String,
16 pub path: String,
18 #[serde(default = "default_branch_fallback")]
21 pub default_branch: String,
22 pub registered_at: u64,
23 pub workstreams: Vec<Workstream>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Workstream {
28 pub id: String,
29 pub name: String,
30 pub repo_id: String,
31 pub branch: String,
32 pub worktree_path: String,
34 pub tmux_session: String,
36 pub status: WorkstreamStatus,
37 pub agents: Vec<Agent>,
38 pub created_at: u64,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub enum WorkstreamStatus {
43 Idle,
44 Running,
45 Stopped,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Agent {
50 pub id: String,
51 pub workstream_id: String,
52 pub tmux_window: u32,
54 pub prompt: String,
55 pub status: AgentStatus,
56 pub exit_code: Option<i32>,
57 pub spawned_at: u64,
58 pub exited_at: Option<u64>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub enum AgentStatus {
63 Running,
64 Exited,
65 Failed,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(tag = "type", content = "data")]
72pub enum Command {
73 Status,
75 Whoami,
76 PairCreate {
77 label: Option<String>,
78 expire_secs: Option<u64>,
80 },
81 PairList,
82 PairRevoke {
83 id: String,
84 },
85 PairRevokeAll,
86
87 RepoRegister {
90 path: String,
91 },
92 RepoList,
93 RepoUnregister {
94 repo_id: String,
95 },
96 RepoSetDefaultBranch {
98 repo_id: String,
99 branch: String,
100 },
101
102 WorkstreamCreate {
104 repo_id: String,
105 name: Option<String>,
107 branch: Option<String>,
109 fetch_latest: bool,
112 },
113 WorkstreamList {
115 repo_id: Option<String>,
116 },
117 WorkstreamDelete {
118 workstream_id: String,
119 },
120
121 AgentSpawn {
123 workstream_id: String,
124 prompt: String,
125 },
126 AgentSpawnInPlace {
129 workstream_id: String,
130 tmux_window: u32,
132 prompt: Option<String>,
134 },
135 AgentKill {
136 agent_id: String,
137 },
138 AgentList {
139 workstream_id: String,
140 },
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(tag = "type", content = "data")]
145pub enum Response {
146 Pong,
148 Ok,
149 DaemonStatus(DaemonStatus),
150 ClientInfo(ClientInfo),
151 Pair(PairPayload),
153 PairedClient(PairedClient),
154 PairedClients(Vec<PairedClient>),
155 Revoked(u32),
157 Error(VexProtoError),
158
159 RepoRegistered(Repository),
161 RepoList(Vec<Repository>),
162 RepoUnregistered,
163 RepoDefaultBranchSet,
164
165 WorkstreamCreated(Workstream),
167 WorkstreamList(Vec<Repository>),
169 WorkstreamDeleted,
170
171 AgentSpawned(Agent),
173 AgentSpawnedInPlace {
175 agent: Agent,
176 exec_cmd: String,
178 },
179 AgentKilled,
180 AgentList(Vec<Agent>),
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct DaemonStatus {
187 pub uptime_secs: u64,
188 pub connected_clients: u32,
189 pub version: String,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct PairPayload {
195 pub token_id: String,
196 pub token_secret: String,
197 pub host: Option<String>,
199}
200
201impl PairPayload {
202 pub fn pairing_string(&self) -> String {
204 format!("{}:{}", self.token_id, self.token_secret)
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct PairedClient {
210 pub token_id: String,
211 pub label: Option<String>,
212 pub created_at: String,
213 pub expires_at: Option<String>,
214 pub last_seen: Option<String>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ClientInfo {
219 pub token_id: Option<String>,
220 pub is_local: bool,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum Transport {
226 Unix,
227 Tcp,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(tag = "code", content = "message")]
232pub enum VexProtoError {
233 Unauthorized,
234 LocalOnly,
235 NotFound,
236 Internal(String),
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct AuthToken {
242 pub token_id: String,
243 pub token_secret: String,
245}
246
247pub mod framing {
250 use serde::{Deserialize, Serialize};
251 use std::io;
252 use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
253
254 #[derive(Debug)]
255 pub enum VexFrameError {
256 Io(io::Error),
257 Json(serde_json::Error),
258 }
259
260 impl std::fmt::Display for VexFrameError {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 match self {
263 VexFrameError::Io(e) => write!(f, "IO error: {e}"),
264 VexFrameError::Json(e) => write!(f, "JSON error: {e}"),
265 }
266 }
267 }
268
269 impl std::error::Error for VexFrameError {}
270
271 impl From<io::Error> for VexFrameError {
272 fn from(e: io::Error) -> Self {
273 VexFrameError::Io(e)
274 }
275 }
276
277 impl From<serde_json::Error> for VexFrameError {
278 fn from(e: serde_json::Error) -> Self {
279 VexFrameError::Json(e)
280 }
281 }
282
283 pub async fn send<W, T>(w: &mut W, msg: &T) -> Result<(), VexFrameError>
285 where
286 W: AsyncWrite + Unpin,
287 T: Serialize,
288 {
289 let body = serde_json::to_vec(msg)?;
290 w.write_u32(body.len() as u32).await?;
291 w.write_all(&body).await?;
292 Ok(())
293 }
294
295 pub async fn recv<R, T>(r: &mut R) -> Result<T, VexFrameError>
297 where
298 R: AsyncRead + Unpin,
299 T: for<'de> Deserialize<'de>,
300 {
301 let len = r.read_u32().await?;
302 let mut buf = vec![0u8; len as usize];
303 r.read_exact(&mut buf).await?;
304 Ok(serde_json::from_slice(&buf)?)
305 }
306}