browser4-cli 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,10 +109,11 @@ browser4-cli -s=<session> <command> [args] [options]
109
109
 
110
110
  | Flag | Description |
111
111
  |--------------------|------------------------------------------------|
112
- | `--help [command]` | Print help (optionally for a specific command) |
113
- | `--version` | Print version |
112
+ | `-h`, `--help [command]` | Print help (optionally for a specific command) |
113
+ | `-v`, `--version` | Print version |
114
114
  | `-s=<name>` | Named session label |
115
115
  | `--server=<url>` | Override Browser4 server URL |
116
+ | `--proxy=<url>` | Manual HTTP proxy override for downloads |
116
117
  | `--json` | Emit machine-parseable JSON to stdout |
117
118
  | `-q`, `--quiet` | Suppress normal output, only show errors |
118
119
 
@@ -120,6 +121,9 @@ browser4-cli -s=<session> <command> [args] [options]
120
121
  single-line JSON envelope (`{"status":"ok","command":"<name>","output":{...}}`).
121
122
  Omit `--json` for the default human-readable output.
122
123
 
124
+ `--proxy=<url>` sets a manual HTTP proxy used only for downloading the
125
+ Browser4 runtime bundle. It does not affect browser traffic.
126
+
123
127
  `-q` / `--quiet` suppresses all normal stdout output. Errors and
124
128
  progress messages still go to stderr. Combine with `--json` for
125
129
  silent-on-success scripting: `browser4-cli --json -q open`.
@@ -138,20 +142,20 @@ The tables below mirror the commands surfaced by the global `browser4-cli help`
138
142
 
139
143
  | Command | Description |
140
144
  |---|---|
141
- | `open [url]` | Open or switch to a browser session. Supports `--headed` (force visible window) and `--headless` (force headless). |
145
+ | `open [url]` | Open or switch to a browser session. Supports `--headed` (force visible window), `--headless` (force headless), `--profile=<path>`, `--profile-mode=<mode>` (temporary / sequential / default), and `--interact-level=<level>` (FASTEST / FAST / DEFAULT). |
142
146
  | `close` | Close the active session |
143
147
  | `goto <url>` | Navigate to a URL, auto-opening or refreshing the session if needed |
144
- | `click <ref> [button]` | Click an element |
145
- | `dblclick <ref> [button]` | Double-click an element |
146
- | `type <text> [ref]` | Type text into the focused element or an optional target element |
147
- | `fill <ref> <text>` | Fill text into an editable element |
148
+ | `click <ref> [button]` | Click an element. Supports `--modifiers` (modifier keys to press). |
149
+ | `dblclick <ref> [button]` | Double-click an element. Supports `--modifiers` (modifier keys to press). |
150
+ | `type <text> [ref]` | Type text into the focused element or an optional target element. Supports `--submit` (press Enter after typing). |
151
+ | `fill <ref> <text>` | Fill text into an editable element. Supports `--submit` (press Enter after filling). |
148
152
  | `hover <ref>` | Hover over an element |
149
153
  | `select <ref> <val>` | Select an option in a dropdown |
150
154
  | `upload <ref> <file>` | Upload a file |
151
155
  | `check <ref>` | Check a checkbox or radio button |
152
156
  | `uncheck <ref>` | Uncheck a checkbox or radio button |
153
157
  | `drag <startRef> <endRef>` | Drag and drop between two elements |
154
- | `snapshot` | Capture accessibility snapshot |
158
+ | `snapshot` | Capture accessibility snapshot. Supports `--filename=<path>` to save snapshot to file. |
155
159
  | `eval <expression> [ref]` | Evaluate JavaScript on the page or a target element. Use `--file=<path>` to read the expression from a file. |
156
160
  | `get <mode> <selector> [name]` | Extract data from a page element. Modes: `text`, `html`, `box`, `styles`, `property`, `attr`. `name` is required for `property` and `attr`. |
