1use anyhow::{Result, anyhow, bail};
2use clap::Parser;
3use futures::{Stream, StreamExt};
4use std::env;
5use std::pin::Pin;
6use std::str::FromStr;
7
8use crate::cli::routing::{
9 Command, is_lightweight_command, run_lightweight_command,
10};
11use crate::storage::get_help_for_tty;
12use crate::utilities::ipc::IPC_ARG;
13
14pub mod base_conversion;
15pub mod routing;
16
17#[derive(Debug)]
22pub enum Invocation {
23 Subprocess(SubprocessArgs),
24 User(Cli),
25}
26
27impl Default for Invocation {
28 fn default() -> Self {
29 Invocation::User(Cli {
30 ctoolbox_ipc_port: None,
31 command: None,
32 })
33 }
34}
35
36impl Invocation {
37 pub fn is_subprocess(&self) -> bool {
38 matches!(self, Invocation::Subprocess(_))
39 }
40
41 pub fn subprocess(&self) -> Option<&SubprocessArgs> {
42 if let Invocation::Subprocess(s) = self {
43 Some(s)
44 } else {
45 None
46 }
47 }
48
49 pub fn expect_cli(&self) -> &Cli {
50 match self {
51 Invocation::User(cli) => cli,
52 Invocation::Subprocess(_) => {
53 panic!("Called expect_cli() on a subprocess invocation")
54 }
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
64pub struct SubprocessArgs {
65 pub ipc: IpcEndpoint,
66 pub subprocess_index: u32,
67 pub service_name: String,
68 pub extra: Vec<String>,
69}
70
71#[derive(Debug, Clone)]
72pub struct IpcEndpoint {
73 pub port: u16,
74 pub identity: String,
75}
76
77impl FromStr for IpcEndpoint {
78 type Err = anyhow::Error;
79
80 fn from_str(s: &str) -> Result<Self> {
81 let mut parts = s.splitn(2, ':');
83 let port_part = parts
84 .next()
85 .ok_or_else(|| anyhow!("Missing port in IPC specification"))?;
86 let identity_part = parts
87 .next()
88 .ok_or_else(|| anyhow!("Missing identity in IPC specification"))?;
89 let port: u16 = port_part
90 .parse()
91 .map_err(|e| anyhow!("Invalid port '{port_part}': {e}"))?;
92 Ok(IpcEndpoint {
93 port,
94 identity: identity_part.to_string(),
95 })
96 }
97}
98
99fn parse_subprocess(raw: &[String]) -> Result<SubprocessArgs> {
102 if raw.len() < 5 {
105 bail!(
106 "Subprocess invocation requires at least 4 arguments after program name: \
107 <magic> <port:identity> <index> <type> [extra..]"
108 );
109 }
110 let ipc = IpcEndpoint::from_str(&raw[2])?;
111 let subprocess_index: u32 = raw[3]
112 .parse()
113 .map_err(|e| anyhow!("Invalid subprocess index '{}': {}", raw[3], e))?;
114 let service_name = raw[4].clone();
115 let extra = if raw.len() > 5 {
116 raw[5..].to_vec()
117 } else {
118 Vec::new()
119 };
120 Ok(SubprocessArgs {
121 ipc,
122 subprocess_index,
123 service_name,
124 extra,
125 })
126}
127
128pub fn parse_invocation() -> Result<Invocation> {
130 let raw: Vec<String> = env::args().collect();
131 if raw.get(1).is_some_and(|s| s == IPC_ARG) {
132 let sub = parse_subprocess(&raw)?;
133 return Ok(Invocation::Subprocess(sub));
134 }
135 let cli = Cli::parse(); Ok(Invocation::User(cli))
138}
139
140#[derive(Parser, Debug)]
145#[command(
146 name = "ctoolbox",
147 version,
148 about = "Collective Toolbox",
149 disable_help_subcommand = true
150)]
151pub struct Cli {
152 #[arg(long)]
153 pub ctoolbox_ipc_port: Option<u16>,
154
155 #[command(subcommand)]
156 pub command: Option<Command>,
157}
158
159pub async fn maybe_run_lightweight(cli: &Cli) -> Result<Option<i32>> {
168 let Some(cmd) = &cli.command else {
169 return Ok(None); };
171
172 let args: Vec<String> = std::env::args().collect();
173 let first = args.get(1);
174 if first.is_some() && !is_lightweight_command(first.unwrap().to_string()) {
175 return Ok(None);
176 }
177
178 let result = run_lightweight_command(cmd).await?;
179 let exit_code = dispatch_tool_result(result).await?;
180 Ok(Some(exit_code))
181}
182
183pub enum ToolResult {
188 Immediate {
190 stdout: Vec<u8>,
191 stderr: Vec<u8>,
192 exit_code: i32,
193 },
194 Streaming {
196 stream: Pin<Box<dyn Stream<Item = OutputChunk> + Send>>,
197 exit_code: i32,
198 },
199}
200
201pub enum OutputChunk {
202 Stdout(Vec<u8>),
203 Stderr(Vec<u8>),
204}
205
206impl ToolResult {
207 pub fn immediate_ok(stdout: Vec<u8>) -> Self {
208 ToolResult::Immediate {
209 stdout,
210 stderr: Vec::new(),
211 exit_code: 0,
212 }
213 }
214 pub fn immediate_err(stderr: Vec<u8>, code: i32) -> Self {
215 ToolResult::Immediate {
216 stdout: Vec::new(),
217 stderr,
218 exit_code: code,
219 }
220 }
221}
222
223async fn dispatch_tool_result(result: ToolResult) -> Result<i32> {
225 use std::io::{Write, stderr, stdout};
226
227 match result {
228 ToolResult::Immediate {
229 stdout: out,
230 stderr: err,
231 exit_code,
232 } => {
233 let mut so = stdout().lock();
234 let mut se = stderr().lock();
235 if !out.is_empty() {
236 so.write_all(&out)?;
237 }
238 if !err.is_empty() {
239 se.write_all(&err)?;
240 }
241 Ok(exit_code)
242 }
243 ToolResult::Streaming {
244 mut stream,
245 exit_code,
246 } => {
247 let mut so = stdout().lock();
248 let mut se = stderr().lock();
249 while let Some(chunk) = stream.next().await {
250 match chunk {
251 OutputChunk::Stdout(d) => so.write_all(&d)?,
252 OutputChunk::Stderr(d) => se.write_all(&d)?,
253 }
254 }
255 Ok(exit_code)
256 }
257 }
258}
259
260#[derive(clap::Args, Debug)]
265pub struct StringInput {
266 pub input: String,
268}
269
270fn generate_help_bytes() -> Vec<u8> {
273 get_help_for_tty(get_width())
279}
280
281pub fn get_width() -> u16 {
283 termsize::get().map_or(80, |s| s.cols)
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[crate::ctb_test]
291 fn test_get_help_bytes() {
292 let help_bytes = generate_help_bytes();
293 assert!(String::from_utf8_lossy(&help_bytes).contains("## Synopsis"));
294 }
295}