Skip to main content

agent_first_http/cli/
args.rs

1//! Top-level CLI argument parsing. Uses `clap` derive for the subcommand
2//! enum; each subcommand owns its own arg struct under `cmd::<name>`.
3
4use clap::{Parser, Subcommand};
5
6use crate::shared::error::{Error, ErrorCode};
7
8pub const DISPLAY_NAME: &str = env!("DISPLAY_NAME");
9
10#[derive(Parser, Debug)]
11#[command(
12    name = env!("DISPLAY_NAME"),
13    bin_name = "afhttp",
14    version,
15    about = env!("CARGO_PKG_DESCRIPTION"),
16    long_about = concat!(env!("DISPLAY_NAME"), " - ", env!("CARGO_PKG_DESCRIPTION")),
17    disable_help_subcommand = true,
18)]
19pub struct Cli {
20    /// Where protocol events go: split (default), stdout, or stderr.
21    ///
22    /// `split` sends successful results to stdout and diagnostics/errors to
23    /// stderr. `stdout` and `stderr` keep every event on one ordered stream.
24    #[arg(
25        long = "output-to",
26        global = true,
27        default_value = "split",
28        value_parser = ["split", "stdout", "stderr"]
29    )]
30    pub output_to: String,
31
32    /// Redirect stdout bytes to this file.
33    #[arg(long = "stdout-file", value_name = "PATH", global = true)]
34    pub stdout_file: Option<String>,
35
36    /// Redirect stderr bytes to this file.
37    #[arg(long = "stderr-file", value_name = "PATH", global = true)]
38    pub stderr_file: Option<String>,
39
40    #[command(subcommand)]
41    pub command: Command,
42}
43
44#[derive(Subcommand, Debug)]
45#[command(disable_help_subcommand = true)]
46pub enum Command {
47    /// Fetch a URL.
48    #[command(
49        long_about = "Use `--takeover` to keep a persistent tab open when a captcha, login, or \
50                      2FA wall needs a human. The result returns the short-lived takeover URL in \
51                      `next_action.takeover_url_secret` together with a command that re-fetches \
52                      the same tab."
53    )]
54    Fetch(Box<crate::cli::cmd::fetch::Args>),
55    /// Run the browser host.
56    Host(crate::cli::cmd::host::Args),
57    /// Upload a local file to a browser tab via DOM.setFileInputFiles.
58    Upload(crate::cli::cmd::upload::Args),
59    /// Send a raw CDP method.
60    Cdp(crate::cli::cmd::cdp::Args),
61    /// Print a short-lived takeover URL.
62    Panel(crate::cli::cmd::panel::Args),
63    /// Query /health.
64    Health(crate::cli::cmd::health::Args),
65    /// Query /capabilities.
66    Capabilities(crate::cli::cmd::capabilities::Args),
67    /// Local profile lifecycle commands.
68    Profile(crate::cli::cmd::profile::Args),
69    /// List and close CDP targets attached to the host.
70    Tabs(crate::cli::cmd::tabs::Args),
71    /// Install, remove, or check the embedded Agent Skill (Codex, Claude Code, opencode, Hermes).
72    Skill(crate::cli::cmd::skill::Args),
73    /// Build and run the host container (Docker or Apple) from the embedded recipe.
74    Container(crate::cli::cmd::container::Args),
75}
76
77pub struct Parsed {
78    pub command: Command,
79}
80
81pub fn parse() -> Result<Parsed, Error> {
82    let cli = Cli::try_parse().map_err(|e| {
83        use clap::error::ErrorKind;
84        // Lifecycle fallback only: the afdata pre-handler normally consumes
85        // every supported version/help request before clap runs.
86        if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
87            let _ = e.print();
88            std::process::exit(0);
89        }
90        if matches!(
91            e.kind(),
92            ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand | ErrorKind::MissingSubcommand
93        ) {
94            return Error::new(
95                ErrorCode::InvalidArgument,
96                "a command is required; try: afhttp --help",
97            );
98        }
99        // clap's error type already includes usage; surface as
100        // invalid_argument so machine consumers can branch.
101        Error::new(ErrorCode::InvalidArgument, e.to_string())
102    })?;
103    let _stream_redirect_args = (&cli.stdout_file, &cli.stderr_file, &cli.output_to);
104    Ok(Parsed {
105        command: cli.command,
106    })
107}
108
109#[cfg(test)]
110mod tests {
111    use clap::CommandFactory;
112
113    use super::*;
114
115    #[test]
116    fn clap_command_flag_snapshot_matches() {
117        let mut snapshot = String::new();
118        write_command_snapshot(&Cli::command(), 0, &mut snapshot);
119        assert_eq!(
120            snapshot,
121            include_str!("../../tests/golden/cli-command-flags.txt")
122        );
123    }
124
125    #[test]
126    fn cli_contract_has_no_legacy_aliases() {
127        let command = Cli::command();
128        assert_eq!(command.get_subcommands().count(), 11);
129        let mut snapshot = String::new();
130        write_command_snapshot(&command, 0, &mut snapshot);
131        for forbidden in [
132            "  command download\n",
133            "  command takeover\n",
134            "  command hard-site\n",
135            "--profile-name",
136            concat!("profile", "_name"),
137            concat!("?", "profile="),
138            "arg timeout --timeout\n",
139            "arg health --health\n",
140            "arg network_redact --network-redact\n",
141            "arg takeover_quality --takeover-quality\n",
142            "legacy",
143        ] {
144            assert!(
145                !snapshot.contains(forbidden),
146                "CLI contract retained forbidden legacy surface {forbidden:?}: {snapshot}"
147            );
148        }
149    }
150
151    fn write_command_snapshot(cmd: &clap::Command, depth: usize, out: &mut String) {
152        let indent = "  ".repeat(depth);
153        out.push_str(&format!("{indent}command {}\n", cmd.get_name()));
154        for arg in cmd.get_arguments() {
155            let long = arg
156                .get_long()
157                .map(|v| format!(" --{v}"))
158                .unwrap_or_default();
159            out.push_str(&format!("{indent}  arg {}{long}\n", arg.get_id()));
160        }
161        for sub in cmd.get_subcommands() {
162            write_command_snapshot(sub, depth + 1, out);
163        }
164    }
165}