157
161
  | `scroll <direction> <pixels>` | Scroll the page. Direction: `up`, `down`, `left`, or `right`. |
@@ -190,7 +194,8 @@ The tables below mirror the commands surfaced by the global `browser4-cli help`
190
194
 
191
195
  | Command | Description |
192
196
  |---|---|
193
- | `screenshot [ref]` | Take a screenshot (optionally of a specific element) |
197
+ | `screenshot [ref]` | Take a screenshot (optionally of a specific element). Supports `--filename=<path>` and `--full-page` (full scrollable page). |
198
+ | `pdf` | Save the current page as a PDF. Supports `--filename=<path>`. |
194
199
 
195
200
  #### Tabs
196
201
 
@@ -211,7 +216,7 @@ Run `tab-list` first to find each tab's zero-based index. Pass that index to `ta
211
216
  | `state-load <path>` | Restore cookies and localStorage from a saved state file |
212
217
  | `cookie-list` | List all cookies (optionally filtered by `--domain` / `--path`) |
213
218
  | `cookie-get <name>` | Get a cookie by name |
214
- | `cookie-set <name> <value>` | Set a cookie (optional `--path`, `--domain`) |
219
+ | `cookie-set <name> <value>` | Set a cookie. Supports `--path`, `--domain`, `--expires`, `--httpOnly`, `--secure`, `--sameSite` (Strict / Lax / None). |
215
220
  | `cookie-delete <name>` | Delete a cookie by name |
216
221
  | `cookie-clear` | Clear all cookies for the current page |
217
222
  | `localstorage-list` | List all localStorage entries |
@@ -229,7 +234,7 @@ Run `tab-list` first to find each tab's zero-based index. Pass that index to `ta
229
234
 
230
235
  | Command | Description |
231
236
  |---|---|
232
- | `list` | List browser sessions |
237
+ | `list` | List browser sessions. Supports `--all` (list sessions across all workspaces). |
233
238
  | `close-all` | Close all browser sessions without stopping `Browser4.jar` / the Browser4 backend |
234
239
  | `kill-all` | Forcefully stop `Browser4.jar` / the Browser4 backend and kill Browser4 browser processes |
235
240
 
@@ -241,6 +246,7 @@ Use `close-all` for session cleanup when you want to keep the current Browser4 s
241
246
  |---|---|
242
247
  | `install` | Download the Browser4 runtime bundle. Supports `--tag=<version>` to pin a release and `--force` to reinstall even when already present. |
243
248
  | `upgrade` | Upgrade the Browser4 runtime bundle to the latest version or a specified `--tag` |
249
+ | `uninstall` | Remove globally installed browser4-cli (npm, cargo) and its runtime data. Supports `-y` / `--yes` (skip confirmation) and `--dry-run` (preview). |
244
250
  | `stop` | Kill the Browser4 backend after closing all sessions |
245
251
  | `status` | Check whether the Browser4 backend is reachable and healthy |
246
252
 
@@ -256,6 +262,17 @@ When a local Browser4 checkout is detected with the `browser4-bundle` module pre
256
262
  `install` and `upgrade` auto-build the runtime bundle from source (via Maven) instead
257
263
  of downloading.
258
264
 
265
+ `uninstall` attempts to remove browser4-cli from npm global packages and cargo
266
+ installs, and deletes the Browser4 runtime data and cache directories. It prompts
267
+ for confirmation unless `-y` / `--yes` is passed. Use `--dry-run` to preview what
268
+ would be removed without making changes. Does not require a running server.
269
+
270
+ ```shell
271
+ browser4-cli uninstall
272
+ browser4-cli uninstall -y
273
+ browser4-cli uninstall --dry-run
274
+ ```
275
+
259
276
  ### Advanced commands
260
277
 
261
278
  These commands are intentionally omitted from the global `browser4-cli help` overview.
@@ -265,7 +282,7 @@ Query `browser4-cli help <command>` for the exact syntax when you need them.
265
282
  |---|---|
