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. `--help-markdown`
22    // feeds scripts/projects/agent-first-http/generate-cli-doc.sh; top-level
23    // `--help` mirrors afpsql's recursive afdata-rendered help. Subcommand help
24    // (e.g. `afhttp fetch --help`) is left to clap.
25    if let Some(code) = maybe_render_help() {
26        return code;
27    }
28    // The fetch/host pipeline polls a deeply nested future chain (inline host
29    // launch → CDP handshake → …). Polling that depth builds a deep synchronous
30    // call stack that overflows Windows' default 1 MiB main-thread stack
31    // (Linux/macOS default to 8 MiB). Run the runtime on a thread with a generous
32    // stack so behavior is uniform across platforms.
33    match std::thread::Builder::new()
34        .name("afhttp-main".to_string())
35        .stack_size(16 * 1024 * 1024)
36        .spawn(run_blocking)
37    {
38        Ok(handle) => match handle.join() {
39            Ok(code) => code,
40            Err(_) => {
41                emit_bootstrap_error("afhttp worker thread panicked");
42                ExitCode::from(2)
43            }
44        },
45        Err(e) => {
46            emit_bootstrap_error(&format!("spawn worker thread: {e}"));
47            ExitCode::from(2)
48        }
49    }
50}
51
52/// Build the tokio runtime and drive the dispatched command to completion.
53/// Runs on a dedicated large-stack thread spawned by `run`.
54fn run_blocking() -> ExitCode {
55    let rt = match tokio::runtime::Builder::new_multi_thread()
56        .enable_all()
57        .thread_stack_size(16 * 1024 * 1024)
58        .build()
59    {
60        Ok(rt) => rt,
61        Err(e) => {
62            emit_bootstrap_error(&format!("tokio runtime: {e}"));
63            return ExitCode::from(2);
64        }
65    };
66    let exit = rt.block_on(async {
67        match args::parse() {
68            Ok(parsed) => dispatch(parsed).await,
69            Err(err) => {
70                emit_cli_error(&err);
71                Err(err)
72            }
73        }
74    });
75    match exit {
76        Ok(()) => ExitCode::SUCCESS,
77        Err(_) => ExitCode::from(1),
78    }
79}
80
81/// Render top-level help and return an exit code, or `None` to continue normal
82/// parsing. afhttp has no top-level global flags, so detection is a simple scan.
83fn maybe_render_help() -> Option<ExitCode> {
84    use clap::CommandFactory;
85    use std::io::Write;
86
87    let raw: Vec<String> = std::env::args().collect();
88    let mut handle = std::io::stdout();
89
90    // `afhttp --help` / `-h` only (let clap handle `afhttp <sub> --help`).
91    if raw.len() == 2 && matches!(raw[1].as_str(), "--help" | "-h") {
92        let _ = writeln!(
93            handle,
94            "{}",
95            agent_first_data::cli_render_help(&args::Cli::command(), &[])
96        );
97        return Some(ExitCode::SUCCESS);
98    }
99
100    // `afhttp --help-markdown` anywhere before a `--` terminator.
101    let wants_markdown = raw
102        .iter()
103        .skip(1)
104        .take_while(|a| a.as_str() != "--")
105        .any(|a| a == "--help-markdown");
106    if wants_markdown {
107        let _ = writeln!(
108            handle,
109            "{}",
110            agent_first_data::cli_render_help_markdown(&args::Cli::command(), &[])
111        );
112        return Some(ExitCode::SUCCESS);
113    }
114
115    None
116}
117
118async fn dispatch(parsed: args::Parsed) -> Result<(), Error> {
119    let res = match parsed.command {
120        args::Command::Host(a) => cmd::host::run(a).await,
121        args::Command::Fetch(a) => cmd::fetch::run(*a).await,
122        args::Command::Upload(a) => cmd::upload::run(a).await,
123        args::Command::Cdp(a) => cmd::cdp::run(a).await,
124        args::Command::Ui(a) => cmd::ui::run(a).await,
125        args::Command::Health(a) => cmd::health::run(a).await,
126        args::Command::Capabilities(a) => cmd::capabilities::run(a).await,
127        args::Command::Profile(a) => cmd::profile::run(a).await,
128        args::Command::Tabs(a) => cmd::tabs::run(a).await,
129        args::Command::Skill(a) => cmd::skill::run(a).await,
130        args::Command::Container(a) => cmd::container::run(a).await,
131    };
132    if let Err(ref e) = res {
133        emit_cli_error(e);
134    }
135    res
136}
137
138fn emit_cli_error(err: &Error) {
139    let stdout = std::io::stdout();
140    let mut handle = stdout.lock();
141    let _ = crate::shared::envelope::emit_error(&mut handle, err);
142}
143
144fn emit_bootstrap_error(msg: &str) {
145    // Fallback path used before the runtime exists. Stays on stdout to
146    // match the AFDATA protocol channel rule (clippy bans stderr usage).
147    use std::io::Write;
148    let stdout = std::io::stdout();
149    let mut handle = stdout.lock();
150    let _ = writeln!(
151        handle,
152        "{{\"code\":\"error\",\"error_code\":\"internal_error\",\"error\":{},\"retryable\":false}}",
153        serde_json::Value::String(msg.to_string())
154    );
155}