Skip to main content

agent_first_http/cli/cmd/
profile.rs

1//! `afhttp profile` subcommand. Local profile lifecycle.
2
3use std::path::PathBuf;
4
5use clap::{Args as ClapArgs, Subcommand};
6
7use crate::cli::output;
8use crate::sdk::profile;
9use crate::shared::error::Error;
10use crate::shared::time::parse_duration;
11
12#[derive(ClapArgs, Debug)]
13pub struct Args {
14    #[command(subcommand)]
15    pub sub: ProfileSub,
16}
17
18#[derive(Subcommand, Debug)]
19pub enum ProfileSub {
20    /// List on-disk profiles under the profiles root.
21    List(ListArgs),
22    /// Show metadata for one profile (size, last use, lock state).
23    Info(InfoArgs),
24    /// Report whether a profile is currently locked by a running host.
25    LockStatus(InfoArgs),
26    /// List files captured in the profile's browser download directory.
27    Downloads(InfoArgs),
28    /// Delete a profile and all of its on-disk state.
29    Delete(DeleteArgs),
30    /// Delete profiles whose last use is older than a cutoff.
31    Prune(PruneArgs),
32    /// Show the non-expired cookies in a profile's jar (values redacted).
33    Cookies(InfoArgs),
34}
35
36#[derive(ClapArgs, Debug)]
37pub struct ListArgs {
38    /// Profiles root directory. Defaults to `$XDG_DATA_HOME/afhttp/profiles`.
39    #[arg(long)]
40    pub profile_root: Option<PathBuf>,
41}
42
43#[derive(ClapArgs, Debug)]
44pub struct InfoArgs {
45    /// Profile name.
46    pub name: String,
47    /// Profiles root directory. Defaults to `$XDG_DATA_HOME/afhttp/profiles`.
48    #[arg(long)]
49    pub profile_root: Option<PathBuf>,
50}
51
52#[derive(ClapArgs, Debug)]
53pub struct DeleteArgs {
54    /// Profile name to delete.
55    pub name: String,
56    /// Confirmation guard: must equal the profile name for the delete to proceed.
57    #[arg(long)]
58    pub confirm: String,
59    /// Profiles root directory. Defaults to `$XDG_DATA_HOME/afhttp/profiles`.
60    #[arg(long)]
61    pub profile_root: Option<PathBuf>,
62}
63
64#[derive(ClapArgs, Debug)]
65pub struct PruneArgs {
66    /// Age cutoff (e.g. `30d`, `12h`); profiles last used before this are removed.
67    #[arg(long)]
68    pub older_than: String,
69    /// Report what would be deleted without deleting anything.
70    #[arg(long, default_value_t = false)]
71    pub dry_run: bool,
72    /// Profiles root directory. Defaults to `$XDG_DATA_HOME/afhttp/profiles`.
73    #[arg(long)]
74    pub profile_root: Option<PathBuf>,
75}
76
77pub async fn run(args: Args) -> Result<(), Error> {
78    match args.sub {
79        ProfileSub::List(a) => {
80            let root = profile_root_for_output(a.profile_root.as_deref());
81            let entries = profile::list(a.profile_root.as_deref())?;
82            output::emit(
83                "profile_list",
84                &serde_json::json!({
85                    "profile_root": root.display().to_string(),
86                    "profiles": entries,
87                }),
88            )
89        }
90        ProfileSub::Info(a) => {
91            let entry = profile::info(&a.name, a.profile_root.as_deref())?;
92            output::emit("profile_info", &entry)
93        }
94        ProfileSub::LockStatus(a) => {
95            let status = profile::lock_status(&a.name, a.profile_root.as_deref())?;
96            output::emit("profile_lock_status", &status)
97        }
98        ProfileSub::Downloads(a) => {
99            let entry = profile::info(&a.name, a.profile_root.as_deref())?;
100            let download_dir = entry.path.join("downloads");
101            let download_dir = download_dir.canonicalize().unwrap_or(download_dir);
102            let downloads = profile::downloads(&a.name, a.profile_root.as_deref())?;
103            output::emit(
104                "profile_downloads",
105                &serde_json::json!({
106                    "name": a.name,
107                    "download_dir": download_dir.display().to_string(),
108                    "downloads": downloads,
109                }),
110            )
111        }
112        ProfileSub::Delete(a) => {
113            profile::delete(&a.name, &a.confirm, a.profile_root.as_deref())?;
114            output::emit(
115                "profile_delete",
116                &serde_json::json!({"name": a.name, "deleted": true}),
117            )
118        }
119        ProfileSub::Prune(a) => {
120            let root = profile_root_for_output(a.profile_root.as_deref());
121            let older_than = parse_duration(&a.older_than)?;
122            let removed = profile::prune(older_than, a.dry_run, a.profile_root.as_deref())?;
123            output::emit(
124                "profile_prune",
125                &serde_json::json!({
126                    "profile_root": root.display().to_string(),
127                    "dry_run": a.dry_run,
128                    "profiles": removed,
129                }),
130            )
131        }
132        ProfileSub::Cookies(a) => {
133            let entry = profile::info(&a.name, a.profile_root.as_deref())?;
134            let jar_path = entry.path.join("cookies.jar.json");
135            let jar = crate::sdk::profile::cookie_jar::CookieJar::load(&jar_path)?;
136            let cookies = jar.cookies_redacted();
137            output::emit(
138                "profile_cookies",
139                &serde_json::json!({
140                    "name": a.name,
141                    "jar_path": jar_path.display().to_string(),
142                    "count": cookies.len(),
143                    "cookies": cookies,
144                }),
145            )
146        }
147    }
148}
149
150fn profile_root_for_output(root: Option<&std::path::Path>) -> PathBuf {
151    root.map(PathBuf::from)
152        .unwrap_or_else(profile::paths::default_root)
153}