266
283
  | `batch [command...]` | Execute multiple commands in one invocation. Only DOM operations are supported (Core, Navigation, Keyboard, Mouse, Export, Tabs categories). Commands like `open`, `close`, `list`, `agent run`, etc. are not allowed in batch mode. |
267
284
  | `console [min-level]` | List console messages |
268
- | `extract <instruction>` | Extract structured data from the current page |
285
+ | `extract <instruction>` | Extract structured data from the current page. Supports `--schema` (JSON Schema for typed output). |
269
286
  | `summarize [instruction]` | Summarize page content using AI |
270
287
  | `agent run <task>` | Run an autonomous agent task |
271
288
  | `agent status <id>` | Check the status of a running agent task |
@@ -433,6 +450,69 @@ browser4-cli swarm query --sql @query.sql --seed-file=./urls.txt --refresh
433
450
  - Prefix the `--sql` value with `@` to read from a file (e.g. `--sql @query.sql`).
434
451
  - All commands return a task ID; use `swarm status` / `swarm result` to track progress.
435
452
 
453
+ ## DOM Snapshot commands (`domsnapshot <subcommand>`)
454
+
455
+ The `domsnapshot` commands capture and query a **static DOM snapshot** of the
456
+ current page. Unlike the real-time accessibility `snapshot`, a DOM snapshot is
457
+ a full HTML capture that can be queried repeatedly with CSS selectors and X-SQL
458
+ without re-fetching the page.
459
+
460
+ Use the spaced `domsnapshot <subcommand>` form:
461
+
462
+ ```shell
463
+ browser4-cli domsnapshot
464
+ browser4-cli domsnapshot get text "#productTitle"
465
+ browser4-cli domsnapshot query --sql "SELECT dom_first_text(dom, '#title') AS title FROM dom(dom)"
466
+ browser4-cli domsnapshot export --file=page.html
467
+ ```
468
+
469
+ ### Command overview
470
+
471
+ | Command | Description |
472
+ |---|---|
473
+ | `domsnapshot` | Capture a static DOM snapshot of the current page |
474
+ | `domsnapshot get <field> [selector] [name]` | Extract elements from the static DOM snapshot |
475
+ | `domsnapshot query [url] --sql=<query>` | Run X-SQL against the DOM snapshot via the scrape API |
476
+ | `domsnapshot export [--file=<path>]` | Save full snapshot HTML content to a local file |
477
+
478
+ ### `domsnapshot get` fields
479
+
480
+ | Field | Description |
481
+ |---|---|
482
+ | `text` | Extract text content from the matched element |
483
+ | `html` | Extract inner HTML from the matched element |
484
+ | `attr` | Extract an attribute value (requires `name`, e.g. `attr "#link" "href"`) |
485
+
486
+ `selector` defaults to `:root` when omitted. `name` (attribute name) is
487
+ required for the `attr` field.
488
+
489
+ ### `domsnapshot query` with X-SQL
490
+
491
+ ```shell
492
+ # Inline X-SQL:
493
+ browser4-cli domsnapshot query "https://example.com" --sql "
494
+ SELECT
495
+ dom_base_uri(dom) AS url,
496
+ dom_first_text(dom, 'h1') AS title
497
+ FROM dom(dom);
498
+ "
499
+
500
+ # From a file:
501
+ browser4-cli domsnapshot query --sql @query.sql
502
+ ```
503
+
504
+ - `--sql` is **required**. Prefix with `@` to read from a file (e.g. `--sql @query.sql`).
505
+ - `url` is optional and defaults to the current session's page URL.
506
+ - These commands do not require an active swarm session.
507
+
508
+ ### Notes
509
+
510
+ - `domsnapshot` commands are not supported inside `batch` mode.
511
+ - Use `domsnapshot get` for quick element extraction; use `domsnapshot query`
512
+ with X-SQL for structured multi-element extraction.
513
+ - The DOM snapshot is cached in the backend until the next `domsnapshot`
514
+ capture or session navigation.
515
+
436
516
  ## Element References
