Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions cli/src/artifacts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Pluggable artifact persistence. Commands that produce files (screenshots,
//! PDFs, HARs, diffs) write through [`write`] so library hosts without a real
//! filesystem (e.g. wasm) can install their own writer via
//! [`set_artifact_writer`]. When no writer is installed, [`write`] falls back
//! to `std::fs::write`, preserving native CLI behavior.

#[cfg(not(target_arch = "wasm32"))]
mod imp {
use std::sync::OnceLock;

pub type ArtifactWriter = Box<dyn Fn(&str, &[u8]) -> Result<(), String> + Send + Sync>;

static WRITER: OnceLock<ArtifactWriter> = OnceLock::new();

/// Install a process-global artifact writer. Returns an error if a writer
/// was already installed.
pub fn set_artifact_writer(writer: ArtifactWriter) -> Result<(), String> {
WRITER
.set(writer)
.map_err(|_| "artifact writer already installed".to_string())
}

pub fn write(path: &str, bytes: &[u8]) -> Result<(), String> {
if let Some(writer) = WRITER.get() {
return writer(path, bytes);
}
std::fs::write(path, bytes).map_err(|e| format!("Failed to write {}: {}", path, e))
}
}

#[cfg(target_arch = "wasm32")]
mod imp {
use std::cell::RefCell;

pub type ArtifactWriter = Box<dyn Fn(&str, &[u8]) -> Result<(), String>>;

thread_local! {
static WRITER: RefCell<Option<ArtifactWriter>> = const { RefCell::new(None) };
}

/// Install a process-global artifact writer. Returns an error if a writer
/// was already installed.
pub fn set_artifact_writer(writer: ArtifactWriter) -> Result<(), String> {
WRITER.with(|w| {
let mut slot = w.borrow_mut();
if slot.is_some() {
return Err("artifact writer already installed".to_string());
}
*slot = Some(writer);
Ok(())
})
}

pub fn write(path: &str, bytes: &[u8]) -> Result<(), String> {
WRITER.with(|w| match w.borrow().as_ref() {
Some(writer) => writer(path, bytes),
None => Err(format!(
"cannot write {}: no artifact writer installed on this platform",
path
)),
})
}
}

pub use imp::{set_artifact_writer, write, ArtifactWriter};
23 changes: 15 additions & 8 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,21 @@ impl ParseError {
}

pub fn gen_id() -> String {
format!(
"r{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros()
% 1000000
)
format!("r{}", now_micros() % 1000000)
}

#[cfg(not(target_arch = "wasm32"))]
fn now_micros() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros()
}

// SystemTime::now is unsupported and panics on wasm32-unknown-unknown.
#[cfg(target_arch = "wasm32")]
fn now_micros() -> u128 {
(js_sys::Date::now() * 1000.0) as u128
}

/// Normalize browser navigation inputs while preserving schemes Chrome can
Expand Down
1 change: 1 addition & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod artifacts;
#[cfg(not(target_arch = "wasm32"))]
pub mod chat;
pub mod color;
Expand Down
8 changes: 5 additions & 3 deletions cli/src/native/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6635,7 +6635,8 @@ async fn handle_pdf(cmd: &Value, state: &DaemonState) -> Result<Value, String> {

let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data)
.map_err(|e| format!("Failed to decode PDF: {}", e))?;
std::fs::write(&save_path, &bytes).map_err(|e| format!("Failed to save PDF: {}", e))?;
crate::artifacts::write(&save_path, &bytes)
.map_err(|e| format!("Failed to save PDF: {}", e))?;

Ok(json!({ "path": save_path }))
}
Expand Down Expand Up @@ -9266,7 +9267,7 @@ async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Valu

let output_path = cmd.get("output").and_then(|v| v.as_str());
if let (Some(out_path), Some(ref diff_data)) = (output_path, &result.diff_image) {
std::fs::write(out_path, diff_data)
crate::artifacts::write(out_path, diff_data)
.map_err(|e| format!("Failed to write diff image: {}", e))?;
}

Expand Down Expand Up @@ -9387,7 +9388,8 @@ async fn handle_har_stop(cmd: &Value, state: &mut DaemonState) -> Result<Value,

let har_str = serde_json::to_string_pretty(&har)
.map_err(|e| format!("Failed to serialize HAR: {}", e))?;
std::fs::write(&path, har_str).map_err(|e| format!("Failed to write HAR: {}", e))?;
crate::artifacts::write(&path, har_str.as_bytes())
.map_err(|e| format!("Failed to write HAR: {}", e))?;

Ok(json!({ "path": path, "requestCount": request_count }))
}
Expand Down
11 changes: 11 additions & 0 deletions cli/src/native/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,17 @@ impl BrowserManager {
) -> Result<Self, String> {
let ws_url = resolve_cdp_url(url).await?;
let client = Arc::new(CdpClient::connect_with_headers(&ws_url, headers).await?);
Self::from_client(client, ws_url, direct_page).await
}

/// Build a manager around an already-connected CDP client, e.g. one
/// created via [`CdpClient::from_transport`] on hosts that supply their
/// own WebSocket. `ws_url` is informational (reported by `cdp_url`).
pub async fn from_client(
client: Arc<CdpClient>,
ws_url: String,
direct_page: bool,
) -> Result<Self, String> {
let mut manager = Self {
client,
browser_process: None,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/native/screenshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ fn save_screenshot(
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, base64_data)
.map_err(|e| format!("Failed to decode screenshot: {}", e))?;

std::fs::write(&save_path, &bytes)
crate::artifacts::write(&save_path, &bytes)
.map_err(|e| format!("Failed to save screenshot to {}: {}", save_path, e))?;

Ok(save_path)
Expand Down
Loading