feat: add --download-path option (#536)

* feat: add --download-path option

Adds a `--download-path` flag (and `AGENT_BROWSER_DOWNLOAD_PATH` env / `downloadPath` config key) to set a default download directory for browser downloads.

Without this, Playwright stores downloads in a temp directory that is deleted when the browser closes. The new option passes through to Playwright's `downloadsPath` on `launch()` and `launchPersistentContext()`.

Fixes #507

* improvements

* fixes

* fixes
This commit is contained in:
Chris Tate
2026-02-24 07:22:55 -06:00
committed by GitHub
parent 2fe7394dbe
commit 77f2caa1bc
12 changed files with 109 additions and 2 deletions
+2
View File
@@ -1896,8 +1896,10 @@ mod tests {
cli_proxy_bypass: false,
cli_allow_file_access: false,
cli_annotate: false,
cli_download_path: false,
annotate: false,
color_scheme: None,
download_path: None,
}
}
+9
View File
@@ -220,6 +220,7 @@ pub fn ensure_daemon(
provider: Option<&str>,
device: Option<&str>,
session_name: Option<&str>,
download_path: Option<&str>,
) -> Result<DaemonResult, String> {
// Check if daemon is running AND responsive
if is_daemon_running(session) && daemon_ready(session) {
@@ -364,6 +365,10 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
}
if let Some(dp) = download_path {
cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp);
}
// Create new process group and session to fully detach
unsafe {
cmd.pre_exec(|| {
@@ -447,6 +452,10 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
}
if let Some(dp) = download_path {
cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp);
}
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008;
+29
View File
@@ -34,6 +34,7 @@ pub struct Config {
pub headers: Option<String>,
pub annotate: Option<bool>,
pub color_scheme: Option<String>,
pub download_path: Option<String>,
}
impl Config {
@@ -68,6 +69,7 @@ impl Config {
headers: other.headers.or(self.headers),
annotate: other.annotate.or(self.annotate),
color_scheme: other.color_scheme.or(self.color_scheme),
download_path: other.download_path.or(self.download_path),
}
}
}
@@ -132,6 +134,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
"--device",
"--session-name",
"--color-scheme",
"--download-path",
];
let mut i = 0;
while i < args.len() {
@@ -203,6 +206,7 @@ pub struct Flags {
pub session_name: Option<String>,
pub annotate: bool,
pub color_scheme: Option<String>,
pub download_path: Option<String>,
// Track which launch-time options were explicitly passed via CLI
// (as opposed to being set only via environment variables)
@@ -216,6 +220,7 @@ pub struct Flags {
pub cli_proxy_bypass: bool,
pub cli_allow_file_access: bool,
pub cli_annotate: bool,
pub cli_download_path: bool,
}
pub fn parse_flags(args: &[String]) -> Flags {
@@ -285,6 +290,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
|| config.annotate.unwrap_or(false),
color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME").ok()
.or(config.color_scheme),
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok()
.or(config.download_path),
cli_executable_path: false,
cli_extensions: false,
cli_profile: false,
@@ -295,6 +302,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
cli_proxy_bypass: false,
cli_allow_file_access: false,
cli_annotate: false,
cli_download_path: false,
};
let mut i = 0;
@@ -440,6 +448,13 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--download-path" => {
if let Some(s) = args.get(i + 1) {
flags.download_path = Some(s.clone());
flags.cli_download_path = true;
i += 1;
}
}
"--config" => {
// Already handled by load_config(); skip the value
i += 1;
@@ -484,6 +499,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--device",
"--session-name",
"--color-scheme",
"--download-path",
"--config",
];
@@ -674,6 +690,19 @@ mod tests {
assert!(!flags.cli_annotate);
}
#[test]
fn test_cli_download_path_tracking() {
let flags = parse_flags(&args("--download-path /tmp/dl snapshot"));
assert!(flags.cli_download_path);
assert_eq!(flags.download_path, Some("/tmp/dl".to_string()));
}
#[test]
fn test_cli_download_path_not_set_without_flag() {
let flags = parse_flags(&args("snapshot"));
assert!(!flags.cli_download_path);
}
#[test]
fn test_cli_multiple_flags_tracking() {
let flags = parse_flags(&args(
+16 -1
View File
@@ -226,6 +226,7 @@ fn main() {
flags.provider.as_deref(),
flags.device.as_deref(),
flags.session_name.as_deref(),
flags.download_path.as_deref(),
) {
Ok(result) => result,
Err(e) => {
@@ -281,6 +282,7 @@ fn main() {
},
flags.ignore_https_errors.then_some("--ignore-https-errors"),
flags.cli_allow_file_access.then_some("--allow-file-access"),
flags.cli_download_path.then_some("--download-path"),
]
.into_iter()
.flatten()
@@ -362,6 +364,10 @@ fn main() {
launch_cmd["colorScheme"] = json!(cs);
}
if let Some(ref dp) = flags.download_path {
launch_cmd["downloadPath"] = json!(dp);
}
let err = match send_command(launch_cmd, &flags.session) {
Ok(resp) if resp.success => None,
Ok(resp) => Some(
@@ -448,6 +454,10 @@ fn main() {
launch_cmd["colorScheme"] = json!(cs);
}
if let Some(ref dp) = flags.download_path {
launch_cmd["downloadPath"] = json!(dp);
}
let err = match send_command(launch_cmd, &flags.session) {
Ok(resp) if resp.success => None,
Ok(resp) => Some(
@@ -507,7 +517,8 @@ fn main() {
|| flags.args.is_some()
|| flags.user_agent.is_some()
|| flags.allow_file_access
|| flags.color_scheme.is_some())
|| flags.color_scheme.is_some()
|| flags.download_path.is_some())
&& flags.cdp.is_none()
&& flags.provider.is_none()
{
@@ -573,6 +584,10 @@ fn main() {
launch_cmd["colorScheme"] = json!(cs);
}
if let Some(ref dp) = flags.download_path {
launch_cmd["downloadPath"] = json!(dp);
}
match send_command(launch_cmd, &flags.session) {
Ok(resp) if !resp.success => {
// Launch command failed (e.g., invalid state file, profile error)
+2
View File
@@ -2106,6 +2106,7 @@ Options:
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
--auto-connect Auto-discover and connect to running Chrome
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
--session-name <name> Auto-save/restore session state (cookies, localStorage)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output
@@ -2148,6 +2149,7 @@ Environment:
AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome
AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files
AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference)
AGENT_BROWSER_DOWNLOAD_PATH Default download directory for browser downloads
AGENT_BROWSER_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000)
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)