437
517
 
438
518
  The `snapshot` command returns an accessibility tree where every interactive
@@ -517,12 +597,27 @@ browser4-cli open
517
597
  browser4-cli open --headed https://browser4.io
518
598
  browser4-cli open --headless https://browser4.io
519
599
 
600
+ # Open with a persistent profile
601
+ browser4-cli open --profile=~/.browser4/profiles/work
602
+
603
+ # Open with a temporary (single-use) profile
604
+ browser4-cli open --profile-mode=temporary https://browser4.io
605
+
606
+ # Open with interact-level control (FASTEST / FAST / DEFAULT)
607
+ browser4-cli open --interact-level=FAST https://browser4.io
608
+
520
609
  # Navigate to a page — auto-opens a session if none is active
521
610
  browser4-cli goto https://browser4.io
522
611
 
523
612
  # Inspect the page — note the eN labels on interactive nodes
524
613
  browser4-cli snapshot
525
614
 
615
+ # Capture a static DOM snapshot and extract data
616
+ browser4-cli domsnapshot
617
+ browser4-cli domsnapshot get text "#productTitle"
618
+ browser4-cli domsnapshot get attr "#productTitle" "href"
619
+ browser4-cli domsnapshot export --file=page.html
620
+
526
621
  # Interact using refs from the snapshot
527
622
  browser4-cli click e15
528
623
  browser4-cli type "Hello World" e15
@@ -537,6 +632,12 @@ browser4-cli keyup Shift
537
632
  # Take a screenshot and save it to disk
538
633
  browser4-cli screenshot
539
634
 
635
+ # Take a full-page screenshot
636
+ browser4-cli screenshot --full-page
637
+
638
+ # Save the current page as a PDF
639
+ browser4-cli pdf --filename=page.pdf
640
+
540
641
  # Inspect tab indices before switching tabs
541
642
  browser4-cli tab-list
542
643
  browser4-cli tab-select 1
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser4-cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "files": [
@@ -14,7 +14,7 @@
14
14
  "browser4": "./bin/browser4-cli.js"
15
15
  },
