Skip to content

Commit 9971a7b

Browse files
shrey150claude
andauthored
[STG-2080] feat: add managed local Chrome args (browserbase#2201)
## Summary Port of [browserbase/cli#127](browserbase/cli#127) into the stagehand monorepo, where the browse CLI now lives at `packages/cli` (package `browse`). The old `browserbase/cli` repo is deprecated. Adds a managed-local Chrome launch-arg surface to the browse CLI, matching the Playwright / Puppeteer / chrome-launcher convention (three repeatable/boolean flags, no JSON): - `--chrome-arg <flag>` (repeatable) — append a launch arg on top of Chrome's defaults - `--ignore-default-chrome-arg <flag>` (repeatable) — drop a specific default arg - `--no-default-chrome-args` — launch without any of Chrome's default args All three are **managed-local only** (rejected with `--remote` / `--cdp` / `--auto-connect`); `--no-default-chrome-args` and `--ignore-default-chrome-arg` are mutually exclusive. Args are threaded into Stagehand `localBrowserLaunchOptions` (`args` / `ignoreDefaultArgs`) and tracked as part of daemon target compatibility so session reuse respects them. **Usage note:** because `--chrome-arg` / `--ignore-default-chrome-arg` are variadic, pass the URL *before* them (`browse open <url> --local --chrome-arg=--foo`) or terminate flags with `--`. The CLI prints a clear error pointing this out otherwise. ## Files - `packages/cli/src/lib/driver/flags.ts` — new flag definitions - `packages/cli/src/lib/driver/mode.ts` — flag parsing, conflict checks, `managedLocalTarget(...)` / `resolveChromeArgs(...)` helpers, chrome-arg target-compatibility comparison (merged on top of the monorepo's `getRemote()` / async `stagehandOptions` refactor) - `packages/cli/src/lib/driver/command-cli.ts` — `hasChromeArgFlags(flags)` wired into `hasExplicitDriverTarget` / `hasModeOnlyFlag` - `packages/cli/src/lib/driver/types.ts` — `chromeArgs` / `ignoreDefaultArgs` / `noDefaultChromeArgs` on the managed-local target - `packages/cli/src/lib/driver/session-manager.ts` — forwards args into `localBrowserLaunchOptions` - `packages/cli/README.md` — flag table rows + example - `packages/cli/tests/driver-{commands,foundation}.test.ts` — parsing, conflicts, compatibility, pass-through - `.changeset/managed-local-chrome-args.md` — `browse` patch ## E2E Test Matrix | Command / flow | Observed output | Confidence / sufficiency | | --- | --- | --- | | `npx turbo run build --filter=browse` | `4 successful, 4 total` — builds `@browserbasehq/stagehand` (with `gen-version` + `build-dom-scripts` prereqs) then `browse`; `tsc` + oclif manifest clean | Proves the merged driver files compile against the monorepo's refactored core. | | `pnpm test` (in `packages/cli`) | `Test Files 15 passed (15)` / `Tests 212 passed (212)` — incl. the new managed-local chrome-arg foundation tests and driver-command parsing/conflict/compatibility tests | Unit + contract coverage for flag parsing, conflict rules, daemon target compatibility/reuse, and Stagehand option pass-through. | | `pnpm lint` (in `packages/cli`) | `prettier --check` clean, `eslint` clean, `tsc --noEmit` clean | Format / lint / typecheck green on the ported + merged files. | Linear: https://linear.app/browserbase/issue/STG-2080 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds managed-local Chrome launch-arg flags to the `browse` CLI so users can add or override Chrome defaults. Fulfills Linear STG-2080 and ensures daemon session reuse only when these options match. - **New Features** - New flags: `--chrome-arg <flag>` (repeatable), `--ignore-default-chrome-arg <flag>` (repeatable), `--no-default-chrome-args`. - Managed-local only; rejected with `--remote`, `--cdp`, `--auto-connect`, or implicit remote mode. - `--no-default-chrome-args` and `--ignore-default-chrome-arg` are mutually exclusive. - Flags flow into `localBrowserLaunchOptions` (`args`, `ignoreDefaultArgs`) for `@browserbasehq/stagehand`. - Daemon session reuse now compares these args; chrome-arg flags also mark an explicit driver target. - **Refactors** - Use Node’s `isDeepStrictEqual` to compare `chromeArgs` and `ignoreDefaultArgs` for target compatibility. - Restored `browse` patch changeset for the decoupled release workflow. <sup>Written for commit e3a9531. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/browserbase/stagehand/pull/2201?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 02d16b0 commit 9971a7b

9 files changed

Lines changed: 385 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"browse": patch
3+
---
4+
5+
Add Chrome launch arg flags for managed local browser sessions: `--chrome-arg <flag>` (repeatable) appends launch args on top of Chrome's defaults, `--ignore-default-chrome-arg <flag>` (repeatable) drops specific default args, and `--no-default-chrome-args` launches without any of Chrome's defaults.

packages/cli/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,14 @@ Every driver command accepts the same flags to pick where the browser runs. Mix
5757
| `--auto-connect` | Auto-discover and attach to a local Chrome with remote debugging enabled |
5858
| `--cdp <url\|port>` | Attach directly to a CDP endpoint (port, `http(s)://`, or `ws(s)://`) |
5959
| `--target-id <id>` | Select a specific CDP target when attaching to an existing browser |
60+
| `--chrome-arg <flag>` | Append a Chrome launch arg on top of the defaults (repeatable, managed-local only) |
61+
| `--ignore-default-chrome-arg <flag>` | Drop a specific Chrome default launch arg (repeatable, managed-local only) |
62+
| `--no-default-chrome-args` | Launch without any of Chrome's default args (managed-local only) |
6063

6164
```bash
6265
browse open https://example.com # default target
6366
browse open https://example.com --local --headed
67+
browse open https://example.com --local --headed --chrome-arg=--no-focus-on-navigate
6468
browse open https://example.com --remote
6569
browse open https://example.com --auto-connect
6670
browse open https://example.com --cdp 9222

packages/cli/src/lib/driver/command-cli.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,36 @@ import type { DriverCommandName } from "./commands/types.js";
44
import {
55
autoConnectFlag,
66
cdpFlag,
7+
chromeArgFlag,
78
headedFlag,
89
headlessFlag,
10+
ignoreDefaultChromeArgFlag,
911
localFlag,
12+
noDefaultChromeArgsFlag,
1013
remoteFlag,
1114
sessionFlag,
1215
sessionName,
1316
targetIdFlag,
1417
} from "./flags.js";
1518
import { getDriverStatus } from "./daemon/client.js";
16-
import { resolveConnectionTarget, type DriverModeFlags } from "./mode.js";
19+
import {
20+
hasChromeArgFlags,
21+
resolveConnectionTarget,
22+
type DriverModeFlags,
23+
} from "./mode.js";
1724
import type { ConnectionTarget } from "./types.js";
1825
import { outputJson } from "../output.js";
1926
import { runDriverCommandWithTarget } from "./runtime.js";
2027

2128
export const driverCommandFlags = {
2229
"auto-connect": autoConnectFlag,
2330
cdp: cdpFlag,
31+
"chrome-arg": chromeArgFlag,
2432
headed: headedFlag,
2533
headless: headlessFlag,
34+
"ignore-default-chrome-arg": ignoreDefaultChromeArgFlag,
2635
local: localFlag,
36+
"no-default-chrome-args": noDefaultChromeArgsFlag,
2737
remote: remoteFlag,
2838
session: sessionFlag,
2939
"target-id": targetIdFlag,
@@ -89,6 +99,7 @@ export function hasExplicitDriverTarget(flags: DriverFlags): boolean {
8999
flags.remote ||
90100
flags["auto-connect"] ||
91101
flags.cdp ||
102+
hasChromeArgFlags(flags) ||
92103
flags["target-id"] ||
93104
flags.headed ||
94105
flags.headless,
@@ -100,6 +111,7 @@ function hasModeOnlyFlag(flags: DriverFlags): boolean {
100111
(flags.local || flags.remote) &&
101112
!flags["auto-connect"] &&
102113
!flags.cdp &&
114+
!hasChromeArgFlags(flags) &&
103115
!flags["target-id"] &&
104116
!flags.headed &&
105117
!flags.headless &&

packages/cli/src/lib/driver/flags.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ export const targetIdFlag = Flags.string({
4040
helpValue: "<target-id>",
4141
});
4242

43+
export const chromeArgFlag = Flags.string({
44+
description:
45+
"Add a Chrome launch arg for managed local sessions. Repeatable.",
46+
helpValue: "<flag>",
47+
multiple: true,
48+
});
49+
50+
export const ignoreDefaultChromeArgFlag = Flags.string({
51+
description:
52+
"Drop one of Chrome's default launch args for managed local sessions. Repeatable.",
53+
helpValue: "<flag>",
54+
multiple: true,
55+
});
56+
57+
export const noDefaultChromeArgsFlag = Flags.boolean({
58+
description:
59+
"Launch managed local Chrome without any of its default launch args.",
60+
});
61+
4362
export function sessionName(value?: string): string {
4463
return value ?? process.env.BROWSE_SESSION ?? "default";
4564
}

packages/cli/src/lib/driver/mode.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isDeepStrictEqual } from "node:util";
2+
13
import { fail } from "../errors.js";
24
import { getRemote } from "./remote-binding.js";
35
import { resolveWsTarget } from "./resolve-ws.js";
@@ -6,13 +8,21 @@ import type { ConnectionTarget } from "./types.js";
68
export interface DriverModeFlags {
79
"auto-connect"?: boolean;
810
cdp?: string;
11+
"chrome-arg"?: string[];
912
headed?: boolean;
1013
headless?: boolean;
14+
"ignore-default-chrome-arg"?: string[];
1115
local?: boolean;
16+
"no-default-chrome-args"?: boolean;
1217
remote?: boolean;
1318
"target-id"?: string;
1419
}
1520

21+
interface ResolvedChromeArgs {
22+
args?: string[];
23+
ignoreDefaultArgs?: boolean | string[];
24+
}
25+
1626
function resolveHeadless(
1727
flags: Pick<DriverModeFlags, "headed" | "headless">,
1828
): boolean {
@@ -25,12 +35,28 @@ function resolveHeadless(
2535
return true;
2636
}
2737

38+
export function hasChromeArgFlags(flags: DriverModeFlags): boolean {
39+
return chromeArgFlagsInUse(flags).length > 0;
40+
}
41+
42+
function chromeArgFlagsInUse(flags: DriverModeFlags): string[] {
43+
const names: string[] = [];
44+
if (flags["chrome-arg"]?.length) names.push("--chrome-arg");
45+
if (flags["ignore-default-chrome-arg"]?.length)
46+
names.push("--ignore-default-chrome-arg");
47+
if (flags["no-default-chrome-args"]) names.push("--no-default-chrome-args");
48+
return names;
49+
}
50+
2851
export async function resolveConnectionTarget(
2952
flags: DriverModeFlags,
3053
): Promise<ConnectionTarget> {
54+
const chromeArgFlags = chromeArgFlagsInUse(flags);
55+
3156
if (flags.cdp) {
3257
failOnConflictingFlags("--cdp", [
3358
flags["auto-connect"] ? "--auto-connect" : null,
59+
...chromeArgFlags,
3460
flags.local ? "--local" : null,
3561
flags.remote ? "--remote" : null,
3662
flags.headed ? "--headed" : null,
@@ -49,6 +75,7 @@ export async function resolveConnectionTarget(
4975

5076
if (flags["auto-connect"]) {
5177
failOnConflictingFlags("--auto-connect", [
78+
...chromeArgFlags,
5279
flags.local ? "--local" : null,
5380
flags.remote ? "--remote" : null,
5481
flags.headed ? "--headed" : null,
@@ -63,26 +90,42 @@ export async function resolveConnectionTarget(
6390

6491
if (flags.remote) {
6592
failOnConflictingFlags("--remote", [
93+
...chromeArgFlags,
6694
flags.headed ? "--headed" : null,
6795
flags.headless ? "--headless" : null,
6896
]);
6997
return (await getRemote()).resolveExplicitRemoteTarget(flags);
7098
}
7199

72100
if (flags.local) {
73-
return { kind: "managed-local", headless: resolveHeadless(flags) };
101+
return managedLocalTarget(resolveHeadless(flags), resolveChromeArgs(flags));
74102
}
75103

76104
const autoRemote = (await getRemote()).autoSelectRemoteTarget();
77105
if (autoRemote) {
78106
failOnConflictingFlags("remote mode", [
107+
...chromeArgFlags,
79108
flags.headed ? "--headed" : null,
80109
flags.headless ? "--headless" : null,
81110
]);
82111
return autoRemote;
83112
}
84113

85-
return { kind: "managed-local", headless: resolveHeadless(flags) };
114+
return managedLocalTarget(resolveHeadless(flags), resolveChromeArgs(flags));
115+
}
116+
117+
function managedLocalTarget(
118+
headless: boolean,
119+
chromeArgs: ResolvedChromeArgs,
120+
): ConnectionTarget {
121+
return {
122+
...(chromeArgs.args?.length ? { chromeArgs: chromeArgs.args } : {}),
123+
...(chromeArgs.ignoreDefaultArgs !== undefined
124+
? { ignoreDefaultArgs: chromeArgs.ignoreDefaultArgs }
125+
: {}),
126+
kind: "managed-local",
127+
headless,
128+
};
86129
}
87130

88131
function failOnConflictingFlags(
@@ -96,15 +139,57 @@ function failOnConflictingFlags(
96139
fail(`${flag} cannot be combined with ${conflicts.join(", ")}.`);
97140
}
98141

142+
function resolveChromeArgs(flags: DriverModeFlags): ResolvedChromeArgs {
143+
const args = flags["chrome-arg"]?.filter((arg) => arg.length > 0);
144+
const ignoreDefaults = flags["ignore-default-chrome-arg"]?.filter(
145+
(arg) => arg.length > 0,
146+
);
147+
const noDefaults = flags["no-default-chrome-args"] === true;
148+
149+
if (noDefaults && ignoreDefaults?.length) {
150+
fail(
151+
"--no-default-chrome-args cannot be combined with --ignore-default-chrome-arg.",
152+
);
153+
}
154+
155+
const resolved: ResolvedChromeArgs = {};
156+
if (args?.length) resolved.args = args;
157+
if (noDefaults) {
158+
resolved.ignoreDefaultArgs = true;
159+
} else if (ignoreDefaults?.length) {
160+
resolved.ignoreDefaultArgs = ignoreDefaults;
161+
}
162+
return resolved;
163+
}
164+
99165
export function targetsCompatible(
100166
left: ConnectionTarget,
101167
right: ConnectionTarget,
102168
): boolean {
103169
if (left.kind !== right.kind) return false;
104170
if (left.kind === "managed-local" && right.kind === "managed-local")
105-
return left.headless === right.headless;
171+
return (
172+
left.headless === right.headless &&
173+
chromeArgsEqual(left.chromeArgs, right.chromeArgs) &&
174+
ignoreDefaultArgsEqual(left.ignoreDefaultArgs, right.ignoreDefaultArgs)
175+
);
106176
if (left.kind === "cdp" && right.kind === "cdp") {
107177
return left.endpoint === right.endpoint && left.targetId === right.targetId;
108178
}
109179
return true;
110180
}
181+
182+
function chromeArgsEqual(left?: string[], right?: string[]): boolean {
183+
return isDeepStrictEqual(left ?? [], right ?? []);
184+
}
185+
186+
function ignoreDefaultArgsEqual(
187+
left?: boolean | string[],
188+
right?: boolean | string[],
189+
): boolean {
190+
if (left === true || right === true) return left === right;
191+
return chromeArgsEqual(
192+
Array.isArray(left) ? left : undefined,
193+
Array.isArray(right) ? right : undefined,
194+
);
195+
}

packages/cli/src/lib/driver/session-manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ export class DriverSessionManager {
279279
disablePino: true,
280280
env: "LOCAL",
281281
localBrowserLaunchOptions: {
282+
...(target.chromeArgs?.length ? { args: target.chromeArgs } : {}),
283+
...(target.ignoreDefaultArgs !== undefined
284+
? { ignoreDefaultArgs: target.ignoreDefaultArgs }
285+
: {}),
282286
headless: target.headless,
283287
},
284288
verbose: 0,

packages/cli/src/lib/driver/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
export type ConnectionTarget =
2-
| { kind: "managed-local"; headless: boolean }
2+
| {
3+
chromeArgs?: string[];
4+
ignoreDefaultArgs?: boolean | string[];
5+
kind: "managed-local";
6+
headless: boolean;
7+
}
38
| { kind: "remote" }
49
| { kind: "auto-connect" }
510
| { kind: "cdp"; endpoint: string; targetId?: string };

packages/cli/tests/driver-commands.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ describe("driver commands", () => {
5959
expect(hasExplicitDriverTarget({ headed: true })).toBe(true);
6060
expect(hasExplicitDriverTarget({ headless: true })).toBe(true);
6161
expect(hasExplicitDriverTarget({ cdp: "9222" })).toBe(true);
62+
expect(
63+
hasExplicitDriverTarget({
64+
"chrome-arg": ["--no-focus-on-navigate"],
65+
}),
66+
).toBe(true);
67+
expect(
68+
hasExplicitDriverTarget({
69+
"ignore-default-chrome-arg": ["--enable-automation"],
70+
}),
71+
).toBe(true);
72+
expect(hasExplicitDriverTarget({ "no-default-chrome-args": true })).toBe(
73+
true,
74+
);
6275
});
6376

6477
it("reuses an existing daemon when a broad mode flag matches", async () => {

0 commit comments

Comments
 (0)