Commit Graph

1413 Commits

Author SHA1 Message Date
mcfn b3884e386b test(network): add phase-a coverage 2026-04-14 16:19:46 +08:00
asensagent a42d58d92b docs: comprehensive audit fix for CLI 1.0.0–1.4.2 (#520)
* docs: comprehensive audit fix for CLI 1.0.0–1.4.2

Align docs/ and skills/ with CLI actual state after 1.0.0→1.4.2 changes.

P0 — broken examples fixed:
- quickstart.mdx: add --session/--tab, use start+goto instead of open
- index.mdx: replace search→get→operate flow with start→snapshot→automate
- api-reference/cli.mdx: remove search/get sections, add full browser
  command reference with --session/--tab on all commands
- guides/browser.mdx: add --session/--tab to Local Mode examples,
  replace search→get recommended workflow with snapshot-based workflow
- guides/cli-and-skills.mdx: add --session/--tab, update skill workflow
  from search→get→operate to start→snapshot→automate

P1 — missing flags and commands:
- command-reference.md: add --provider/-p, --stealth/--no-stealth,
  --header, --session to start; add --wait-until to goto; add --by-ref
  to select; add --no-isolate, pre-context fields, structured error
  details to eval; add --screenshot-format, --selector to screenshot;
  add hermes target and --browser to setup; remove --executable-path
- SKILL.md: add missing commands to Observation row (viewport, attr,
  attrs, box, styles, describe, state, inspect-point, pdf) and
  Interaction row (focus, drag, upload, eval, mouse-move,
  cursor-position); add cloud provider workflow section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: restore search/get references per review feedback

Keep actionbook search and actionbook get sections in docs — they will
be adjusted later. Restores search/get to api-reference/cli.mdx,
index.mdx, browser.mdx recommended workflow, and cli-and-skills.mdx
core commands and skill workflow. The --session/--tab fixes on browser
commands remain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add select diagnostic output to command-reference

Document that `browser select` now returns structured diagnostics
(available values, visible texts, match mode, total count) when an
option is not found (PR #521).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 15:18:28 +08:00
Junliang Feng 7c166e2b73 fix(tests): allow clippy::too_many_arguments on select diagnostics helper
fix(tests): allow too_many_arguments on assert_select_not_found_diagnostics

clippy::too_many_arguments fires on the 8-parameter test helper.
Adding the targeted allow attribute is the right fix for a test-only
helper that doesn't warrant a struct refactor.
2026-04-14 15:09:34 +08:00
Junliang Feng fe03acfdf8 fix(select): collapse nested if-let to fix clippy collapsible_if lint (#522)
fix(select): collapse nested if-let to satisfy clippy collapsible_if

Combine the inner `if diag["status"]...` check into the outer
`if let Ok(diag)` using a let-chain `&&` guard, as required by
clippy::collapsible_if.
2026-04-14 15:02:42 +08:00
Junliang Feng 04b0d73be9 feat(select): add diagnostic details on option-not-found (#521) 2026-04-14 14:59:21 +08:00
mcfn a4cca162c6 feat(select): add diagnostic details on option-not-found
When a browser select command cannot find the requested option, return a
structured error with available values, visible texts, current match mode
(by-value or by-text), and total option count (truncated to first 20).

The JS function now returns a JSON diagnostic string instead of the bare
'option not found' literal. The Rust match block parses this JSON and
calls fatal_with_details() so callers can inspect the structured details
field to understand what options were actually available.

The old "option not found" string arm is kept as a defensive fallback.
2026-04-14 14:57:36 +08:00
mcfn 0c0f85617b test(select): add diagnostics failure coverage 2026-04-14 14:31:31 +08:00
Junliang Feng c74af883ba feat(eval): scope isolation, pre-context, structured error details (#516) 2026-04-14 14:27:28 +08:00
mcfn 4723cfed44 test(eval): add regression for async trailing line comment
Covers the case where an expression like `await Promise.resolve(1) // note`
previously caused a SyntaxError because the trailing comment commented out
the generated closing tokens of the async wrapper.
2026-04-14 14:24:16 +08:00
mcfn 914426fbbf fix(eval): guard async wrapper against trailing line comments
An expression ending with a single-line comment (e.g. `await f() // note`)
would comment out the generated closing tokens `); }})()` on the same line,
producing a parse error.

Add a newline before and after the embedded expression so trailing comments
only affect that line, not the wrapper scaffolding.
2026-04-14 14:23:06 +08:00
mcfn bb514e1b39 docs(eval): document multi-statement async limitation in --help
Multi-statement top-level await (e.g. `let x = await ...; x + 1`) cannot be
supported under default scope isolation because eval() does not inherit async
context, and `return (multi-statement)` is invalid JS syntax.

Known workarounds: --no-isolate flag, or an explicit async arrow wrapper.
2026-04-14 11:59:07 +08:00
mcfn b185f4e0c8 fix(eval): broaden await detection, add details to all fatal paths
- await detection: use contains() instead of starts_with() so expressions
  like (await Promise.resolve(42)) + 1 correctly use the async wrapper
- CDP transport error path: replace cdp_error_to_result() with
  fatal_with_details() including pre_url/pre_origin/pre_readyState
- "no result" path: replace fatal() with fatal_with_details()

All 22 eval e2e tests still pass.
2026-04-14 11:54:30 +08:00
github-actions[bot] f416dff24f chore: version packages (#519)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
actionbook-cli-v1.4.2
2026-04-14 11:33:14 +08:00
sen 2d4fc6daa8 [root]chore: add changesets for CLI v1.4.2 (#518) 2026-04-14 11:27:25 +08:00
sen dfa50c6ad2 [packages/cli]fix: harden extension bridge startup and surface error hints (#517)
* [packages/cli]fix: retry bridge port bind with exponential backoff

Extension bridge failed permanently on first bind error (e.g. TIME_WAIT
after daemon restart), leaving agents unable to attach. Retry up to 6
times with 100ms/500ms/1s/2s/5s backoff (~8.6s total) before giving up.

* [packages/cli]fix: surface chrome:// active tab as RESTRICTED_ACTIVE_TAB

When the active tab is a restricted scheme (chrome://, devtools://, etc.)
the extension's debugger.attach fails with an opaque message. Detect
restricted schemes up front (and in the error path) and return
RESTRICTED_ACTIVE_TAB with hint `pass --open-url <url>` so the agent
knows to open a regular page instead of retrying.

* [packages/cli]fix: close CdpSession WebSocket gracefully on failed session start

Extension-mode start paths that build a CdpSession and then bail (e.g.
RESTRICTED_ACTIVE_TAB, Extension.createTab error) dropped the local
CdpSession without closing the WS. Dropping only closes the writer half;
the reader task keeps the TCP connection open, so the bridge never sees
EOF and keeps the old CDP client registered. The next start attempt is
then rejected by the bridge's 1:1 gate, surfacing as an instant
SessionClosed error on the first execute_browser call.

writer_loop now sends a Close frame before returning, CdpSession::close
awaits the writer task so the handshake is flushed, and the extension
start failure branches call cdp.close().await before removing the
registry entry.

* [packages/cli]fix: wait up to 5s for extension reconnect on browser start

When the daemon is auto-started by `browser start --mode extension`, the
bridge binds port 19222 a few hundred ms before the extension completes
its WS handshake. The previous one-shot `is_extension_connected()` check
raced against that window and surfaced EXTENSION_NOT_CONNECTED even
though the extension was about to (and did) connect.

Wait up to 5s, polling every 100ms, for the extension to appear on the
bridge before giving up. Clone the SharedBridgeState out of the registry
so the registry lock is not held across the wait.

* [packages/cli]fix: print hint line for Fatal/Retryable/UserAction results

Error results carry an actionable hint set by the daemon (e.g.
`pass --open-url <url>` for RESTRICTED_ACTIVE_TAB), but the text
formatter destructured `hint` away with `..` and only printed
`error {code}: {message}`. The hint never reached the user, so every
fix that improved daemon-side hints was silently discarded by the
CLI renderer.

Append a `hint: {hint}` line when hint is non-empty for all three
error variants. JSON output already includes the hint field, so that
path is unaffected.

* [packages/cli]style: apply rustfmt to extension wait deadline

Collapse the two-line `let deadline = ...` binding that rustfmt wanted
on a single line.

* [packages/cli]fix: move bridge bind retry to background task

`spawn_bridge()` used to block the caller on `bind_with_retry` for up
to ~8.6s. Because `run_daemon()` awaited it before entering the accept
loop, daemon cold start stalled every first request — including
`--mode local`, `--mode cloud`, and pure read-only commands — behind
extension-bridge port contention, violating the documented invariant
that a failed bridge bind should only disable extension mode.

Make `spawn_bridge()` synchronous: it immediately returns a
SharedBridgeState and spawns a background task that does bind + retry
+ accept loop. Add a BridgeListenerStatus field (Binding / Listening
/ Failed) so `browser start --mode extension` can distinguish "still
binding, keep polling" from "bind failed, stop waiting" without a
JoinHandle. Extension start now also surfaces BRIDGE_BIND_FAILED when
the listener gives up, instead of timing out generically.

* [packages/cli]fix: bound CdpSession::close writer wait to 500ms

The earlier fix that made close() await the writer JoinHandle gave the
Close frame a chance to reach the peer, but removed the timeout: if the
CDP socket is half-open or the peer is stalled, close() could hang
forever, dragging `browser close` and daemon shutdown with it. That
violates the daemon's graceful-shutdown-must-be-bounded contract.

Wrap the join in `tokio::time::timeout(500ms, ...)` and abort the
writer task on timeout so the OS can reclaim the socket when the task
drops. 500ms comfortably covers a healthy Close-frame RTT while
keeping pathological shutdowns snappy. Emits a warn! trace on timeout
so the case is observable in daemon logs.
2026-04-14 11:18:23 +08:00
mcfn 19cef8fe53 feat(cli): eval scope isolation, pre-context, structured error details
- navigation/mod.rs: add get_tab_ready_state() and get_tab_origin() helpers
- eval: add --no-isolate flag; default now isolates let/const per eval
  - Non-await expressions: wrapped via (function(){ return eval(EXPR); })()
    preserving multi-statement completion value
  - Await expressions: wrapped in (async function(){ return (EXPR); })()
    since eval() cannot inherit async context in this Chrome version
- eval: capture pre_url, pre_origin, pre_readyState before execution
  and include in both success response and failure details
- eval: populate fatal_with_details() on JS exceptions with
  {stage, pre_url, pre_origin, pre_readyState, error_type}

All 10 new e2e tests pass (22 eval tests total, 0 failures).
2026-04-13 23:27:49 +08:00
mcfn e16219cfd7 test(cli): add eval improvements e2e coverage 2026-04-13 23:13:12 +08:00
sen df22a1ec40 docs: remove deprecated actionbook app documentation (#515)
[docs]chore: remove deprecated actionbook app documentation

The `actionbook app` (Electron desktop automation) feature was removed
during the CLI rewrite from TypeScript to Rust (v1.0.0+). The MDX
documentation was never cleaned up, causing users to encounter
"unknown command" errors when following the docs.

Removed:
- docs/guides/app-use.mdx (entire file)
- actionbook app section from docs/api-reference/cli.mdx
- Navigation entry from docs/docs.json
2026-04-13 20:39:56 +08:00
sen 94c7e2bea7 docs: update browser automation guide with cloud providers and CLI alignment (#514)
* [docs]docs: add cloud provider documentation for browser automation

- Add Cloud mode to Modes table and new Cloud Providers section in browser.mdx
- Add browser start/restart commands and expanded global flags in cli.mdx
- Add cloud provider tip in installation.mdx

* [docs]fix: align browser.mdx commands with actual CLI implementation

- Rename mode: Isolated → Local (matches Mode::Local enum)
- Fix command names: tab list → list-tabs, session list → list-sessions,
  wait-nav → wait navigation, wait-idle → wait network-idle,
  wait-fn → wait condition, hotkey → press, console → logs console,
  inspect → inspect-point, storage → local-storage/session-storage
- Remove 7 non-existent commands: fetch, batch, switch-frame, emulate,
  fingerprint, connect, info
- Fix argument order: fill/type use [selector] value (selector first)
- Fix coordinate format: mouse-move/inspect-point use comma x,y
- Fix flag: --full-page → --full for screenshot
- Fix cookies: session-level only (no --tab flag)
- Remove non-existent --ref and --human flags
- Remove non-existent -S short flag (use --session)
- Add --session/--tab to all per-tab command examples
- Restructure: add Session Lifecycle and Wait Conditions sections

All 47 documented commands verified against actionbook v1.4.1.

* [docs]refactor: reorder sections, remove stale content, polish wording

- Reorder: Local Mode → Cloud Providers → Extension Mode (by stability)
- Add "under active development" note on Extension mode
- Remove deprecated upgrade tip (v0.2.0 migration)
- Remove extension serve/stop mention
- Remove Risk Levels section (internal implementation detail)
- Simplify Extension Security to single paragraph
- Replace all em dashes with natural punctuation
- Simplify Session Lifecycle examples

* [docs]fix: remove unsupported -S shorthand from cli.mdx global flags

Address PR review comment: CLI only supports --session (long form),
no -S short alias exists.
2026-04-13 20:12:01 +08:00
sen 902b321818 feat: add Hermes agent integration (#513)
* [skills]feat: add Hermes-compatible metadata to actionbook SKILL.md

Add frontmatter fields that Hermes agent recognizes for skill
discovery and filtering:

- metadata.hermes.tags: enables `hermes skills search` hit
- metadata.hermes.requires_toolsets: [terminal] hides skill on
  platforms without terminal (Discord, Telegram gateways)
- prerequisites.commands: [actionbook] lets Hermes advisory-check
  that the CLI binary is on PATH
- version, license, platforms: informational fields Hermes displays

Update README AI Agent Skills section with a dedicated Hermes
one-liner install path alongside the existing npx skills add command.

* [packages/cli]feat: add Hermes as a setup target

Add SetupTarget::Hermes variant that invokes `hermes skills install`
directly instead of going through `npx skills add`. Hermes has its
own skill registry at ~/.hermes/skills/ and a native CLI installer,
so the npx path is not the right fit.

When hermes binary is missing, the error message points users to
install Hermes rather than Node.js (previously hardcoded for all
targets).

Post-install verification step checks `hermes skills list` output
to confirm the skill was actually registered before reporting success.

* [packages/cli]style: fix cargo fmt formatting

* [packages/cli]fix: match Hermes installed skill by exact identifier

Replace substring matching (line.contains) with exact column parsing
in the post-install verification. Splits hermes skills list table
rows by │ delimiter and compares the name column exactly, preventing
false positives from similarly-named skills like actionbook-extension.
2026-04-13 12:45:21 +08:00
github-actions[bot] a49caeadc3 chore: version packages (#512) actionbook-cli-v1.4.1 2026-04-12 20:22:53 +08:00
Junliang Feng 07a53a1c06 chore: add changeset for Windows Chrome cleanup patch (PR #510) (#511) 2026-04-12 20:13:43 +08:00
Junliang Feng 1b4062d8f9 fix(e2e): fix 54 Windows e2e test failures for cross-platform compatibility (#510) 2026-04-12 19:51:13 +08:00
mcfn 58fbaf5551 fix(test): log powershell stderr on non-zero exit in chrome_pids_for_profile
Address Codex review: non-zero exit from Get-CimInstance is logged but
stdout is still parsed (some Windows Server versions exit 1 on empty
result sets). Panic is reserved for spawn failure (Err path).
2026-04-12 19:50:44 +08:00
mcfn 13d49c6762 fix: remove unused profile variable in close.rs
Left behind after merge conflict resolution removed `profile` from the
return tuple. Clippy -D warnings caught the dead binding.
2026-04-12 19:34:45 +08:00
mcfn ee24aa28ff style: fix rustfmt formatting in chrome_reaper and start
Auto-format import ordering in chrome_reaper.rs and wrap long if-let
in start.rs to satisfy cargo fmt --check.
2026-04-12 19:32:14 +08:00
mcfn 6faea9940a Merge origin/main into feat/expand-windows-e2e
Resolve conflicts in packages/cli/src/browser/session/close.rs and
packages/cli/Cargo.lock. Integrate main's 3-phase provider close with
our Windows Job Object chrome_job extraction in Phase 3.
2026-04-12 19:30:40 +08:00
mcfn 638d8c2b94 fix(windows): ignore 2 flaky Windows tests + improve orphan error message
Job Object approach is blocked by CI runner's own Job Object nesting
restrictions (GitHub Actions places all processes in a parent Job).
Per team decision, ignore these 2 tests on Windows as follow-up work.

Changes:
- close_kills_chrome_process: #[cfg_attr(windows, ignore)]
- lifecycle_daemon_sigkill_orphan_recovered: #[cfg_attr(windows, ignore)]
- start.rs: if orphan Chrome still holds user-data-dir lock after kill
  attempts, return CHROME_ORPHAN_STILL_RUNNING with actionable message
  "Kill it manually: taskkill /F /IM chrome.exe" instead of a generic
  CDP_CONNECTION_FAILED timeout

Expected: 528 passed, 0 failed, 8 ignored
2026-04-12 19:05:15 +08:00
mcfn 200235296c fix(windows): add Win32_Security feature and define JOB_OBJECT_TERMINATE constant
CreateJobObjectW requires SECURITY_ATTRIBUTES from Win32_Security.
JOB_OBJECT_TERMINATE (0x0004) is not exported by windows-sys 0.59
Win32_System_JobObjects, so define it as a local constant instead.
2026-04-12 18:44:26 +08:00
mcfn e4707febe4 fix(windows): replace NtQIP/ToolHelp with Win32 Job Objects for Chrome cleanup
NtQueryInformationProcess(ProcessCommandLineInformation) is completely unusable
from a DETACHED_PROCESS daemon context — confirmed by CI across multiple attempts.

Replace the entire process-enumeration approach with Win32 named Job Objects:

- ChromeJobObject::create() creates "Local\actionbook-chrome-{profile}" at
  Chrome launch; AssignProcessToJobObject adds the main process to the job.
  All Chrome helper processes (renderer, GPU, utility) join automatically.

- On browser close: job.terminate() → TerminateJobObject kills all processes
  atomically, no WMI or snapshot enumeration needed.

- On daemon crash (SIGKILL): no destructors run, so Chrome stays alive as an
  orphan (required by lifecycle_daemon_sigkill_orphan_recovered step 4).
  The next daemon opens the named job via ChromeJobObject::open(profile) and
  calls terminate() to kill the orphan before starting fresh Chrome.

- ChromeJobObject::Drop calls TerminateJobObject + CloseHandle as a backstop
  for error paths (e.g. CDP connection failure during browser start).

Remove all dead code: NtQueryInformationProcess extern, read_process_cmdline,
enumerate_chrome_pids_for_dir, wstr_to_string, terminate_pids_and_wait,
kill_and_wait_for_chrome_by_user_data_dir*, kill_chrome_helpers_and_wait*.
Also drop Win32_System_Diagnostics_ToolHelp from Cargo.toml features.

Fixes close_kills_chrome_process and lifecycle_daemon_sigkill_orphan_recovered.
2026-04-12 18:40:37 +08:00
mcfn 90b722ee8a fix(windows): fix NtQueryInformationProcess NULL-probe + add direct PID kill
ProcessCommandLineInformation (class 60) does not reliably populate
return_length when called with a NULL buffer on Windows Server — the probe
call may return return_length=0, causing enumerate_chrome_pids_for_dir to
see None for every Chrome process and return an empty vec (root cause of
both test failures).

Fix read_process_cmdline:
- Skip the NULL probe call entirely.
- Allocate 4096 bytes upfront (covers any real Chrome command line).
- Retry with the exact size only if STATUS_INFO_LENGTH_MISMATCH is returned
  and return_length is larger than the initial buffer.

Add terminate_pid_and_wait(pid): public helper that directly terminates a
known PID via TerminateProcess + WaitForSingleObject (5s), bypassing
enumeration entirely.

Update start.rs orphan recovery to call terminate_pid_and_wait(_pid) on the
known orphan main PID first, then run the enumerate-and-sweep as a safety
net for any surviving helpers. This makes orphan cleanup robust even if
command-line enumeration is unavailable for some processes.
2026-04-12 17:50:24 +08:00
mcfn 0a622e5462 fix(windows): fix HANDLE null checks and suppress unused variable warning
- chrome_reaper.rs: HANDLE is *mut c_void in windows-sys 0.59, not usize.
  Replace `handle == 0` with `handle.is_null()` at both OpenProcess call sites.
  Remove unused BOOL import. Change /// doc comment on extern block to //.
- start.rs: rename `pid` to `_pid` in orphan detection block to suppress
  unused variable warning on Windows (Windows path uses user_data_dir, not pid).
2026-04-12 17:14:24 +08:00
mcfn 9c0b2c2cf5 fix(windows): replace WMI/PowerShell with Win32 API for Chrome process cleanup
WMI/PowerShell queries launched from the daemon process fail to enumerate
Chrome processes in some Windows contexts (DETACHED_PROCESS, non-interactive
sessions), causing kill loops to return count=0 immediately and leaving Chrome
helper processes alive after browser close.

Replace all WMI-based Chrome enumeration with Win32 kernel APIs that work
from any process context without COM/WMI initialization:

- chrome_reaper.rs: new enumerate_chrome_pids_for_dir() using
  CreateToolhelp32Snapshot + NtQueryInformationProcess(ProcessCommandLineInformation)
  to filter chrome.exe by --user-data-dir; terminate_pids_and_wait() uses
  TerminateProcess + WaitForSingleObject. Removes ps_run_and_get_count,
  build_kill_wait_ps, and kill_chrome_by_user_data_dir.

- close.rs / server.rs: simplified to kill_and_reap_async (main) followed by
  kill_and_wait_for_chrome_by_user_data_dir_async (Win32 straggler sweep).
  Win32 snapshots are unaffected by Chrome's helper re-parenting, so the
  helpers-before-main ordering is no longer needed.

- start.rs: orphan recovery simplified to single
  kill_and_wait_for_chrome_by_user_data_dir_async call.

- main.rs: add flush_and_exit() — flushes stdout before exit(1) to prevent
  buffered JSON output from being lost when stdout is redirected (fully
  buffered on Windows when piped to a file).

Adds windows-sys = "0.59" with Win32_Foundation + ToolHelp + Threading
features (already a transitive dep; no net new dependency resolution).
2026-04-12 17:05:52 +08:00
mcfn b6282a7c13 fix(windows): prefer pwsh over powershell for WMI queries in daemon context
CI runners ship pwsh.EXE (PowerShell 7); Get-CimInstance Win32_Process
works reliably from detached daemon processes under PS 7.  Under classic
powershell.exe (PS 5.1), WMI queries launched from a DETACHED_PROCESS
context may silently return empty results, causing the kill functions to
find 0 Chrome processes and return prematurely.

Switch ps_run_and_get_count and kill_chrome_by_user_data_dir to try
'pwsh' first, falling back to 'powershell' if pwsh is not available.
Add tracing::debug! to log the exe used and count found for diagnosis.
2026-04-12 16:09:12 +08:00
mcfn f9192201ef fix(windows): kill Chrome helpers before main + use Wait-Process for exit confirmation
Two root causes identified from CI run 24301361586:

1. Re-parenting transient state: when Chrome's main process dies first,
   its re-parented helpers briefly disappear from WMI, causing the
   previous wait-loop to see a false zero and return while helpers are
   still alive.  Fix: kill helpers first (while main is alive) via a
   WMI query that excludes main_pid, then reap main — no helpers remain
   to re-parent.

2. WMI cache lag: checking death via repeated WMI polls can race with
   WMI's own update latency.  Fix: use Wait-Process (process handle)
   inside the PowerShell snippet; it blocks until the kernel confirms
   the process has exited, independent of WMI cache state.

Changes:
- chrome_reaper.rs: add ps_run_and_get_count/build_kill_wait_ps helpers;
  add kill_chrome_helpers_and_wait[_async] (excludes main_pid, uses
  Wait-Process); update kill_and_wait_for_chrome_by_user_data_dir to
  also use Wait-Process for the orphan-cleanup path
- close.rs: kill_chrome_helpers_and_wait_async(main_pid) before
  kill_and_reap_async — helpers confirmed dead before main exits
- server.rs: same kill-helpers-first pattern in both shutdown paths
- start.rs: orphan recovery kills helpers first, then kills isolated
  main via kill_and_wait_for_chrome_by_user_data_dir_async
2026-04-12 16:07:03 +08:00
mcfn 623e509f13 fix(windows): switch to taskkill /F /T /PID and wait-until-gone loop for Chrome cleanup
Stop-Process -Force fails silently on Chrome sandbox processes that have
special job-object protections, so Chrome helpers survive >5 s after
browser close returns — causing close_kills_chrome_process to fail.
The orphan double-kill+sleep in start.rs also left helpers alive long
enough for the new Chrome to race with the dying lock-holder, timing out
discover_ws_url and failing lifecycle_daemon_sigkill_orphan_recovered.

Switch kill_chrome_by_user_data_dir to use `taskkill /F /T /PID` per
found PID (tree-kill, different kernel path than Stop-Process).  Add
kill_and_wait_for_chrome_by_user_data_dir that loops kill+WMI-count
every 300 ms until zero processes remain (up to 10 s), and an async
wrapper for Tokio callers.

- close.rs: after kill_and_reap_async, run the wait-loop so browser
  close does not return while any Chrome helper is still visible to WMI
- start.rs: replace double-kill+fixed-sleep with the wait-loop; new
  Chrome cannot start until the orphan's user-data-dir lock is released
- server.rs: same wait-loop in both daemon shutdown paths
2026-04-12 15:21:38 +08:00
mcfn 845e33f84c fix(windows): kill Chrome children before main process in close/shutdown
Reorder kill_chrome_by_user_data_dir to run BEFORE kill_and_reap_async
in session close and daemon shutdown paths. After the main Chrome
process dies, sandboxed children enter a transient re-parenting state
that can make them temporarily invisible to WMI queries. Killing
while the process tree is intact ensures all children are found.

This complements d7e04a3 which fixed the WMI filter issue and added
double-kill in start.rs orphan detection.
2026-04-12 14:37:59 +08:00
mcfn d7e04a3105 Fix: kill_chrome_by_user_data_dir — use Where-Object instead of WMI -Filter
WMI's -Filter "Name='chrome.exe'" can silently return no results when the
CIM provider is momentarily busy, causing the ForEach-Object body to never
run and Chrome helper processes to survive after close/orphan-kill.

Switch to PowerShell-side Where-Object (same pattern as find_chrome_pids_for_dir
in tests, which is known-reliable).  Also drop the name restriction so that
sandbox-re-parented Chrome helpers visible under any image name are caught.

In start.rs orphan detection: call kill_chrome_by_user_data_dir twice with
an 800ms gap so processes that appear in WMI only after a brief delay are
also caught, before a fresh Chrome is launched into the same user-data-dir.

Fixes: browser_lifecycle::close_kills_chrome_process (PID survives > 5s)
Fixes: browser_lifecycle::lifecycle_daemon_sigkill_orphan_recovered (timeout)
2026-04-12 14:36:01 +08:00
mcfn a81a72197c fix(windows): kill Chrome sandbox children by user-data-dir matching
Chrome's sandboxed child processes (renderer, GPU, utility) may be
re-parented by the OS during sandbox token manipulation, making them
invisible to `taskkill /F /T` process-tree enumeration.

Add `kill_chrome_by_user_data_dir()` which uses PowerShell
Get-CimInstance to find remaining chrome.exe processes whose command
line contains the session's --user-data-dir path, and kills them
individually.  Called from:
- browser close (close.rs)
- orphan detection on browser start (start.rs)
- daemon shutdown (server.rs, both graceful and forced paths)
2026-04-12 13:28:29 +08:00
mcfn 95223f5915 fix(windows): kill Chrome process tree to prevent helper lingering
On Windows, child.kill() / taskkill /F /PID only terminates the main
Chrome process, leaving renderer/GPU/utility helper processes alive.
These helpers continue holding the user-data-dir lock, causing:

- close_kills_chrome_process: PowerShell finds helper PIDs after close
- lifecycle_daemon_sigkill_orphan_recovered: new Chrome hangs on launch
  because the orphan's helpers still hold the profile directory lock

Fix:
1. chrome_reaper.rs: add taskkill /F /T /PID before child.kill() on
   Windows to kill the entire process tree before the main-process kill.
2. start.rs orphan detection: change /F /PID to /F /T /PID and extend
   wait to 1000ms so helpers are fully gone before launching new Chrome.
3. browser_lifecycle.rs: replace fixed 500ms sleep with a 5s poll loop
   in close_kills_chrome_process to handle WMI lag after process tree kill.
2026-04-12 12:57:18 +08:00
mcfn 94eea61490 fix(windows): resolve daemon restart timeout and Chrome PID detection
- Remove daemon.port after taskkill in restart_daemon_windows() so the
  TCP-based is_pid_alive() check returns false immediately instead of
  timing out for 5 seconds.
- Add daemon.lock to cleanup_stale_files_windows().
- Fix PowerShell -like pattern escaping in find_chrome_pids_for_dir():
  backslash is literal in single-quoted strings, so doubling it caused
  the pattern to never match Windows paths.
2026-04-12 12:29:04 +08:00
mcfn d0cb9a9bb3 fix(windows): resolve remaining 10 Windows e2e test failures
- Replace deprecated `wmic` with PowerShell `Get-CimInstance` in
  `find_chrome_pids_for_dir()` — wmic removed from newer Windows Server
  images (fixes 3 tests)
- Use separate `daemon.lock` for exclusive singleton lock instead of
  locking `daemon.pid` — Windows mandatory byte-range locks prevent
  other processes from reading locked files (fixes 6 tests)
- Trim snapshot content before `starts_with("- ")` check to handle
  potential CRLF/BOM prefix on Windows (fixes 1 test)
2026-04-12 11:47:44 +08:00
github-actions[bot] 35247f649e chore: version packages (#509)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
actionbook-cli-v1.4.0
2026-04-12 11:39:58 +08:00
mcfn 5c4c5039d1 style(e2e): apply rustfmt to Windows compatibility fixes
Auto-format browser_lifecycle.rs and cloud_mode.rs to pass CI lint check.
2026-04-12 11:23:01 +08:00
mcfn 288af816dd test(e2e): fix 54 Windows e2e test failures for cross-platform compatibility
- cloud_mode.rs: find_chrome_executable() gains #[cfg(windows)] branch
  with PROGRAMFILES/PROGRAMFILES(X86)/LOCALAPPDATA Chrome paths and
  uses `where` instead of `which` to locate Chrome on Windows

- browser_lifecycle.rs: replace Unix-only process management with
  cross-platform helpers
  - Add pid_is_alive() using tasklist on Windows, kill -0 on Unix
  - Add force_kill_pid() using taskkill /F on Windows, kill -9 on Unix
  - Add kill_chrome_for_dir() using taskkill /IM chrome.exe on Windows, pkill on Unix
  - find_chrome_pids_for_dir() gains #[cfg(windows)] wmic branch
  - daemon_shutdown_kills_all_chrome: #[cfg_attr(windows, ignore)]
    (SIGTERM-based graceful shutdown is Unix-only)
  - lifecycle_daemon_sighup_closes_chrome: #[cfg_attr(windows, ignore)]
    (SIGHUP does not exist on Windows)

- interaction.rs: fix upload error-path tests and cursor_position tests
  - Replace hardcoded /tmp/example.txt with std::env::temp_dir() so
    Path::is_absolute() returns true on Windows (fixes INVALID_ARGUMENT
    errors masking the expected SESSION_NOT_FOUND / TAB_NOT_FOUND)
  - cursor_position_json, cursor_position_text, cursor_position_after_click_json:
    #[cfg_attr(windows, ignore)] — mouse-move coordinate tracking is
    unreliable on Windows headless Chrome

Fixes the 54 pre-existing Windows e2e failures discovered after ACT-897.
2026-04-12 11:18:46 +08:00
sen 5bddf13420 [packages/cli] Improve cloud provider session lifecycle (#507)
* [packages/cli]feat: add cloud browser provider support (-p flag)

Adds first-class support for launching cloud browser sessions through
4 providers (driver.dev, hyperbrowser, browseruse, browserless) via
the new `-p`/`--provider` flag, plus supporting registry, restart and
output plumbing.

Provider module
- New `browser/session/provider.rs` with typed `connect_provider` /
  `close_provider_session` and per-provider env-var resolution.
- HTTP client built with bounded request (30s) and connect (10s)
  timeouts so a hung control-plane can't stall the daemon. Cleanup
  client uses tighter 10s/5s budgets.
- Hyperbrowser profile IDs are normalised to UUID v5 so non-UUID
  identifiers (e.g. "user-42") map deterministically.

Cloud session lifecycle
- Registry tracks `provider` + `provider_session` so cleanup paths
  can release paid sessions on close/restart/error.
- New `find_cloud_session_by_provider(provider, profile)` reuse path
  in `start::execute` performs a `Target.getTargets` health probe
  before reuse and removes stale entries on failure (mirrors the
  existing endpoint reuse path).
- `execute_cloud` cleanup paths funnel through `fail_reserved_cloud_start`
  / `cleanup_provider_session_if_any` so all four error branches
  release the provider session — `panic = "abort"` means we cannot
  rely on Drop.
- Restart hybrid policy: stateless providers (driver.dev, browseruse)
  reuse the original cdp_endpoint + headers so a rotated env var
  cannot break a stable session; stateful providers (hyperbrowser,
  browserless) re-mint via the connect path. Provider tag is
  re-applied to the new entry so observability survives.

Errors and redaction
- New `CliError::ApiUnauthorized` (401/403), `ApiRateLimited` (429),
  `ApiServerError` (5xx) variants with stable codes and actionable
  hints; rate-limit and server errors are marked retryable.
- `redact_endpoint` now scrubs query parameters whose name matches a
  secret key list (apiKey, token, password, …, case-insensitive) in
  addition to long path segments. Provider WSS URLs carry the API
  key in the query string, so the previous path-only redactor was
  leaking credentials in success responses.
- 5 new redaction unit tests cover query, path, case and edge cases.

Config / CLI / output
- `BrowserConfig.provider` + `ACTIONBOOK_BROWSER_PROVIDER` env var
  with cloud-mode auto-flip when a provider is set implicitly.
- `browser status` / `browser list` / start responses now surface
  the provider name when present.

* [packages/cli]fix: forward provider env vars from CLI client to daemon

The daemon's std::env::var reads its own frozen environment (captured at
daemon-spawn time), not the user's current shell. This silently broke
provider config: rotating an API key in the shell never reached the daemon
until it was killed and restarted.

Add a per-request snapshot mechanism: the CLI client process collects all
DRIVER_DEV_*/HYPERBROWSER_*/BROWSER_USE_*/BROWSERLESS_* env vars via
collect_provider_env_from_process() and ships them in start::Cmd.provider_env
and restart::Cmd.provider_env (#[arg(skip)], serde default). The daemon
reads from this map instead of touching its own env.

For close/restart, snapshot the env onto ProviderSession.provider_env so
the daemon can talk to the provider control plane later, even when the
calling shell no longer has the keys exported.

* [packages/cli]fix: rewrite driver.dev as stateful provider with REST API

The previous implementation hardcoded wss://cdp.driver.dev as a "stateless"
provider, which DNS-resolves to NXDOMAIN. The real driver.dev is stateful:
mint a session via POST https://api.driver.dev/v1/browser/session and use
the per-session distributed cdpUrl returned in the response.

Changes:
- Replace DRIVER_DEV_WS_BASE constant with DRIVER_DEV_API_BASE
- Rewrite connect_driver_dev to POST /v1/browser/session with bearer auth
  and parse {sessionId, cdpUrl}; supports country, nodeId, type, proxyUrl,
  windowSize, profile body fields
- Add driver.dev case to close_provider_session (DELETE with sessionId)
- DRIVER_DEV_WS_URL escape hatch preserved for tests / private deployments
- Accept both DRIVER_DEV_API_KEY (Actionbook namespace) and DRIVER_API_KEY
  (driver.dev's official docs name) via read_driver_dev_api_key
- Broaden PROVIDER_ENV_PREFIXES from "DRIVER_DEV_" to "DRIVER_" so the
  fallback official-name keys also reach the daemon
- Add is_driver_dev_auth_failure body sniffer: driver.dev returns HTTP 500
  with {"error":"Invalid consumer token"} for bad credentials, which the
  generic 5xx → ApiServerError(retryable=true) mapping wrongly classifies.
  Reclassify known auth bodies to ApiUnauthorized(retryable=false).
- 3 new unit tests

* [packages/cli]fix: filter dangerous URLs from preserved open_url on restart

restart preserves the registry's tab.url so the new session lands on the
same page. But the registry's tab.url is captured at session-launch time
and only refreshed by list-tabs — after a goto, the in-memory copy is
stale. Worse, driver.dev launches with a data:text/html,<title>... watermark
page that the L3 dangerous-protocol guard blocks on re-navigation.

Drop data:, javascript:, and about:blank from the preserved open_url so
the new session boots to the provider default and the user can goto again
from a clean state.

Also update the stale comment that listed driver.dev as a stateless
provider — after the API rewrite it has a control-plane session that needs
explicit cleanup.

* [packages/cli]fix: redact cdp_endpoint in reuse and local start paths

reuse_running_session emitted entry.ws_url verbatim, which for cloud sessions
contains the raw provider WSS URL with tokens embedded as query params
(e.g. Hyperbrowser's JWT). HYP-3 reproduced this in E2E by reusing a running
session — the JWT showed up plaintext in stdout.

execute_local was inconsistent for the same reason: local CDP URLs don't
carry secrets, but the rule should be uniform — every cdp_endpoint emission
must go through redact_endpoint().

Both sites now route through redact_endpoint(). All four cdp_endpoint JSON
emissions in start.rs are now redacted.

* Fix cloud provider session lifecycle

* [packages/cli]fix: address PR #507 cloud provider review comments

Bundle of PR #507 review fixes for cloud provider session lifecycle:

P1 — correctness / resource leaks
- start.rs: cleanup provider session on race with concurrent close
  during startup (codex P1, start.rs:1064). When the placeholder
  reserved at the top of execute_cloud is removed while we were busy
  minting the remote session, the local provider_session handle was
  dropped without stopping the remote browser. Now release the lock
  and call cleanup_provider_session_if_any before returning
  SESSION_NOT_FOUND.
- start.rs: close stale provider session before reconnecting on
  failed health probe (codex P2, start.rs:314). A transient CDP hiccup
  would orphan the previous remote session and spin up a duplicate.
- close.rs: introduce Closing state to close out the TOCTOU race
  between Phase 1 (clone handle) and Phase 2 (HTTP PUT stop). A second
  concurrent close now short-circuits with SESSION_CLOSING; on
  provider stop failure the state reverts to the prior status so the
  caller can retry.
- registry.rs: find_cloud_session_by_provider ignores WS_URL override
  sessions (they have no provider_session handle); is_active() now
  excludes both Closing and Closed.

P2 — output / configuration hygiene
- start.rs: endpoint_for_mode() emits local loopback CDP URLs
  verbatim (so the caller can actually attach) while still redacting
  cloud ws_urls that embed tokens.
- restart.rs: stateful provider restart keeps user-supplied CDP
  headers (codex P2, restart.rs:177) instead of clearing them to
  vec![], so sessions that needed custom headers for the initial
  connect can also reconnect.
- goto.rs: refresh TabEntry.url/title in the registry after a
  successful navigation so a restart-preserved open_url reflects the
  navigated page, not the launch-time stub.
- Cargo.toml: release profile uses lto = "thin" (full LTO was
  SIGKILLing the binary on macOS before commands reached the daemon).

P3 — observability / internals
- provider.rs: each connect_* helper now sets provider_env directly
  when constructing ProviderSession, removing the post-hoc assignment
  in connect_provider.
- provider.rs: is_driver_dev_auth_failure logs its classification
  decision (ApiUnauthorized vs ApiServerError) with a 120-char body
  snippet for easier triage.

Verified: cargo check --all-targets clean, cargo test --lib 335/335
passing. Pre-existing clippy warnings (collapsible_if, too_many_args)
are unrelated to this change.

* [packages/cli]style: fix fmt + clippy lints for CI

Addresses `Lint (fmt + clippy)` check failures on PR #507.

fmt: let cargo fmt rewrap a handful of long function signatures,
assert!() args, and closures to the project default width. No
semantic changes.

clippy:
- restart.rs:134 — collapse nested `if let Some(..)` / `if let Err(..)`
  into a single `let`-chain condition.
- restart.rs:187 — replace the `if let Some(saved) / else` fallback
  with `saved_provider_env.unwrap_or_default()`.
- start.rs:861 — collapse the same nested `if let` pattern in
  `cleanup_provider_session_if_any`.
- start.rs:908 — annotate `execute_cloud` with
  `#[allow(clippy::too_many_arguments)]`. All 8 arguments are
  independent inputs resolved by the caller; folding them into a
  struct would just be lint placation.

Verified locally: cargo fmt -- --check, cargo clippy --all-targets
-- -D warnings, and cargo test --lib (335/335) all clean.

* [packages/cli]docs: make -p/--provider self-documenting for agents

Before: \`-p <PROVIDER>\` rendered a single-line help string with no
list of valid values and no hint about the required auth env var.
Agents reading \`browser start --help\` had to guess a name, run the
command, read the runtime error, then guess the env var — 2–3 round
trips per provider.

After: the same field uses a \`PossibleValuesParser\` with per-value
help text, and the doc comment / \`after_help\` block spell out the
conflict and env-var rules up front. One \`--help\` read is enough
for an agent to pick a provider, know what to export, and see a
working command line.

Rendered output:

  -p, --provider <PROVIDER>
          Cloud browser provider (implies --mode cloud).

          ... mutual exclusion + stateful-restart note ...

          Possible values:
          - driver:       driver.dev — requires DRIVER_DEV_API_KEY (or DRIVER_API_KEY)
          - hyperbrowser: hyperbrowser.ai — requires HYPERBROWSER_API_KEY
          - browseruse:   browser-use.com — requires BROWSER_USE_API_KEY

Plus an after_help section with export-then-run examples for each
provider and a note about \`browser restart\` minting fresh remote
sessions for provider-backed IDs.

Implementation notes:
- Kept \`Cmd.provider\` as \`Option<String>\` deliberately. Switching to
  a \`ValueEnum\` would have required touching \`config.rs\`,
  \`restart.rs\`, registry lookups, and three in-repo test setups —
  the string form survives round-tripping through IPC, TOML config,
  and env merging with no conversion helpers.
- The \`browseruse\` alias for \`browser-use\` is preserved via
  \`PossibleValue::aliases\`.
- \`normalize_provider_name\` still runs at \`execute()\` time so
  daemon-side IPC input gets the same validation that clap now
  enforces for CLI input.
- Clap now rejects invalid \`-p\` values at parse time with
  \`[possible values: driver, hyperbrowser, browseruse]\` — the
  runtime branch at \`execute()\` remains as an IPC safety net.

* [packages/cli]refactor: drop DRIVER_DEV_API_KEY alias, accept only DRIVER_API_KEY

driver.dev's official docs use the bare `DRIVER_API_KEY` name for the
auth credential. We previously accepted both `DRIVER_DEV_API_KEY` and
`DRIVER_API_KEY` as a transitional alias, but that exposed two names
for the same secret in every surface (--help, error hints, docs) and
confused agents reading the help output.

Keep only `DRIVER_API_KEY`:
  - provider.rs: `read_driver_dev_api_key` no longer falls back to the
    namespaced name; error message names only `DRIVER_API_KEY`.
  - start.rs: after_help and PossibleValue help text drop the alias.
  - error.rs: ApiUnauthorized hint names `DRIVER_API_KEY`.
  - tests: rewritten to assert the retired name is rejected so a
    future silent-fallback regression is caught.

`PROVIDER_ENV_PREFIXES` still includes `DRIVER_` (not `DRIVER_DEV_`),
so `DRIVER_API_KEY` and any `DRIVER_DEV_*` tuning knobs both survive
the CLI→daemon env forward.

* [root]chore: add changeset for CLI v1.4.0 (cloud provider support)

Minor bump for the `-p / --provider` cloud browser provider feature
(driver.dev, hyperbrowser.ai, browser-use.com) plus the stateful
session lifecycle, self-documenting --help, and the DRIVER_API_KEY
alias cleanup shipped on feature/cli-cloud-provider.
2026-04-12 11:00:56 +08:00
mcfn ccac471dda fix: add missing -- separator for test-threads arg in Windows e2e workflow
--test-threads is a test harness argument, not a cargo argument, so it
must come after the -- separator.
2026-04-12 10:13:02 +08:00
mcfn aacbb889e3 ci: run full e2e suite on Windows CI
Now that pipe inheritance hang is fixed, run the complete e2e test
suite on Windows instead of only windows_daemon tests.
2026-04-12 10:07:36 +08:00
Junliang Feng 11e71d9d03 feat(daemon): add Windows TCP localhost IPC support (ACT-897) (#508)
* test(daemon): add Windows daemon red coverage (ACT-897)

Add tests and CI configuration for Windows daemon TCP transport:
- config.rs: test_actionbook_home_uses_userprofile_when_home_unset — cross-platform
  red test; fails until USERPROFILE fallback is implemented
- server.rs: port_path() stub (path-only) + #[cfg(all(test,windows))] unit tests for
  is_daemon_running, send_sigterm, is_pid_alive Windows behaviour
- tests/e2e/windows_daemon.rs: 3 e2e tests ignored on non-Windows, run on Windows CI
- .github/workflows/e2e-windows.yml: windows-latest CI job for unit + e2e tests

* feat(daemon): add Windows TCP localhost IPC support (ACT-897)

Unix behaviour is unchanged (UnixListener/UnixStream, flock).
Windows now uses TCP localhost instead of the missing Unix socket:

server.rs:
- run_daemon(): bind TcpListener on 127.0.0.1:0, write ephemeral port to
  daemon.port; use fs2::FileExt for cross-platform exclusive file locking
- is_daemon_running(): TCP connect-probe against daemon.port (Windows)
- send_sigterm(): taskkill /F /PID (Windows)
- is_pid_alive(): delegates to is_daemon_running() TCP probe (Windows)
- handle_connection_inner(): generic AsyncRead+AsyncWrite handler shared by
  both platforms; Unix/Windows handle_connection() call it with the
  appropriate stream type
- port_path(): daemon.port path (used both platforms for path derivation)

client.rs:
- DaemonClient (Windows): TcpStream-based connect/send_action
- auto_start_daemon_windows(): spawns daemon with DETACHED_PROCESS |
  CREATE_NEW_PROCESS_GROUP flags so it survives parent exit
- wait/restart/cleanup helpers mirroring the Unix equivalents

config.rs:
- actionbook_home(): fall back to USERPROFILE when HOME is not set
  (standard Windows home directory convention)

Cargo.toml:
- Add fs2 = "0.4" for cross-platform LockFileEx file locking

* style: apply cargo fmt to windows_daemon tests

* fix: serialize USERPROFILE test with config test_lock to prevent env var race

The test_actionbook_home_uses_userprofile_when_home_unset test manipulates
ACTIONBOOK_HOME and HOME env vars without acquiring the shared test_lock,
causing intermittent failures in other config tests that run concurrently.

* fix: gate Unix-only kill() FFI behind #[cfg(unix)] for Windows build

- start.rs: wrap orphan Chrome kill(pid, 0)/kill(pid, 9) in #[cfg(unix)],
  add #[cfg(windows)] equivalent using taskkill
- chrome_reaper.rs: gate Duration/Instant imports behind #[cfg(unix)]
  since they're only used in the Unix SIGTERM graceful shutdown path

* fix: make drop_session_entry test cross-platform for Windows CI

The test used Unix-only `sleep` and `kill -0` commands. Use `ping` for
a long-lived process on Windows and `tasklist` for liveness checks.

* ci: limit Windows e2e to lifecycle, tab management, and daemon tests

Run only windows_daemon, browser_lifecycle, and tab_management test
modules on Windows CI to avoid timeouts from tests not yet adapted
for Windows. Run sequentially with --test-threads=1.

* ci: narrow Windows e2e to windows_daemon + tab_management

browser_lifecycle tests hang on Windows (close_kills_chrome_process
stuck for 30min). These tests exercise daemon lifecycle edge cases
not yet adapted for Windows. Narrow scope to windows_daemon and
tab_management which cover core IPC and browser operations.

* ci: narrow Windows e2e to windows_daemon tests only

tab_management tests require full Chrome browser interaction and hang
on Windows CI. Only run our Windows-specific daemon IPC tests which
validate the TCP localhost approach without Chrome dependencies.

* fix: avoid pipe-inheritance hang in e2e harness on Windows

On Windows, assert_cmd pipes stdout/stderr. When the CLI spawns a
detached daemon, the daemon inherits those pipe handles via
CreateProcess(bInheritHandles=TRUE). Even after the CLI exits, the
daemon still holds write handles, blocking the test's read_to_end()
forever.

Fix: redirect stdout/stderr to temp files on Windows instead of pipes.
Also make SoloEnv::drop() cross-platform (taskkill on Windows).

* fix: add wait-timeout dev-dependency for Windows e2e harness

The Windows-specific timeout code in the e2e harness uses
wait_timeout::ChildExt directly. It was previously only available
transitively via assert_cmd, which isn't sufficient on Windows.

* fix: add Windows Chrome discovery and fix e2e test subcommand

- find_chrome(): Add Windows paths (Program Files, LOCALAPPDATA) and
  use `where` instead of `which` for PATH lookup on Windows
- Fix session_status_works_over_tcp test: use 'status' subcommand
  (not 'session-status' which doesn't exist)

* fix: use test env's ACTIONBOOK_HOME for daemon.port path assertion

The port file test was checking the global ~/.actionbook/daemon.port
but the test daemon writes to SoloEnv's isolated temp directory.
2026-04-12 10:06:24 +08:00
Junliang Feng 09ffea1a2f fix(cli): unify version output for all subcommands (ACT-893)
The JS wrapper's isVersionOnlyFlag only matched when --version was the
sole argument, so `actionbook browser --version` fell through to the
Rust binary and printed CARGO_PKG_VERSION instead of the npm package
version. Rename to isVersionFlag and use args.some() to intercept
--version/-V regardless of preceding subcommands.

Closes ACT-893
2026-04-12 00:10:55 +08:00