1use anyhow::{anyhow, Result};
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{
9 AcquireLockRequest, ExtendLockRequest, ListLocksResponse, LockResponse, ReleaseLockRequest,
10};
11
12use super::client::ApiClient;
13use super::output::OutputConfig;
14
15const BASE: &str = "/api/v1/locks";
16
17pub fn build_command() -> Command {
18 Command::new("lock")
19 .about("Manage distributed locks for resource coordination")
20 .subcommand_required(true)
21 .subcommand(
22 Command::new("acquire")
23 .about("Acquire a lock on a resource")
24 .arg(
25 Arg::new("resource-type").required(true).help(
26 "Type of resource to lock (trajectory, scope, artifact, note, agent)",
27 ),
28 )
29 .arg(
30 Arg::new("resource-id")
31 .required(true)
32 .help("ID of the resource to lock"),
33 )
34 .arg(
35 Arg::new("agent-id")
36 .help("Agent requesting the lock (defaults to session agent)"),
37 )
38 .arg(
39 Arg::new("timeout-ms")
40 .long("timeout-ms")
41 .default_value("30000")
42 .help("Lock timeout in milliseconds"),
43 )
44 .arg(
45 Arg::new("mode")
46 .long("mode")
47 .default_value("exclusive")
48 .help("Lock mode (exclusive, shared)"),
49 ),
50 )
51 .subcommand(
52 Command::new("list")
53 .about("List active locks")
54 .arg(
55 Arg::new("resource-type")
56 .long("resource-type")
57 .help("Filter by resource type"),
58 )
59 .arg(
60 Arg::new("agent-id")
61 .long("agent-id")
62 .help("Filter by holder agent"),
63 )
64 .arg(
65 Arg::new("limit")
66 .long("limit")
67 .default_value("20")
68 .help("Max results"),
69 )
70 .arg(
71 Arg::new("offset")
72 .long("offset")
73 .default_value("0")
74 .help("Offset"),
75 ),
76 )
77 .subcommand(
78 Command::new("get")
79 .about("Get lock details")
80 .arg(Arg::new("id").required(true).help("Lock ID")),
81 )
82 .subcommand(
83 Command::new("release")
84 .about("Release a held lock")
85 .arg(Arg::new("id").required(true).help("Lock ID"))
86 .arg(
87 Arg::new("agent-id")
88 .help("Agent releasing the lock (defaults to session agent)"),
89 ),
90 )
91 .subcommand(
92 Command::new("extend")
93 .about("Extend a lock's expiration")
94 .arg(Arg::new("id").required(true).help("Lock ID"))
95 .arg(
96 Arg::new("additional-ms")
97 .required(true)
98 .help("Additional time in milliseconds"),
99 ),
100 )
101}
102
103pub async fn dispatch(
104 matches: &ArgMatches,
105 client: &ApiClient,
106 output: &OutputConfig,
107 session: &crate::session::CliSession,
108) -> Result<()> {
109 match matches.subcommand() {
110 Some(("acquire", sub)) => {
111 let resource_type = sub
112 .get_one::<String>("resource-type")
113 .unwrap()
114 .parse()
115 .map_err(|e: String| anyhow!(e))?;
116 let resource_id: uuid::Uuid = sub.get_one::<String>("resource-id").unwrap().parse()?;
117 let holder_agent_id =
118 super::require_arg(sub, "agent-id", session.agent_id.as_deref(), "agent")?
119 .parse()?;
120 let timeout_ms: i64 = sub.get_one::<String>("timeout-ms").unwrap().parse()?;
121 let mode = sub.get_one::<String>("mode").unwrap().clone();
122 let req = AcquireLockRequest {
123 resource_type,
124 resource_id,
125 holder_agent_id,
126 timeout_ms,
127 mode,
128 };
129 let resp: LockResponse = client.post(&format!("{BASE}/acquire"), &req).await?;
130 output.print(&resp);
131 }
132 Some(("list", sub)) => {
133 let limit: i64 = sub.get_one::<String>("limit").unwrap().parse()?;
134 let offset: i64 = sub.get_one::<String>("offset").unwrap().parse()?;
135 let mut path = format!("{BASE}?limit={limit}&offset={offset}");
136 if let Some(resource_type) = sub.get_one::<String>("resource-type") {
137 path.push_str(&format!("&resource_type={resource_type}"));
138 }
139 if let Some(agent_id) = super::resolve_arg(sub, "agent-id", session.agent_id.as_deref())
140 {
141 path.push_str(&format!("&agent_id={agent_id}"));
142 }
143 let resp: ListLocksResponse = client.get(&path).await?;
144 output.print_list(&resp.locks, resp.total);
145 }
146 Some(("get", sub)) => {
147 let id = sub.get_one::<String>("id").unwrap();
148 let resp: LockResponse = client.get(&format!("{BASE}/{id}")).await?;
149 output.print(&resp);
150 }
151 Some(("release", sub)) => {
152 let id = sub.get_one::<String>("id").unwrap();
153 let releasing_agent_id =
154 super::require_arg(sub, "agent-id", session.agent_id.as_deref(), "agent")?
155 .parse()?;
156 let req = ReleaseLockRequest { releasing_agent_id };
157 let resp: serde_json::Value = client
158 .post_raw(&format!("{BASE}/{id}/release"), &req)
159 .await?;
160 output.print_value(&resp);
161 }
162 Some(("extend", sub)) => {
163 let id = sub.get_one::<String>("id").unwrap();
164 let additional_ms: i64 = sub.get_one::<String>("additional-ms").unwrap().parse()?;
165 let req = ExtendLockRequest { additional_ms };
166 let resp: serde_json::Value = client
167 .post_raw(&format!("{BASE}/{id}/extend"), &req)
168 .await?;
169 output.print_value(&resp);
170 }
171 _ => unreachable!("subcommand_required(true) prevents this"),
172 }
173 Ok(())
174}