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().with_protocol_v1(),
104    ) {
105        Ok(Some(version)) => {
106            let _ = write!(handle, "{version}");
107            Some(ExitCode::SUCCESS)
108        }
109        Ok(None) => None,
110        Err(err) => {
111            let err = Error::new(
112                crate::shared::error::ErrorCode::InvalidArgument,
113                err.to_string(),
114            );
115            let _ = crate::shared::afdata::emit_error(&mut handle, &err);
116            Some(ExitCode::from(2))
117        }
118    }
119}
120
121/// Render help and return an exit code, or `None` to continue normal parsing.
122fn maybe_render_help() -> Option<ExitCode> {
123    use clap::CommandFactory;
124    use std::io::Write;
125
126    let raw: Vec<String> = std::env::args().collect();
127    let mut handle = std::io::stdout();
128    match agent_first_data::cli_handle_help_or_continue(
129        &raw,
130        &args::Cli::command(),
131        &agent_first_data::HelpConfig::human_cli_default().with_protocol_v1(),
132    ) {
133        Ok(Some(help)) => {
134            let _ = write!(handle, "{help}");
135            Some(ExitCode::SUCCESS)
136        }
137        Ok(None) => None,
138        Err(err) => {
139            let err = Error::new(
140                crate::shared::error::ErrorCode::InvalidArgument,
141                err.to_string(),
142            );
143            let _ = crate::shared::afdata::emit_error(&mut handle, &err);
144            Some(ExitCode::from(2))
145        }
146    }
147}
148
149async fn dispatch(parsed: args::Parsed) -> Result<(), Error> {
150    let command = match parsed.command {
151        args::Command::Fetch(a) => {
152            // `fetch` owns error emission so it can attach the fetch-local trace
153            // without adding trace fields to the global Error type.
154            return cmd::fetch::run(*a).await;
155        }
156        command => command,
157    };
158    let res = match command {
159        args::Command::Host(a) => cmd::host::run(a).await,
160        args::Command::Fetch(_) => unreachable!("fetch handled above"),
161        args::Command::Upload(a) => cmd::upload::run(a).await,
162        args::Command::Cdp(a) => cmd::cdp::run(a).await,
163        args::Command::Panel(a) => cmd::panel::run(a).await,
164        args::Command::Health(a) => cmd::health::run(a).await,
165        args::Command::Capabilities(a) => cmd::capabilities::run(a).await,
166        args::Command::Profile(a) => cmd::profile::run(a).await,
167        args::Command::Tabs(a) => cmd::tabs::run(a).await,
168        args::Command::Skill(a) => cmd::skill::run(a).await,
169        args::Command::Container(a) => cmd::container::run(a).await,
170    };
171    if let Err(ref e) = res {
172        emit_cli_error(e);
173    }
174    res
175}
176
177fn emit_cli_error(err: &Error) {
178    let stdout = std::io::stdout();
179    let mut handle = stdout.lock();
180    let _ = crate::shared::afdata::emit_error(&mut handle, err);
181}
182
183fn emit_bootstrap_error(msg: &str) {
184    // Fallback path used before the runtime exists. Stays on stdout to
185    // match the AFDATA protocol channel rule (clippy bans stderr usage).
186    let stdout = std::io::stdout();
187    let mut handle = stdout.lock();
188    let err = Error::new(crate::shared::error::ErrorCode::InternalError, msg);
189    let _ = crate::shared::afdata::emit_error(&mut handle, &err);
190}