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    /// Profile backend scope (for example chromium, brave, camoufox).
48    /// Required when the same profile name exists under multiple backends.
49    #[arg(long)]
50    pub backend: Option<String>,
51    /// Profiles root directory. Defaults to `$XDG_DATA_HOME/afhttp/profiles`.
52    #[arg(long)]
53    pub profile_root: Option<PathBuf>,
54}
55
56#[derive(ClapArgs, Debug)]
57pub struct DeleteArgs {
58    /// Profile name to delete.
59    pub name: String,
60    /// Profile backend scope (for example chromium, brave, camoufox).
61    /// Required when the same profile name exists under multiple backends.
62    #[arg(long)]
63    pub backend: Option<String>,
64    /// Confirmation guard: must equal the profile name for the delete to proceed.
65    #[arg(long)]
66    pub confirm: String,
67    /// Profiles root directory. Defaults to `$XDG_DATA_HOME/afhttp/profiles`.
68    #[arg(long)]
69    pub profile_root: Option<PathBuf>,
70}
71
72#[derive(ClapArgs, Debug)]
73pub struct PruneArgs {
74    /// Age cutoff (e.g. `30d`, `12h`); profiles last used before this are removed.
75    #[arg(long)]
76    pub older_than: String,
77    /// Report what would be deleted without deleting anything.
78    #[arg(long, default_value_t = false)]
79    pub dry_run: bool,
80    /// Profiles root directory. Defaults to `$XDG_DATA_HOME/afhttp/profiles`.
81    #[arg(long)]
82    pub profile_root: Option<PathBuf>,
83}
84
85pub async fn run(args: Args) -> Result<(), Error> {
86    match args.sub {
87        ProfileSub::List(a) => {
88            let root = profile_root_for_output(a.profile_root.as_deref());
89            let entries = profile::list(a.profile_root.as_deref())?;
90            output::emit(
91                "profile_list",
92                &serde_json::json!({
93                    "profile_root": root.display().to_string(),
94                    "profiles": entries,
95                }),
96            )
97        }
98        ProfileSub::Info(a) => {
99            let entry = profile::info(&a.name, a.backend.as_deref(), a.profile_root.as_deref())?;
100            output::emit("profile_info", &entry)
101        }
102        ProfileSub::LockStatus(a) => {
103            let status =
104                profile::lock_status(&a.name, a.backend.as_deref(), a.profile_root.as_deref())?;
105            output::emit("profile_lock_status", &status)
106        }
107        ProfileSub::Downloads(a) => {
108            let entry = profile::info(&a.name, a.backend.as_deref(), a.profile_root.as_deref())?;
109            let download_dir = entry.path.join("downloads");
110            let download_dir = download_dir.canonicalize().unwrap_or(download_dir);
111            let downloads =
112                profile::downloads(&a.name, a.backend.as_deref(), a.profile_root.as_deref())?;
113            output::emit(
114                "profile_downloads",
115                &serde_json::json!({
116                    "backend": entry.backend,
117                    "name": a.name,
118                    "download_dir": download_dir.display().to_string(),
119                    "downloads": downloads,
120                }),
121            )
122        }
123        ProfileSub::Delete(a) => {
124            let entry = profile::info(&a.name, a.backend.as_deref(), a.profile_root.as_deref())?;
125            profile::delete(
126                &a.name,
127                &a.confirm,
128                a.backend.as_deref(),
129                a.profile_root.as_deref(),
130            )?;
131            output::emit(
132                "profile_delete",
133                &serde_json::json!({"backend": entry.backend, "name": a.name, "deleted": true}),
134            )
135        }
136        ProfileSub::Prune(a) => {
137            let root = profile_root_for_output(a.profile_root.as_deref());
138            let older_than = parse_duration(&a.older_than)?;
139            let removed = profile::prune(older_than, a.dry_run, a.profile_root.as_deref())?;
140            output::emit(
141                "profile_prune",
142                &serde_json::json!({
143                    "profile_root": root.display().to_string(),
144                    "dry_run": a.dry_run,
145                    "profiles": removed,
146                }),
147            )
148        }
149        ProfileSub::Cookies(a) => {
150            let entry = profile::info(&a.name, a.backend.as_deref(), a.profile_root.as_deref())?;
151            let jar_path = entry.path.join("cookies.jar.json");
152            let jar = crate::sdk::profile::cookie_jar::CookieJar::load(&jar_path)?;
153            let cookies = jar.cookies_redacted();
154            output::emit(
155                "profile_cookies",
156                &serde_json::json!({
157                    "backend": entry.backend,
158                    "name": a.name,
159                    "jar_path": jar_path.display().to_string(),
160                    "count": cookies.len(),
161                    "cookies": cookies,
162                }),
163            )
164        }
165    }
166}
167
168fn profile_root_for_output(root: Option<&std::path::Path>) -> PathBuf {
169    root.map(PathBuf::from)
170        .unwrap_or_else(profile::paths::default_root)
171}