agent_first_http/cli/
args.rs1use 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)]
18pub struct Cli {
19 #[arg(long = "stdout-file", value_name = "PATH", global = true)]
21 pub stdout_file: Option<String>,
22
23 #[arg(long = "stderr-file", value_name = "PATH", global = true)]
25 pub stderr_file: Option<String>,
26
27 #[command(subcommand)]
28 pub command: Command,
29}
30
31#[derive(Subcommand, Debug)]
32pub enum Command {
33 Fetch(Box<crate::cli::cmd::fetch::Args>),
35 Host(crate::cli::cmd::host::Args),
37 Upload(crate::cli::cmd::upload::Args),
39 Cdp(crate::cli::cmd::cdp::Args),
41 Panel(crate::cli::cmd::panel::Args),
43 Health(crate::cli::cmd::health::Args),
45 Capabilities(crate::cli::cmd::capabilities::Args),
47 Profile(crate::cli::cmd::profile::Args),
49 Tabs(crate::cli::cmd::tabs::Args),
51 Skill(crate::cli::cmd::skill::Args),
53 Container(crate::cli::cmd::container::Args),
55}
56
57pub struct Parsed {
58 pub command: Command,
59}
60
61pub fn parse() -> Result<Parsed, Error> {
62 let cli = Cli::try_parse().map_err(|e| {
63 use clap::error::ErrorKind;
64 if matches!(
69 e.kind(),
70 ErrorKind::DisplayHelp
71 | ErrorKind::DisplayVersion
72 | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
73 ) {
74 let _ = e.print();
75 std::process::exit(0);
76 }
77 Error::new(ErrorCode::InvalidArgument, e.to_string())
80 })?;
81 let _stream_redirect_args = (&cli.stdout_file, &cli.stderr_file);
82 Ok(Parsed {
83 command: cli.command,
84 })
85}
86
87#[cfg(test)]
88mod tests {
89 use clap::CommandFactory;
90
91 use super::*;
92
93 #[test]
94 fn clap_command_flag_snapshot_matches() {
95 let mut snapshot = String::new();
96 write_command_snapshot(&Cli::command(), 0, &mut snapshot);
97 assert_eq!(
98 snapshot,
99 include_str!("../../tests/golden/cli-command-flags.txt")
100 );
101 }
102
103 #[test]
104 fn cli_contract_has_no_legacy_aliases() {
105 let command = Cli::command();
106 assert_eq!(command.get_subcommands().count(), 11);
107 let mut snapshot = String::new();
108 write_command_snapshot(&command, 0, &mut snapshot);
109 for forbidden in [
110 " command download\n",
111 " command takeover\n",
112 " command hard-site\n",
113 "--profile-name",
114 concat!("profile", "_name"),
115 concat!("?", "profile="),
116 "arg timeout --timeout\n",
117 "arg health --health\n",
118 "arg network_redact --network-redact\n",
119 "arg takeover_quality --takeover-quality\n",
120 "legacy",
121 ] {
122 assert!(
123 !snapshot.contains(forbidden),
124 "CLI contract retained forbidden legacy surface {forbidden:?}: {snapshot}"
125 );
126 }
127 }
128
129 fn write_command_snapshot(cmd: &clap::Command, depth: usize, out: &mut String) {
130 let indent = " ".repeat(depth);
131 out.push_str(&format!("{indent}command {}\n", cmd.get_name()));
132 for arg in cmd.get_arguments() {
133 let long = arg
134 .get_long()
135 .map(|v| format!(" --{v}"))
136 .unwrap_or_default();
137 out.push_str(&format!("{indent} arg {}{long}\n", arg.get_id()));
138 }
139 for sub in cmd.get_subcommands() {
140 write_command_snapshot(sub, depth + 1, out);
141 }
142 }
143}