From bdb74615f0b40093bcde4c4ac8db298f8b0c0b19 Mon Sep 17 00:00:00 2001 From: abrichr Date: Mon, 27 Jul 2026 01:09:03 -0400 Subject: [PATCH] feat: add explicit browser install capability --- README.md | 12 ++++++++---- openadapt/cli.py | 40 +++++++++++++++++++++++++++++++++++++++- pyproject.toml | 7 ++++++- tests/test_cli_smoke.py | 11 ++++++++++- uv.lock | 7 ++++++- 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c1e84a053..8a908469a 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,11 @@ unavoidable and the outcome needs proof. ## Try it locally -OpenAdapt requires Python 3.10–3.12. The base package includes the compiler and -runtime: +OpenAdapt requires Python 3.10–3.12. Install the browser capability for the +bundled tutorial: ```bash -python -m pip install --upgrade openadapt +python -m pip install --upgrade 'openadapt[browser]' ``` Run the complete bundled tutorial with one command. It needs no account, @@ -68,7 +68,8 @@ effect verifiers, fault cases, and deployment policy. Continue with the ## Record your workflow -The browser path is available in the base installation: +The browser path is an explicit capability, so native and remote-only installs +do not carry the Playwright driver: ```bash openadapt flow record --backend web --url https://your-app.example --out rec @@ -76,6 +77,9 @@ openadapt flow compile rec --out bundle --name my-workflow openadapt flow replay bundle --url https://your-app.example --run-dir run ``` +The first browser action downloads its matching Chromium build once. A native +desktop, RDP, or Citrix workflow never downloads or imports it. + Install only the capabilities needed for native or remote work: ```bash diff --git a/openadapt/cli.py b/openadapt/cli.py index c3b24b5f0..74ccb761d 100644 --- a/openadapt/cli.py +++ b/openadapt/cli.py @@ -861,7 +861,13 @@ def version(): @main.command() -def doctor(): +@click.option( + "--backend", + type=click.Choice(["web", "windows", "macos", "linux", "rdp", "citrix"]), + default=None, + help="Explain setup for one execution surface.", +) +def doctor(backend: str | None): """Check system requirements and dependencies.""" click.echo("OpenAdapt System Check") click.echo("=" * 40) @@ -874,6 +880,37 @@ def doctor(): from importlib.util import find_spec + click.echo("\nSelected execution surface:") + if backend and backend != "web": + click.echo( + f" [OK] {backend}: browser support is not required; " + "no Playwright or Chromium setup will run. The selected " + "native or remote driver is checked when that surface opens." + ) + else: + playwright = find_spec("playwright") is not None + if not playwright: + prefix = "[SETUP]" if backend == "web" else "[--]" + click.echo( + f" {prefix} Browser: optional and not installed. Run " + "`python -m pip install 'openadapt[browser]'` before a web " + "recording or replay." + ) + else: + try: + from openadapt_flow._browser_setup import _chromium_present + + chromium = _chromium_present() + except Exception: + chromium = False + if chromium: + click.echo(" [OK] Browser: Playwright and Chromium are ready.") + else: + click.echo( + " [READY] Browser: Playwright is installed; the matching " + "Chromium downloads automatically on the first web action." + ) + # Core packages: installed by the base `pip install openadapt`. Only # these are treated as required; a missing one is a real problem. click.echo("\nCore packages (installed with `pip install openadapt`):") @@ -897,6 +934,7 @@ def doctor(): # install it rather than flagging it. Maps import name -> extra name. click.echo("\nOptional packages (install with `pip install openadapt[...]`):") optional = [ + ("playwright", "browser"), ("openadapt_capture", "capture"), ("openadapt_ml", "ml"), ("openadapt_evals", "evals"), diff --git a/pyproject.toml b/pyproject.toml index 6f0394316..8d42fb6f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,11 @@ dependencies = [ [project.optional-dependencies] # Individual packages +browser = [ + # Web recording/replay only. The base launcher stays lightweight for + # native desktop, RDP, and Citrix users; Chromium remains lazy on first use. + "playwright>=1.44", +] # Local human desktop recording. Flow owns the supported capture adapter # contract; the direct floor prevents an already-installed pre-1.0 Capture # from satisfying the launcher's public recording path. FFmpeg remains a @@ -87,7 +92,7 @@ all = [ # Cross-platform clients are always safe to resolve. Native bindings remain # conditional so `openadapt[all]` never pulls PyObjC onto Linux/Windows or # PyGObject onto macOS/Windows. - "openadapt[core,grounding,retrieval,privacy,flow,windows,rdp]", + "openadapt[browser,core,grounding,retrieval,privacy,flow,windows,rdp]", "openadapt[macos]; sys_platform == 'darwin'", "openadapt[linux]; sys_platform == 'linux'", ] diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py index 486732ffb..445ed7adf 100644 --- a/tests/test_cli_smoke.py +++ b/tests/test_cli_smoke.py @@ -209,6 +209,7 @@ def test_launcher_flow_and_substrate_extras_metadata(): assert metadata["dependencies"].count("openadapt-flow[hosted]>=1.20.1,<2.0.0") == 1 assert extras["flow"] == ["openadapt-flow>=1.20.1,<2.0.0"] + assert extras["browser"] == ["playwright>=1.44"] assert extras["privacy"] == ["openadapt-flow[privacy]>=1.20.1,<2.0.0"] assert extras["capture"] == [ "openadapt-capture>=1.0.4,<2.0.0", @@ -223,12 +224,20 @@ def test_launcher_flow_and_substrate_extras_metadata(): ] assert extras["rdp"] == ["openadapt-flow[rdp]>=1.20.1,<2.0.0"] assert extras["all"] == [ - "openadapt[core,grounding,retrieval,privacy,flow,windows,rdp]", + "openadapt[browser,core,grounding,retrieval,privacy,flow,windows,rdp]", "openadapt[macos]; sys_platform == 'darwin'", "openadapt[linux]; sys_platform == 'linux'", ] +def test_doctor_does_not_require_browser_for_citrix(monkeypatch): + monkeypatch.setattr("importlib.util.find_spec", lambda _name: None) + result = CliRunner().invoke(cli_main, ["doctor", "--backend", "citrix"]) + assert result.exit_code == 0, result.output + assert "browser support is not required" in result.output + assert "no Playwright or Chromium setup will run" in result.output + + def test_top_level_help_leads_with_flow(): """`openadapt --help` must list flow before the other commands.""" runner = CliRunner() diff --git a/uv.lock b/uv.lock index 8af908bb4..8f50b12ea 100644 --- a/uv.lock +++ b/uv.lock @@ -2266,6 +2266,10 @@ all = [ { name = "openadapt-ml" }, { name = "openadapt-retrieval" }, { name = "openadapt-viewer" }, + { name = "playwright" }, +] +browser = [ + { name = "playwright" }, ] capture = [ { name = "openadapt-capture" }, @@ -2321,8 +2325,8 @@ windows = [ requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.0" }, { name = "click", specifier = ">=8.0.0" }, + { name = "openadapt", extras = ["browser", "core", "grounding", "retrieval", "privacy", "flow", "windows", "rdp"], marker = "extra == 'all'" }, { name = "openadapt", extras = ["capture", "ml", "evals", "viewer"], marker = "extra == 'core'" }, - { name = "openadapt", extras = ["core", "grounding", "retrieval", "privacy", "flow", "windows", "rdp"], marker = "extra == 'all'" }, { name = "openadapt", extras = ["linux"], marker = "sys_platform == 'linux' and extra == 'all'" }, { name = "openadapt", extras = ["macos"], marker = "sys_platform == 'darwin' and extra == 'all'" }, { name = "openadapt-capture", marker = "extra == 'capture'", specifier = ">=1.0.4,<2.0.0" }, @@ -2339,6 +2343,7 @@ requires-dist = [ { name = "openadapt-ml", marker = "extra == 'ml'", specifier = ">=0.2.0" }, { name = "openadapt-retrieval", marker = "extra == 'retrieval'", specifier = ">=0.1.0" }, { name = "openadapt-viewer", marker = "extra == 'viewer'", specifier = ">=0.1.0" }, + { name = "playwright", marker = "extra == 'browser'", specifier = ">=1.44" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, ]