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