ctoolbox/
cli.rs

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// -----------------------------------------------------------------------------
18// Invocation Enum – top-level discrimination between subprocess & user CLI
19// -----------------------------------------------------------------------------
20
21#[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// -----------------------------------------------------------------------------
60// Subprocess Argument Structures
61// -----------------------------------------------------------------------------
62
63#[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        // Expect "<port>:<identity>"
82        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
99// Manual parsing of subprocess arguments (since the magic token won't play
100// nicely as a Clap subcommand).
101fn parse_subprocess(raw: &[String]) -> Result<SubprocessArgs> {
102    // raw[0] is program name
103    // raw[1] == SUBPROCESS_MAGIC
104    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
128// Public parsing entry point used by lib::entry().
129pub 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    // Fallback: user CLI
136    let cli = Cli::parse(); // Clap handles errors & help display
137    Ok(Invocation::User(cli))
138}
139
140// -----------------------------------------------------------------------------
141// Regular (human CLI use or desktop app main process) CLI definition
142// -----------------------------------------------------------------------------
143
144#[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
159// ---------------------------
160// Lightweight Execution Gate
161// ---------------------------
162
163// Decides if we exit early.
164// Returns Ok(Some(exit_code)) if a lightweight command was executed.
165// Returns Ok(None) if we should proceed to heavy boot.
166// Errors bubble up as Err(...).
167pub async fn maybe_run_lightweight(cli: &Cli) -> Result<Option<i32>> {
168    let Some(cmd) = &cli.command else {
169        return Ok(None); // no command => proceed to full app
170    };
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
183// ---------------------------
184// Tool Result Abstractions
185// ---------------------------
186
187pub enum ToolResult {
188    // Immediate, single-buffer outputs
189    Immediate {
190        stdout: Vec<u8>,
191        stderr: Vec<u8>,
192        exit_code: i32,
193    },
194    // Streaming output (future extensibility)
195    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
223// Central dispatcher for writing a ToolResult to real stdio.
224async 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// ---------------------------
261// Shared Arg Structures
262// ---------------------------
263
264#[derive(clap::Args, Debug)]
265pub struct StringInput {
266    /// Input number or string
267    pub input: String,
268}
269
270// Utilities
271
272fn generate_help_bytes() -> Vec<u8> {
273    // Could introspect Clap auto-generated help if desired:
274    // let mut cmd = Cli::command();
275    // let mut buf = Vec::new();
276    // cmd.write_help(&mut buf).unwrap();
277    // buf
278    get_help_for_tty(get_width())
279}
280
281/// Return the width of the terminal
282pub 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}