Skip to main content

agent_first_http/cli/
mod.rs

1//! CLI layer. Parses arguments, calls into the SDK, formats output.
2
3pub mod args;
4pub mod cmd;
5pub mod output;
6
7use std::process::ExitCode;
8
9use crate::shared::error::Error;
10
11/// Binary entry point. Always returns `ExitCode::SUCCESS` on a structured
12/// response (success *or* error); the response itself carries the
13/// success/failure shape on stdout.
14pub fn run() -> ExitCode {
15    // Install the process-wide rustls crypto provider before anything builds a
16    // reqwest/TLS client. On the inline-fetch path the host-side CDP fetch runs
17    // before the SDK client's own guard, so without this `afhttp fetch` panics
18    // with "no rustls crypto provider is configured".
19    crate::host::bootstrap::install_rustls_provider();
20
21    // Help is rendered without spinning up the async runtime so
22    // `--help --recursive --output markdown` can feed generated docs.
23    if let Some(code) = maybe_render_help() {
24        return code;
25    }
26    // The fetch/host pipeline polls a deeply nested future chain (inline host
27    // launch → CDP handshake → …). Polling that depth builds a deep synchronous
28    // call stack that overflows Windows' default 1 MiB main-thread stack
29    // (Linux/macOS default to 8 MiB). Run the runtime on a thread with a generous
30    // stack so behavior is uniform across platforms.
31    match std::thread::Builder::new()
32        .name("afhttp-main".to_string())
33        .stack_size(16 * 1024 * 1024)
34        .spawn(run_blocking)
35    {
36        Ok(handle) => match handle.join() {
37            Ok(code) => code,
38            Err(_) => {
39                emit_bootstrap_error("afhttp worker thread panicked");
40                ExitCode::from(2)
41            }
42        },
43        Err(e) => {
44            emit_bootstrap_error(&format!("spawn worker thread: {e}"));
45            ExitCode::from(2)
46        }
47    }
48}
49
50/// Build the tokio runtime and drive the dispatched command to completion.
51/// Runs on a dedicated large-stack thread spawned by `run`.
52fn run_blocking() -> ExitCode {
53    let rt = match tokio::runtime::Builder::new_multi_thread()
54        .enable_all()
55        .thread_stack_size(16 * 1024 * 1024)
56        .build()
57    {
58        Ok(rt) => rt,
59        Err(e) => {
60            emit_bootstrap_error(&format!("tokio runtime: {e}"));
61            return ExitCode::from(2);
62        }
63    };
64    let exit = rt.block_on(async {
65        match args::parse() {
66            Ok(parsed) => dispatch(parsed).await,
67            Err(err) => {
68                emit_cli_error(&err);
69                Err(err)
70            }
71        }
72    });
73    match exit {
74        Ok(()) => ExitCode::SUCCESS,
75        Err(_) => ExitCode::from(1),
76    }
77}
78
79/// Render help and return an exit code, or `None` to continue normal parsing.
80fn maybe_render_help() -> Option<ExitCode> {
81    use clap::CommandFactory;
82    use std::io::Write;
83
84    let raw: Vec<String> = std::env::args().collect();
85    let mut handle = std::io::stdout();
86    match agent_first_data::cli_handle_help_or_continue(
87        &raw,
88        &args::Cli::command(),
89        &agent_first_data::HelpConfig::human_cli_default(),
90    ) {
91        Ok(Some(help)) => {
92            let _ = write!(handle, "{help}");
93            Some(ExitCode::SUCCESS)
94        }
95        Ok(None) => None,
96        Err(err) => {
97            let _ = writeln!(handle, "{}", agent_first_data::output_json(&err));
98            Some(ExitCode::from(2))
99        }
100    }
101}
102
103async fn dispatch(parsed: args::Parsed) -> Result<(), Error> {
104    let command = match parsed.command {
105        args::Command::Fetch(a) => {
106            // `fetch` owns error emission so it can attach the fetch-local trace
107            // without adding trace fields to the global Error type.
108            return cmd::fetch::run(*a).await;
109        }
110        command => command,
111    };
112    let res = match command {
113        args::Command::Host(a) => cmd::host::run(a).await,
114        args::Command::Fetch(_) => unreachable!("fetch handled above"),
115        args::Command::Upload(a) => cmd::upload::run(a).await,
116        args::Command::Cdp(a) => cmd::cdp::run(a).await,
117        args::Command::Ui(a) => cmd::ui::run(a).await,
118        args::Command::Takeover(a) => cmd::takeover::run(a).await,
119        args::Command::Health(a) => cmd::health::run(a).await,
120        args::Command::Capabilities(a) => cmd::capabilities::run(a).await,
121        args::Command::Profile(a) => cmd::profile::run(a).await,
122        args::Command::Tabs(a) => cmd::tabs::run(a).await,
123        args::Command::Skill(a) => cmd::skill::run(a).await,
124        args::Command::Container(a) => cmd::container::run(a).await,
125    };
126    if let Err(ref e) = res {
127        emit_cli_error(e);
128    }
129    res
130}
131
132fn emit_cli_error(err: &Error) {
133    let stdout = std::io::stdout();
134    let mut handle = stdout.lock();
135    let _ = crate::shared::envelope::emit_error(&mut handle, err);
136}
137
138fn emit_bootstrap_error(msg: &str) {
139    // Fallback path used before the runtime exists. Stays on stdout to
140    // match the AFDATA protocol channel rule (clippy bans stderr usage).
141    use std::io::Write;
142    let stdout = std::io::stdout();
143    let mut handle = stdout.lock();
144    let _ = writeln!(
145        handle,
146        "{{\"code\":\"error\",\"error_code\":\"internal_error\",\"error\":{},\"retryable\":false}}",
147        serde_json::Value::String(msg.to_string())
148    );
149}