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:
@@ -466,6 +466,7 @@ This is useful for multimodal AI models that can reason about visual layout, unl
|
|||||||
| `--cdp <port\|url>` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |
|
| `--cdp <port\|url>` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |
|
||||||
| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |
|
| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |
|
||||||
| `--color-scheme <scheme>` | Color scheme: `dark`, `light`, `no-preference` (or `AGENT_BROWSER_COLOR_SCHEME` env) |
|
| `--color-scheme <scheme>` | Color scheme: `dark`, `light`, `no-preference` (or `AGENT_BROWSER_COLOR_SCHEME` env) |
|
||||||
|
| `--download-path <path>` | Default download directory (or `AGENT_BROWSER_DOWNLOAD_PATH` env) |
|
||||||
| `--config <path>` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) |
|
| `--config <path>` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) |
|
||||||
| `--debug` | Debug output |
|
| `--debug` | Debug output |
|
||||||
|
|
||||||
|
|||||||
@@ -1896,8 +1896,10 @@ mod tests {
|
|||||||
cli_proxy_bypass: false,
|
cli_proxy_bypass: false,
|
||||||
cli_allow_file_access: false,
|
cli_allow_file_access: false,
|
||||||
cli_annotate: false,
|
cli_annotate: false,
|
||||||
|
cli_download_path: false,
|
||||||
annotate: false,
|
annotate: false,
|
||||||
color_scheme: None,
|
color_scheme: None,
|
||||||
|
download_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ pub fn ensure_daemon(
|
|||||||
provider: Option<&str>,
|
provider: Option<&str>,
|
||||||
device: Option<&str>,
|
device: Option<&str>,
|
||||||
session_name: Option<&str>,
|
session_name: Option<&str>,
|
||||||
|
download_path: Option<&str>,
|
||||||
) -> Result<DaemonResult, String> {
|
) -> Result<DaemonResult, String> {
|
||||||
// Check if daemon is running AND responsive
|
// Check if daemon is running AND responsive
|
||||||
if is_daemon_running(session) && daemon_ready(session) {
|
if is_daemon_running(session) && daemon_ready(session) {
|
||||||
@@ -364,6 +365,10 @@ pub fn ensure_daemon(
|
|||||||
cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
|
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
|
// Create new process group and session to fully detach
|
||||||
unsafe {
|
unsafe {
|
||||||
cmd.pre_exec(|| {
|
cmd.pre_exec(|| {
|
||||||
@@ -447,6 +452,10 @@ pub fn ensure_daemon(
|
|||||||
cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
|
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
|
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub struct Config {
|
|||||||
pub headers: Option<String>,
|
pub headers: Option<String>,
|
||||||
pub annotate: Option<bool>,
|
pub annotate: Option<bool>,
|
||||||
pub color_scheme: Option<String>,
|
pub color_scheme: Option<String>,
|
||||||
|
pub download_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -68,6 +69,7 @@ impl Config {
|
|||||||
headers: other.headers.or(self.headers),
|
headers: other.headers.or(self.headers),
|
||||||
annotate: other.annotate.or(self.annotate),
|
annotate: other.annotate.or(self.annotate),
|
||||||
color_scheme: other.color_scheme.or(self.color_scheme),
|
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",
|
"--device",
|
||||||
"--session-name",
|
"--session-name",
|
||||||
"--color-scheme",
|
"--color-scheme",
|
||||||
|
"--download-path",
|
||||||
];
|
];
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
@@ -203,6 +206,7 @@ pub struct Flags {
|
|||||||
pub session_name: Option<String>,
|
pub session_name: Option<String>,
|
||||||
pub annotate: bool,
|
pub annotate: bool,
|
||||||
pub color_scheme: Option<String>,
|
pub color_scheme: Option<String>,
|
||||||
|
pub download_path: Option<String>,
|
||||||
|
|
||||||
// Track which launch-time options were explicitly passed via CLI
|
// Track which launch-time options were explicitly passed via CLI
|
||||||
// (as opposed to being set only via environment variables)
|
// (as opposed to being set only via environment variables)
|
||||||
@@ -216,6 +220,7 @@ pub struct Flags {
|
|||||||
pub cli_proxy_bypass: bool,
|
pub cli_proxy_bypass: bool,
|
||||||
pub cli_allow_file_access: bool,
|
pub cli_allow_file_access: bool,
|
||||||
pub cli_annotate: bool,
|
pub cli_annotate: bool,
|
||||||
|
pub cli_download_path: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_flags(args: &[String]) -> Flags {
|
pub fn parse_flags(args: &[String]) -> Flags {
|
||||||
@@ -285,6 +290,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
|| config.annotate.unwrap_or(false),
|
|| config.annotate.unwrap_or(false),
|
||||||
color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME").ok()
|
color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME").ok()
|
||||||
.or(config.color_scheme),
|
.or(config.color_scheme),
|
||||||
|
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok()
|
||||||
|
.or(config.download_path),
|
||||||
cli_executable_path: false,
|
cli_executable_path: false,
|
||||||
cli_extensions: false,
|
cli_extensions: false,
|
||||||
cli_profile: false,
|
cli_profile: false,
|
||||||
@@ -295,6 +302,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
cli_proxy_bypass: false,
|
cli_proxy_bypass: false,
|
||||||
cli_allow_file_access: false,
|
cli_allow_file_access: false,
|
||||||
cli_annotate: false,
|
cli_annotate: false,
|
||||||
|
cli_download_path: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -440,6 +448,13 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
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" => {
|
"--config" => {
|
||||||
// Already handled by load_config(); skip the value
|
// Already handled by load_config(); skip the value
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -484,6 +499,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
|||||||
"--device",
|
"--device",
|
||||||
"--session-name",
|
"--session-name",
|
||||||
"--color-scheme",
|
"--color-scheme",
|
||||||
|
"--download-path",
|
||||||
"--config",
|
"--config",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -674,6 +690,19 @@ mod tests {
|
|||||||
assert!(!flags.cli_annotate);
|
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]
|
#[test]
|
||||||
fn test_cli_multiple_flags_tracking() {
|
fn test_cli_multiple_flags_tracking() {
|
||||||
let flags = parse_flags(&args(
|
let flags = parse_flags(&args(
|
||||||
|
|||||||
+16
-1
@@ -226,6 +226,7 @@ fn main() {
|
|||||||
flags.provider.as_deref(),
|
flags.provider.as_deref(),
|
||||||
flags.device.as_deref(),
|
flags.device.as_deref(),
|
||||||
flags.session_name.as_deref(),
|
flags.session_name.as_deref(),
|
||||||
|
flags.download_path.as_deref(),
|
||||||
) {
|
) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -281,6 +282,7 @@ fn main() {
|
|||||||
},
|
},
|
||||||
flags.ignore_https_errors.then_some("--ignore-https-errors"),
|
flags.ignore_https_errors.then_some("--ignore-https-errors"),
|
||||||
flags.cli_allow_file_access.then_some("--allow-file-access"),
|
flags.cli_allow_file_access.then_some("--allow-file-access"),
|
||||||
|
flags.cli_download_path.then_some("--download-path"),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
@@ -362,6 +364,10 @@ fn main() {
|
|||||||
launch_cmd["colorScheme"] = json!(cs);
|
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) {
|
let err = match send_command(launch_cmd, &flags.session) {
|
||||||
Ok(resp) if resp.success => None,
|
Ok(resp) if resp.success => None,
|
||||||
Ok(resp) => Some(
|
Ok(resp) => Some(
|
||||||
@@ -448,6 +454,10 @@ fn main() {
|
|||||||
launch_cmd["colorScheme"] = json!(cs);
|
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) {
|
let err = match send_command(launch_cmd, &flags.session) {
|
||||||
Ok(resp) if resp.success => None,
|
Ok(resp) if resp.success => None,
|
||||||
Ok(resp) => Some(
|
Ok(resp) => Some(
|
||||||
@@ -507,7 +517,8 @@ fn main() {
|
|||||||
|| flags.args.is_some()
|
|| flags.args.is_some()
|
||||||
|| flags.user_agent.is_some()
|
|| flags.user_agent.is_some()
|
||||||
|| flags.allow_file_access
|
|| flags.allow_file_access
|
||||||
|| flags.color_scheme.is_some())
|
|| flags.color_scheme.is_some()
|
||||||
|
|| flags.download_path.is_some())
|
||||||
&& flags.cdp.is_none()
|
&& flags.cdp.is_none()
|
||||||
&& flags.provider.is_none()
|
&& flags.provider.is_none()
|
||||||
{
|
{
|
||||||
@@ -573,6 +584,10 @@ fn main() {
|
|||||||
launch_cmd["colorScheme"] = json!(cs);
|
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) {
|
match send_command(launch_cmd, &flags.session) {
|
||||||
Ok(resp) if !resp.success => {
|
Ok(resp) if !resp.success => {
|
||||||
// Launch command failed (e.g., invalid state file, profile error)
|
// Launch command failed (e.g., invalid state file, profile error)
|
||||||
|
|||||||
@@ -2106,6 +2106,7 @@ Options:
|
|||||||
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
||||||
--auto-connect Auto-discover and connect to running Chrome
|
--auto-connect Auto-discover and connect to running Chrome
|
||||||
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
|
--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)
|
--session-name <name> Auto-save/restore session state (cookies, localStorage)
|
||||||
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
|
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
|
||||||
--debug Debug output
|
--debug Debug output
|
||||||
@@ -2148,6 +2149,7 @@ Environment:
|
|||||||
AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome
|
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_ALLOW_FILE_ACCESS Allow file:// URLs to access local files
|
||||||
AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference)
|
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_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000)
|
||||||
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name
|
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)
|
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ agent-browser download <sel> <path> # Click element to trigger download
|
|||||||
agent-browser wait --download [path] # Wait for any download to complete
|
agent-browser wait --download [path] # Wait for any download to complete
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `--download-path <dir>` (or `AGENT_BROWSER_DOWNLOAD_PATH` env) to set a default download directory. Without it, downloads go to a temporary directory that is deleted when the browser closes.
|
||||||
|
|
||||||
## Mouse
|
## Mouse
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
|
|||||||
<tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr>
|
<tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr>
|
||||||
<tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr>
|
<tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr>
|
||||||
<tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
|
<tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
|
||||||
|
<tr><td><code>downloadPath</code></td><td><code>--download-path</code></td><td>string</td></tr>
|
||||||
<tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>
|
<tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -151,6 +152,7 @@ These environment variables configure additional daemon and runtime behavior:
|
|||||||
<tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr>
|
<tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr>
|
||||||
<tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr>
|
<tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr>
|
||||||
<tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr>
|
<tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr>
|
||||||
|
<tr><td><code>AGENT_BROWSER_DOWNLOAD_PATH</code></td><td>Default directory for browser downloads.</td><td>(temp directory)</td></tr>
|
||||||
<tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr>
|
<tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr>
|
||||||
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr>
|
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr>
|
||||||
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr>
|
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr>
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ agent-browser wait --load networkidle # Wait for network idle
|
|||||||
agent-browser wait --url "**/page" # Wait for URL pattern
|
agent-browser wait --url "**/page" # Wait for URL pattern
|
||||||
agent-browser wait 2000 # Wait milliseconds
|
agent-browser wait 2000 # Wait milliseconds
|
||||||
|
|
||||||
|
# Downloads
|
||||||
|
agent-browser download @e1 ./file.pdf # Click element to trigger download
|
||||||
|
agent-browser wait --download ./output.zip # Wait for any download to complete
|
||||||
|
agent-browser --download-path ./downloads open <url> # Set default download directory
|
||||||
|
|
||||||
# Capture
|
# Capture
|
||||||
agent-browser screenshot # Screenshot to temp dir
|
agent-browser screenshot # Screenshot to temp dir
|
||||||
agent-browser screenshot --full # Full page screenshot
|
agent-browser screenshot --full # Full page screenshot
|
||||||
|
|||||||
+39
-1
@@ -16,7 +16,7 @@ import {
|
|||||||
} from 'playwright-core';
|
} from 'playwright-core';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
|
import { existsSync, mkdirSync, rmSync, readFileSync, statSync } from 'node:fs';
|
||||||
import { writeFile, mkdir } from 'node:fs/promises';
|
import { writeFile, mkdir } from 'node:fs/promises';
|
||||||
import type { LaunchCommand, TraceEvent } from './types.js';
|
import type { LaunchCommand, TraceEvent } from './types.js';
|
||||||
import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
|
import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
|
||||||
@@ -116,6 +116,7 @@ export class BrowserManager {
|
|||||||
private lastSnapshot: string = '';
|
private lastSnapshot: string = '';
|
||||||
private scopedHeaderRoutes: Map<string, (route: Route) => Promise<void>> = new Map();
|
private scopedHeaderRoutes: Map<string, (route: Route) => Promise<void>> = new Map();
|
||||||
private colorScheme: 'light' | 'dark' | 'no-preference' | null = null;
|
private colorScheme: 'light' | 'dark' | 'no-preference' | null = null;
|
||||||
|
private downloadPath: string | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the persistent color scheme preference.
|
* Set the persistent color scheme preference.
|
||||||
@@ -1173,6 +1174,17 @@ export class BrowserManager {
|
|||||||
this.colorScheme = options.colorScheme;
|
this.colorScheme = options.colorScheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.downloadPath) {
|
||||||
|
this.downloadPath = options.downloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.downloadPath && (cdpEndpoint || options.autoConnect)) {
|
||||||
|
const warning =
|
||||||
|
"--download-path is ignored when connecting via CDP or auto-connect (downloads use the remote browser's configuration)";
|
||||||
|
this.launchWarnings.push(warning);
|
||||||
|
console.error(`[WARN] ${warning}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (cdpEndpoint) {
|
if (cdpEndpoint) {
|
||||||
await this.connectViaCDP(cdpEndpoint);
|
await this.connectViaCDP(cdpEndpoint);
|
||||||
return;
|
return;
|
||||||
@@ -1186,6 +1198,12 @@ export class BrowserManager {
|
|||||||
// Cloud browser providers require explicit opt-in via -p flag or AGENT_BROWSER_PROVIDER env var
|
// Cloud browser providers require explicit opt-in via -p flag or AGENT_BROWSER_PROVIDER env var
|
||||||
// -p flag takes precedence over env var
|
// -p flag takes precedence over env var
|
||||||
const provider = options.provider ?? process.env.AGENT_BROWSER_PROVIDER;
|
const provider = options.provider ?? process.env.AGENT_BROWSER_PROVIDER;
|
||||||
|
if (this.downloadPath && provider) {
|
||||||
|
const warning =
|
||||||
|
"--download-path is ignored when using a cloud provider (downloads use the remote browser's configuration)";
|
||||||
|
this.launchWarnings.push(warning);
|
||||||
|
console.error(`[WARN] ${warning}`);
|
||||||
|
}
|
||||||
if (provider === 'browserbase') {
|
if (provider === 'browserbase') {
|
||||||
await this.connectToBrowserbase();
|
await this.connectToBrowserbase();
|
||||||
return;
|
return;
|
||||||
@@ -1201,6 +1219,23 @@ export class BrowserManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.downloadPath) {
|
||||||
|
const resolved = path.resolve(this.downloadPath);
|
||||||
|
const stat = statSync(resolved, { throwIfNoEntry: false });
|
||||||
|
if (stat && !stat.isDirectory()) {
|
||||||
|
throw new Error(`Download path is not a directory: ${resolved}`);
|
||||||
|
}
|
||||||
|
if (!stat) {
|
||||||
|
try {
|
||||||
|
mkdirSync(resolved, { recursive: true });
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
throw new Error(`Cannot create download directory '${resolved}': ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.downloadPath = resolved;
|
||||||
|
}
|
||||||
|
|
||||||
const browserType = options.browser ?? 'chromium';
|
const browserType = options.browser ?? 'chromium';
|
||||||
if (hasExtensions && browserType !== 'chromium') {
|
if (hasExtensions && browserType !== 'chromium') {
|
||||||
throw new Error('Extensions are only supported in Chromium');
|
throw new Error('Extensions are only supported in Chromium');
|
||||||
@@ -1258,6 +1293,7 @@ export class BrowserManager {
|
|||||||
...(options.proxy && { proxy: options.proxy }),
|
...(options.proxy && { proxy: options.proxy }),
|
||||||
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
|
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
|
||||||
...(this.colorScheme && { colorScheme: this.colorScheme }),
|
...(this.colorScheme && { colorScheme: this.colorScheme }),
|
||||||
|
...(this.downloadPath && { downloadsPath: this.downloadPath }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.isPersistentContext = true;
|
this.isPersistentContext = true;
|
||||||
@@ -1275,6 +1311,7 @@ export class BrowserManager {
|
|||||||
...(options.proxy && { proxy: options.proxy }),
|
...(options.proxy && { proxy: options.proxy }),
|
||||||
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
|
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
|
||||||
...(this.colorScheme && { colorScheme: this.colorScheme }),
|
...(this.colorScheme && { colorScheme: this.colorScheme }),
|
||||||
|
...(this.downloadPath && { downloadsPath: this.downloadPath }),
|
||||||
});
|
});
|
||||||
this.isPersistentContext = true;
|
this.isPersistentContext = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -1283,6 +1320,7 @@ export class BrowserManager {
|
|||||||
headless: options.headless ?? true,
|
headless: options.headless ?? true,
|
||||||
executablePath: options.executablePath,
|
executablePath: options.executablePath,
|
||||||
args: baseArgs,
|
args: baseArgs,
|
||||||
|
...(this.downloadPath && { downloadsPath: this.downloadPath }),
|
||||||
});
|
});
|
||||||
this.cdpEndpoint = null;
|
this.cdpEndpoint = null;
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const launchSchema = baseCommandSchema.extend({
|
|||||||
ignoreHTTPSErrors: z.boolean().optional(),
|
ignoreHTTPSErrors: z.boolean().optional(),
|
||||||
allowFileAccess: z.boolean().optional(),
|
allowFileAccess: z.boolean().optional(),
|
||||||
colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(),
|
colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(),
|
||||||
|
downloadPath: z.string().optional(),
|
||||||
profile: z.string().optional(),
|
profile: z.string().optional(),
|
||||||
storageState: z.string().optional(),
|
storageState: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface LaunchCommand extends BaseCommand {
|
|||||||
ignoreHTTPSErrors?: boolean;
|
ignoreHTTPSErrors?: boolean;
|
||||||
allowFileAccess?: boolean; // Enable file:// URL access and cross-origin file requests
|
allowFileAccess?: boolean; // Enable file:// URL access and cross-origin file requests
|
||||||
colorScheme?: 'light' | 'dark' | 'no-preference'; // Persistent color scheme override
|
colorScheme?: 'light' | 'dark' | 'no-preference'; // Persistent color scheme override
|
||||||
|
downloadPath?: string; // Directory for browser downloads (Playwright's downloadsPath)
|
||||||
// Auto-load state file for session persistence
|
// Auto-load state file for session persistence
|
||||||
autoStateFilePath?: string;
|
autoStateFilePath?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user