|
| 1 | +# Architecture |
| 2 | + |
| 3 | +`chrome-cdp` attaches to the user's **already-running** Chrome over the DevTools Protocol (CDP) and drives it from the command line. |
| 4 | +It never launches a headless browser for real work — the whole point is to reuse the live session's cookies, logins, and extensions. |
| 5 | + |
| 6 | +## The one-envelope, one-exit-code contract |
| 7 | + |
| 8 | +Every command emits a single `result.Envelope` (`internal/result/result.go`) and exits with a code derived from it. |
| 9 | +This contract is the load-bearing interface: both humans and the Claude skill parse against it, so treat it as public API. |
| 10 | + |
| 11 | +- `error.code` strings (fine-grained, e.g. `target_not_found`) map to process exit codes via `result.codeToExit` / `ExitCodeFor`. |
| 12 | +- Exit codes are the coarse, stable contract: `0` ok, `2` usage, `3` connection, `4` target/timeout, `5` cdp, `6` daemon. |
| 13 | +- Adding a new failure mode means adding a `Code*` constant **and** its `codeToExit` entry — an unmapped code silently degrades to `ExitGeneric`. |
| 14 | +- Usage/validation errors (exit `2`) are checked **before** touching Chrome, so a bad flag never launches or connects. |
| 15 | + |
| 16 | +## Package map (`internal/`) |
| 17 | + |
| 18 | +Data flows outermost → innermost: `cli` parses → resolves a `target` → gets a `chrome.Browser` (via `daemon` or direct) → emits a `result`. |
| 19 | + |
| 20 | +- `result` — the envelope, `Err`, and the exit-code table. No dependencies; the root of the contract. |
| 21 | +- `target` — the target grammar (`idprefix | url:<s> | title:<s> | @N`) and `Resolve` against a tab list. |
| 22 | +- `config` — layered defaults: built-in < config file (`~/.config/chrome-cdp/config.toml`) < `CHROME_CDP_*` env < flag. `Builtin()`, `Resolve()`, `FromEnv()`. |
| 23 | +- `browser` — endpoint discovery: finds Chrome's `DevToolsActivePort` file and computes the per-endpoint key (see connection model below). |
| 24 | +- `chrome` — the `Browser` interface and its chromedp-backed implementation: snapshot, click/type/fill/select, grid, wait, raw CDP. The real driver logic lives here. |
| 25 | +- `chrometest` — `StubBrowser`, a permissive `chrome.Browser` double embedded by the `cli` and `daemon` tests. |
| 26 | +- `state` — the sticky current-target store, keyed per endpoint so distinct `--port`s don't share a "current tab". |
| 27 | +- `daemon` — the held-connection RPC: a background process holds the CDP attach so Chrome's consent prompt appears once per session, not per command. |
| 28 | +- `cli` — the cobra command tree (`app.go` = wiring + envelope emission, `commands.go` = the verbs). Knows nothing about how the browser connects; `main` injects that. |
| 29 | + |
| 30 | +## Dependency injection at the seams (`cmd/chrome-cdp/main.go`) |
| 31 | + |
| 32 | +`cli.App` is deliberately ignorant of daemons, sockets, and process spawning — it holds function seams that `main` wires up: |
| 33 | + |
| 34 | +- `WithConnector` — how to get a `chrome.Browser` (daemon client vs. `--no-daemon` direct connect), invoked lazily only when a command needs Chrome. |
| 35 | +- `WithStickyTarget` — get/set the current target; keyed by `ConnOpts` so each endpoint has its own. |
| 36 | +- `WithDaemonCtl` — start/stop/status for the per-endpoint daemon. |
| 37 | +- `WithDefaults` — inject config+env defaults (tests keep `config.Builtin()`). |
| 38 | + |
| 39 | +This is why tests can inject a `chrometest.StubBrowser` directly and never spawn a process. |
| 40 | +When adding a command that needs a new capability, add the method to the `chrome.Browser` interface, give it a default in `chrometest.StubBrowser` (one place), then implement it in `internal/chrome`. |
| 41 | + |
| 42 | +## Connection & daemon model |
| 43 | + |
| 44 | +- The **endpoint key** derives from the port file + effective `--port`; it names both the daemon socket and the sticky-state file. |
| 45 | + It cannot be computed once at startup — `--port` isn't known until cobra parses flags — so `main` computes it per command from `ConnOpts`. |
| 46 | +- The **daemon** (`chrome-cdp __daemon <socket>`, a hidden mode) holds one CDP connection for ~30 min and serves commands over a Unix socket, so the "Allow debugging?" consent fires once. |
| 47 | + `--no-daemon` bypasses it and connects directly (used by tests and one-shot scripts). |
| 48 | +- Chrome M136+ dropped the classic `--remote-debugging-port` for the default profile; `browser` reads `DevToolsActivePort` and connects directly, which is why it keeps working where older tools broke. |
| 49 | + |
| 50 | +## Human vs. JSON rendering |
| 51 | + |
| 52 | +`--json` emits the raw envelope; otherwise `renderHuman` prints a terse line (`✓ …` / `✗ …` to stderr), honoring `NO_COLOR` and `--quiet`. |
| 53 | +The human path is a courtesy; the JSON envelope is the contract. Never let a human-formatting change alter the envelope shape. |
0 commit comments