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