16
16
  "scripts": {
17
- "version:sync": "node scripts/sync-version.js",
17
+ "version:sync": "node ../bin/version.mjs cli sync",
18
18
  "version": "npm run version:sync && git add browser4-cli/Cargo.toml browser4-cli/Cargo.lock",
19
19
  "build:native": "npm run version:sync && cargo build --release --manifest-path browser4-cli/Cargo.toml && node scripts/copy-native.js",
20
20
  "build:linux": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-linux",
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Verifies that package.json and browser4-cli/Cargo.toml have the same version.
5
- * Used in CI to catch version drift.
6
- */
7
-
8
- import { readFileSync } from 'fs';
9
- import { dirname, join } from 'path';
10
- import { fileURLToPath } from 'url';
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const rootDir = join(__dirname, '..');
14
-
15
- // Read package.json version
16
- const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
17
- const packageVersion = packageJson.version;
18
-
19
- // Read Cargo.toml version
20
- const cargoToml = readFileSync(join(rootDir, 'browser4-cli/Cargo.toml'), 'utf-8');
21
- const cargoVersionMatch = cargoToml.match(/^version\s*=\s*"([^"]*)"/m);
22
-
23
- if (!cargoVersionMatch) {
24
- console.error('Could not find version in browser4-cli/Cargo.toml');
25
- process.exit(1);
26
- }
27
-
28
- const cargoVersion = cargoVersionMatch[1];
29
-
30
- const mismatches = [];
31
- if (packageVersion !== cargoVersion) {
32
- mismatches.push(` browser4-cli/Cargo.toml: ${cargoVersion}`);
33
- }
34
-
35
- if (mismatches.length > 0) {
36
- console.error('Version mismatch detected!');
37
- console.error(` package.json: ${packageVersion}`);
38
- for (const m of mismatches) console.error(m);
39
- console.error('');
40
- console.error("Run 'pnpm run version:sync' to fix this.");
41
- process.exit(1);
42
- }
43
-
44
- console.log(`Versions are in sync: ${packageVersion}`);
@@ -1,277 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Syncs the CLI version from cli/VERSION-CLI to all dependent files.
5
- *
6
- * cli/VERSION-CLI is the single source of truth for the CLI version. This
7
- * allows the backend Maven project and the CLI to be published separately with
8
- * different versions. This script:
9
- * - Reads the version from cli/VERSION-CLI
10
- * - Strips any "-SNAPSHOT" suffix (if present)
11
- * - Fetches the latest published version from npm and compares it
12
- * against the local version, warning when the bump is neither a
13
- * patch nor a minor increment.
14
- * - Writes the version to cli/package.json
15
- * - Writes the version to cli/browser4-cli/Cargo.toml
16
- * - Updates Cargo.lock to match
17
- *
18
- * Usage:
19
- * node cli/scripts/sync-version.js # sync all files
20
- * node cli/scripts/sync-version.js --check # exit 1 if out of sync (CI lint)
21
- */
22
-
23
- import {execSync} from "child_process";
24
- import {readFileSync, writeFileSync} from "fs";
25
- import {dirname, join, resolve} from "path";
26
- import {fileURLToPath} from "url";
27
-
28
- const __dirname = dirname(fileURLToPath(import.meta.url));
29
- const scriptsDir = __dirname; // cli/scripts
30
- const cliDir = resolve(scriptsDir, ".."); // cli
31
- const cargoDir = join(cliDir, "browser4-cli"); // cli/browser4-cli
32
-
33
- // ---------------------------------------------------------------------------
34
- // Helpers
35
- // ---------------------------------------------------------------------------
36
-
37
- /** Strip the "-SNAPSHOT" suffix if present. */
38
- function stripSnapshot(version) {
39
- if (version.endsWith("-SNAPSHOT")) {
40
- return version.slice(0, -"-SNAPSHOT".length);
41
- }
42
- return version;
43
- }
44
-
45
- /**
46
- * Parse a semver string into { major, minor, patch } integers.
47
- * Returns null for non-semver strings (e.g. snapshots, prereleases).
48
- */
49
- function parseSemver(version) {
50
- const m = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
51
- if (!m) return null;
52
- return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
53
- }
54
-
55
- /**
56
- * Fetch the latest published version of a package from the npm registry.
57
- * Returns the version string, or null if the package hasn't been published
58
- * yet or the registry is unreachable.
59
- */
60
- function getLatestPublishedVersion(packageName) {
61
- try {
62
- const raw = execSync(`npm view "${packageName}" version --json`, {
63
- stdio: ["ignore", "pipe", "ignore"],
64
- timeout: 10_000,
65
- });
66
- return JSON.parse(raw.toString().trim());
67
- } catch {
68
- // Package may not be published yet, or network is unavailable.
69
- return null;
70
- }
71
- }
72
-
73
- /**
74
- * Check whether `local` is an expected bump from `published`:
75
- * - same version (already published)
76
- * - next patch
77
- * - next minor
78
- *
79
- * Returns { ok: true } or { ok: false, reason: "..." }.
80
- */
81
- function checkVersionBump(published, local) {
82
- const pub = parseSemver(published);
83
- const loc = parseSemver(local);
84
-
85
- if (!pub || !loc) {
86
- // Can't compare — pre-release / snapshot versions are fine.
87
- return { ok: true };
88
- }
89
-
90
- // Same version — already published.
91
- if (loc.major === pub.major && loc.minor === pub.minor && loc.patch === pub.patch) {
92
- return { ok: true, note: `version ${local} is already published` };
93
- }
94
-
95
- // Next patch: X.Y.Z → X.Y.(Z+1)
96
- if (loc.major === pub.major && loc.minor === pub.minor && loc.patch === pub.patch + 1) {
97
- return { ok: true };
98
- }
99
-
100
- // Next minor: X.Y.Z → X.(Y+1).0
101
- if (loc.major === pub.major && loc.minor === pub.minor + 1 && loc.patch === 0) {
102
- return { ok: true };
103
- }
104
-
105
- // Local is behind published.
106
- if (
107
- loc.major < pub.major ||
108
- (loc.major === pub.major && loc.minor < pub.minor) ||
109
- (loc.major === pub.major && loc.minor === pub.minor && loc.patch < pub.patch)
110
- ) {
111
- return {
112
- ok: false,
113
- reason: `local version ${local} is behind the published version ${published}`,
114
- };
115
- }
116
-
117
- // Anything else — skipping versions, major bump, etc.
118
- return {
119
- ok: false,
120
- reason: `version bump from ${published} to ${local} is neither a patch nor a minor increment`,
121
- };
122
- }
123
-
124
- // ---------------------------------------------------------------------------
125
- // Main
126
- // ---------------------------------------------------------------------------
127
-
128
- const checkOnly = process.argv.includes("--check");
129
-
130
- // The npm package name (hard-coded — stable across versions)
131
- const pkgName = "browser4-cli";
132
-
133
- // 1. Read the CLI version from cli/VERSION-CLI (the single source of truth)
134
- const versionCliPath = join(cliDir, "VERSION-CLI");
135
- let versionRaw;
136
- try {
137
- versionRaw = readFileSync(versionCliPath, "utf-8").trim();
138
- } catch {
139
- console.error(`ERROR: Cannot read ${versionCliPath}`);
140
- process.exit(1);
141
- }
142
-
143
- if (!versionRaw) {
144
- console.error("ERROR: cli/VERSION-CLI is empty.");
145
- process.exit(1);
146
- }
147
-
148
- const version = stripSnapshot(versionRaw);
149
- if (checkOnly) console.log(`cli/VERSION-CLI: ${version}`);
150
-
151
- // 2. Compare against the latest published version on npm
152
- const publishedVersion = getLatestPublishedVersion(pkgName);
153
- if (publishedVersion) {
154
- console.log(`Latest published: ${pkgName}@${publishedVersion}`);
155
- const bump = checkVersionBump(publishedVersion, version);
156
- if (!bump.ok) {
157
- console.warn(`\x1b[33mWARNING: ${bump.reason}\x1b[0m`);
158
- } else if (bump.note) {
159
- console.log(` (${bump.note})`);
160
- }
161
- } else {
162
- console.log(`Latest published: (not found — package may not be published yet)`);
163
- }
164
-
165
- // ---------------------------------------------------------------------------
166
- // 3. Sync cli/package.json
167
- // ---------------------------------------------------------------------------
168
- const packageJsonPath = join(cliDir, "package.json");
169
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
170
-
171
- if (!packageJson.version) {
172
- console.error("ERROR: cli/package.json does not contain a version field.");
173
- process.exit(1);
174
- }
175
-
176
- if (packageJson.version !== version) {
177
- if (checkOnly) {
178
- console.error(`MISMATCH: ${packageJsonPath} version is "${packageJson.version}", expected "${version}"`);
179
- process.exitCode = 1;
180
- } else {
181
- const oldVersion = packageJson.version;
182
- packageJson.version = version;
183
- writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
184
- console.log(` Updated ${packageJsonPath}: ${oldVersion} -> ${version}`);
185
- }
186
- } else {
187
- if (!checkOnly) console.log(` ${packageJsonPath} already up to date`);
188
- }
189
-
190
- // ---------------------------------------------------------------------------
191
- // 4. Sync cli/browser4-cli/Cargo.toml
192
- // ---------------------------------------------------------------------------
193
- const cargoTomlPath = join(cargoDir, "Cargo.toml");
194
- let cargoToml = readFileSync(cargoTomlPath, "utf-8");
195
- const cargoVersionRegex = /^version\s*=\s*"[^"]*"/m;
196
- const cargoMatch = cargoToml.match(cargoVersionRegex);
197
-
198
- if (!cargoMatch) {
199
- console.error("ERROR: Could not find version field in Cargo.toml");
200
- process.exit(1);
201
- }
202
-
203
- const cargoVersion = cargoMatch[0].match(/"([^"]*)"/)[1];
204
- if (cargoVersion !== version) {
205
- if (checkOnly) {
206
- console.error(`MISMATCH: ${cargoTomlPath} version is "${cargoVersion}", expected "${version}"`);
207
- process.exitCode = 1;
208
- } else {
209
- cargoToml = cargoToml.replace(cargoVersionRegex, `version = "${version}"`);
210
- writeFileSync(cargoTomlPath, cargoToml);
211
- console.log(` Updated ${cargoTomlPath}: ${cargoVersion} -> ${version}`);
212
- }
213
- } else {
214
- if (!checkOnly) console.log(` ${cargoTomlPath} already up to date`);
215
- }
216
-
217
- // ---------------------------------------------------------------------------
218
- // 5. Update Cargo.lock (only in sync mode)
219
- // ---------------------------------------------------------------------------
220
- if (!checkOnly && cargoVersion !== version) {
221
- let lockUpdated = false;
222
- try {
223
- execSync("cargo update -p browser4-cli --offline", {
224
- cwd: cargoDir,
225
- stdio: "pipe",
226
- });
227
- console.log(" Updated Cargo.lock");
228
- lockUpdated = true;
229
- } catch {
230
- try {
231
- execSync("cargo update -p browser4-cli", {
232
- cwd: cargoDir,
233
- stdio: "pipe",
234
- });
235
- console.log(" Updated Cargo.lock");
236
- lockUpdated = true;
237
- } catch (e) {
238
- console.error(` Warning: Could not update Cargo.lock via cargo: ${e.message}`);
239
- }
240
- }
241
-
242
- // Fallback: edit Cargo.lock directly when cargo update fails (e.g. due to
243
- // lock file version mismatch between the installed cargo and the lock file).
244
- if (!lockUpdated) {
245
- const cargoLockPath = join(cargoDir, "Cargo.lock");
246
- try {
247
- let lockContent = readFileSync(cargoLockPath, "utf-8");
248
- // Match the [[package]] block for browser4-cli and replace its version.
249
- // Works for both v3 and v4 lock file formats.
250
- const lockPkgRegex = new RegExp(
251
- `(\\[\\[package\\]\\][\\r\\n]+\\s*name\\s*=\\s*"browser4-cli"[\\r\\n]+\\s*version\\s*=\\s*)"[^"]*"`,
252
- "m"
253
- );
254
- if (lockPkgRegex.test(lockContent)) {
255
- lockContent = lockContent.replace(lockPkgRegex, `$1"${version}"`);
256
- writeFileSync(cargoLockPath, lockContent);
257
- console.log(` Updated Cargo.lock directly: ${cargoVersion} -> ${version}`);
258
- lockUpdated = true;
259
- } else {
260
- console.error(" Warning: Could not find browser4-cli entry in Cargo.lock");
261
- }
262
- } catch (e2) {
263
- console.error(` Warning: Could not update Cargo.lock directly: ${e2.message}`);
264
- }
265
- }
266
- }
267
-
268
- // 6. Report
269
- if (checkOnly) {
270
- if (process.exitCode === 1) {
271
- console.error("\nVersion mismatch detected! Run 'node cli/scripts/sync-version.js' to fix.");
272
- } else {
273
- console.log(`\nAll versions in sync with cli/VERSION-CLI: ${version}`);
274
- }
275
- } else {
276
- console.log(`\nVersion sync complete: ${pkgName}@${version}`);
277
- }