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    let _stream_redirect =
16        match agent_first_data::stream_redirect::install_from_raw_args(std::env::args()) {
17            Ok(redirect) => redirect,
18            Err(err) => {
19                emit_bootstrap_error(&err.to_string());
20                return ExitCode::from(2);
21            }
22        };
23
24    // Install the process-wide rustls crypto provider before anything builds a
25    // reqwest/TLS client. On the inline-fetch path the host-side CDP fetch runs
26    // before the SDK client's own guard, so without this `afhttp fetch` panics
27    // with "no rustls crypto provider is configured".
28    crate::host::bootstrap::install_rustls_provider();
29
30    // Version/help are rendered without spinning up the async runtime so
31    // machine-readable requests do not fall through to clap's plain text exits.
32    if let Some(code) = maybe_render_version() {
33        return code;
34    }
35    // Help is rendered without spinning up the async runtime so
36    // `--help --recursive --output markdown` can feed generated docs.
37    if let Some(code) = maybe_render_help() {
38        return code;
39    }
40    // The fetch/host pipeline polls a deeply nested future chain (inline host
41    // launch → CDP handshake → …). Polling that depth builds a deep synchronous
42    // call stack that overflows Windows' default 1 MiB main-thread stack
43    // (Linux/macOS default to 8 MiB). Run the runtime on a thread with a generous
44    // stack so behavior is uniform across platforms.
45    match std::thread::Builder::new()
46        .name("afhttp-main".to_string())
47        .stack_size(16 * 1024 * 1024)
48        .spawn(run_blocking)
49    {
50        Ok(handle) => match handle.join() {
51            Ok(code) => code,
52            Err(_) => {
53                emit_bootstrap_error("afhttp worker thread panicked");
54                ExitCode::from(2)
55            }
56        },
57        Err(e) => {
58            emit_bootstrap_error(&format!("spawn worker thread: {e}"));
59            ExitCode::from(2)
60        }
61    }
62}
63
64/// Build the tokio runtime and drive the dispatched command to completion.
65/// Runs on a dedicated large-stack thread spawned by `run`.
66fn run_blocking() -> ExitCode {
67    let rt = match tokio::runtime::Builder::new_multi_thread()
68        .enable_all()
69        .thread_stack_size(16 * 1024 * 1024)
70        .build()
71    {
72        Ok(rt) => rt,
73        Err(e) => {
74            emit_bootstrap_error(&format!("tokio runtime: {e}"));
75            return ExitCode::from(2);
76        }
77    };
78    let exit = rt.block_on(async {
79        match args::parse() {
80            Ok(parsed) => dispatch(parsed).await,
81            Err(err) => {
82                emit_cli_error(&err);
83                Err(err)
84            }
85        }
86    });
87    match exit {
88        Ok(()) => ExitCode::SUCCESS,
89        Err(_) => ExitCode::from(1),
90    }
91}
92
93/// Render version and return an exit code, or `None` to continue normal parsing.
94fn maybe_render_version() -> Option<ExitCode> {
95    use std::io::Write;
96
97    let raw: Vec<String> = std::env::args().collect();
98    let mut handle = std::io::stdout();
99    match agent_first_data::cli_handle_version_or_continue(
100        &raw,
101        "afhttp",
102        env!("CARGO_PKG_VERSION"),
103        &agent_first_data::VersionConfig::conventional_default(),
104    ) {
105        Ok(Some(version)) => {
106            let _ = write!(handle, "{version}");
107            Some(ExitCode::SUCCESS)
108        }
109        Ok(None) => None,
110        Err(err) => {
111            let _ = writeln!(handle, "{}", agent_first_data::output_json(&err));
112            Some(ExitCode::from(2))
113        }
114    }
115}
116
117/// Render help and return an exit code, or `None` to continue normal parsing.
118fn maybe_render_help() -> Option<ExitCode> {
119    use clap::CommandFactory;
120    use std::io::Write;
121
122    let raw: Vec<String> = std::env::args().collect();
123    let mut handle = std::io::stdout();
124    match agent_first_data::cli_handle_help_or_continue(
125        &raw,
126        &args::Cli::command(),
127        &agent_first_data::HelpConfig::human_cli_default(),
128    ) {
129        Ok(Some(help)) => {
130            let _ = write!(handle, "{help}");
131            Some(ExitCode::SUCCESS)
132        }
133        Ok(None) => None,
134        Err(err) => {
135            let _ = writeln!(handle, "{}", agent_first_data::output_json(&err));
136            Some(ExitCode::from(2))
137        }
138    }
139}
140
141async fn dispatch(parsed: args::Parsed) -> Result<(), Error> {
142    let command = match parsed.command {
143        args::Command::Fetch(a) => {
144            // `fetch` owns error emission so it can attach the fetch-local trace
145            // without adding trace fields to the global Error type.
146            return cmd::fetch::run(*a).await;
147        }
148        command => command,
149    };
150    let res = match command {
151        args::Command::Host(a) => cmd::host::run(a).await,
152        args::Command::Fetch(_) => unreachable!("fetch handled above"),
153        args::Command::Upload(a) => cmd::upload::run(a).await,
154        args::Command::Cdp(a) => cmd::cdp::run(a).await,
155        args::Command::Panel(a) => cmd::panel::run(a).await,
156        args::Command::Health(a) => cmd::health::run(a).await,
157        args::Command::Capabilities(a) => cmd::capabilities::run(a).await,
158        args::Command::Profile(a) => cmd::profile::run(a).await,
159        args::Command::Tabs(a) => cmd::tabs::run(a).await,
160        args::Command::Skill(a) => cmd::skill::run(a).await,
161        args::Command::Container(a) => cmd::container::run(a).await,
162    };
163    if let Err(ref e) = res {
164        emit_cli_error(e);
165    }
166    res
167}
168
169fn emit_cli_error(err: &Error) {
170    let stdout = std::io::stdout();
171    let mut handle = stdout.lock();
172    let _ = crate::shared::envelope::emit_error(&mut handle, err);
173}
174
175fn emit_bootstrap_error(msg: &str) {
176    // Fallback path used before the runtime exists. Stays on stdout to
177    // match the AFDATA protocol channel rule (clippy bans stderr usage).
178    use std::io::Write;
179    let stdout = std::io::stdout();
180    let mut handle = stdout.lock();
181    let _ = writeln!(
182        handle,
183        "{{\"code\":\"error\",\"error_code\":\"internal_error\",\"error\":{},\"retryable\":false}}",
184        serde_json::Value::String(msg.to_string())
185    );
186}