browser4-cli 0.1.6 → 0.1.8

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
@@ -1,4 +1,4 @@
1
- # Browser4 CLI
1
+ # Browser4
2
2
 
3
3
  Make websites accessible for AI agents. Automate tasks online with ease.
4
4
 
@@ -53,16 +53,6 @@ pnpm link --global # Makes browser4-cli available globally
53
53
  - **Java 17+** - Required to run the Browser4 backend (`Browser4.jar`).
54
54
  - **Rust** - Only needed when building from source (see From Source above).
55
55
 
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
56
  ## Usage
67
57
 
68
58
  ```
@@ -143,7 +133,6 @@ The tables below mirror the commands surfaced by the global `browser4-cli help`
143
133
  | Command | Description |
144
134
  |---|---|
145
135
  | `screenshot [ref]` | Take a screenshot |
146
- | `pdf` | Save page as PDF |
147
136
 
148
137
  #### Tabs
149
138
 
@@ -156,6 +145,28 @@ The tables below mirror the commands surfaced by the global `browser4-cli help`
156
145
 
157
146
  Use `tab-list` first to find the zero-based tab index you want to select or close.
158
147
 
148
+ #### Browser storage
149
+
150
+ | Command | Description |
151
+ |---|---|
152
+ | `state-save <path>` | Save cookies and localStorage to a JSON file |
153
+ | `state-load <path>` | Restore cookies and localStorage from a saved state file |
154
+ | `cookie-list` | List all cookies (optionally filtered by `--domain` / `--path`) |
155
+ | `cookie-get <name>` | Get a cookie by name |
156
+ | `cookie-set <name> <value>` | Set a cookie (optional `--path`, `--domain`) |
157
+ | `cookie-delete <name>` | Delete a cookie by name |
158
+ | `cookie-clear` | Clear all cookies for the current page |
159
+ | `localstorage-list` | List all localStorage entries |
160
+ | `localstorage-get <key>` | Get a localStorage value by key |
161
+ | `localstorage-set <key> <value>` | Set a localStorage key-value pair |
162
+ | `localstorage-delete <key>` | Delete a localStorage key |
163
+ | `localstorage-clear` | Clear all localStorage entries |
164
+ | `sessionstorage-list` | List all sessionStorage entries |
165
+ | `sessionstorage-get <key>` | Get a sessionStorage value by key |
166
+ | `sessionstorage-set <key> <value>` | Set a sessionStorage key-value pair |
167
+ | `sessionstorage-delete <key>` | Delete a sessionStorage key |
168
+ | `sessionstorage-clear` | Clear all sessionStorage entries |
169
+
159
170
  #### Browser sessions
160
171
 
161
172
  | Command | Description |
@@ -166,6 +177,17 @@ Use `tab-list` first to find the zero-based tab index you want to select or clos
166
177
 
167
178
  Use `close-all` for session cleanup when you want to keep the current Browser4 service running. Use `kill-all` only when you explicitly want to stop the backend and clean up tracked Browser4 processes.
168
179
 
180
+ #### Server management
181
+
182
+ | Command | Description |
183
+ |---|---|
184
+ | `install` | Download the self-contained Browser4 runtime bundle (JAR + bundled JRE) from GitHub Releases |
185
+ | `upgrade` | Upgrade `browser4-cli` itself to the latest release (requires `cargo`) |
186
+ | `stop` | Kill the Browser4 backend after closing all sessions |
187
+ | `status` | Check whether the Browser4 backend is reachable and healthy |
188
+
189
+ When a local Browser4 checkout is detected with the `browser4-bundle` module present,
190
+ `install` auto-builds the runtime bundle from source instead of downloading.
169
191
 
170
192
  ### Advanced commands
171
193
 
@@ -174,18 +196,186 @@ Query `browser4-cli help <command>` for the exact syntax when you need them.
174
196
 
175
197
  | Command | Description |
176
198
  |---|---|
177
- | `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. |
199
+ | `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. |
178
200
  | `console [min-level]` | List console messages |
179
201
  | `extract <instruction>` | Extract structured data from the current page |
180
202
  | `summarize [instruction]` | Summarize page content using AI |
181
- | `agent-run <task>` | Run an autonomous agent task |
182
- | `agent-status <id>` | Check the status of a running agent task |
183
- | `agent-result <id>` | Get the result of a completed agent task |
184
- | `co-create` | Create a collective session with parallel browser contexts |
185
- | `co-submit [url]` | Submit URL(s) or tasks to the active collective session |
186
- | `co-scrape <url>` | Scrape data from a URL using CSS selectors |
187
- | `co-status <id>` | Check the status of a collective task |
188
- | `co-result <id>` | Get the result of a completed collective task |
203
+ | `agent run <task>` | Run an autonomous agent task |
204
+ | `agent status <id>` | Check the status of a running agent task |
205
+ | `agent result <id>` | Get the result of a completed agent task |
206
+ | `swarm create` | Create a swarm scrape session with parallel browser contexts |
207
+ | `swarm submit [url]` | Submit URL(s) or X-SQL payloads as scrape jobs |
208
+ | `swarm status <id>` | Check the status of a scrape job |
209
+ | `swarm result <id>` | Get the result of a completed scrape job |
210
+
211
+ ## Agent task workflow (`agent <subcommand>`)
212
+
213
+ The `agent-*` commands wrap the backend command agent's asynchronous task API.
214
+ They are useful when you want Browser4 to plan and execute a natural-language
215
+ task in the background instead of issuing one low-level browser action at a
216
+ time.
217
+
218
+ Like other advanced commands, they are intentionally omitted from the global
219
+ `browser4-cli help` overview. Query `browser4-cli help agent run` (or
220
+ `agent status` / `agent result`) when you need the exact syntax.
221
+
222
+ Use the spaced `agent <subcommand>` form:
223
+
224
+ ```shell
225
+ browser4-cli agent run "Open example.com and summarize the hero section"
226
+ browser4-cli agent status agent-task-1
227
+ browser4-cli agent result agent-task-1
228
+ ```
229
+
230
+ ### Command lifecycle
231
+
232
+ | Step | Command | What it does |
233
+ |---|---|---|
234
+ | 1 | `agent run <task>` | Submits an asynchronous natural-language task through `command_run` and prints the returned task ID |
235
+ | 2 | `agent status <id>` | Fetches the latest task status payload through `command_status` |
236
+ | 3 | `agent result <id>` | Fetches the completed task result payload through `command_result` |
237
+
238
+ ### Notes
239
+
240
+ - `agent run` is asynchronous: it returns immediately after the backend accepts
241
+ the task and prints a follow-up `agent status` command with the generated task
242
+ ID.
243
+ - `agent status` prints the backend status payload as-is. In practice this is a
244
+ JSON object that commonly includes fields such as `id`, `status`,
245
+ `statusCode`, `processState`, `message`, `agentState`, `agentHistory`, and
246
+ `commandResult`.
247
+ - `agent result` prints the backend result payload as-is. Depending on the task,
248
+ it may be plain text or structured JSON.
249
+ - These commands are task-ID based and do not require an active CLI browser
250
+ session slot. The global `-s=<name>` option is therefore usually not relevant
251
+ for `agent-*` follow-up calls.
252
+ - `agent` subcommands are not supported inside `batch` mode.
253
+ - `agent run` performs a short post-submit status probe so obvious missing-LLM
254
+ configuration failures can be surfaced immediately instead of leaving you with
255
+ a task ID that will never succeed.
256
+
257
+ ### Use cases
258
+
259
+ #### 1. Submit an autonomous agent task
260
+
261
+ ```shell
262
+ browser4-cli agent run "Open example.com and summarize the hero section"
263
+ ```
264
+
265
+ Typical output:
266
+
267
+ ```text
268
+ Task submitted: agent-task-1
269
+ Use 'browser4-cli agent status agent-task-1' to check progress.
270
+ ```
271
+
272
+ #### 2. Poll task progress
273
+
274
+ ```shell
275
+ browser4-cli agent status agent-task-1
276
+ ```
277
+
278
+ Example status payload:
279
+
280
+ ```json
281
+ {"id":"agent-task-1","status":"RUNNING"}
282
+ ```
283
+
284
+ On a real Browser4 backend the payload can be richer and may include lifecycle
285
+ details such as `processState`, agent history snapshots, or an embedded partial
286
+ `commandResult`.
287
+
288
+ #### 3. Read the final result
289
+
290
+ ```shell
291
+ browser4-cli agent result agent-task-1
292
+ ```
293
+
294
+ If the backend returns a structured `CommandResult`, expect fields such as
295
+ `summary`, `pageSummary`, `fields`, `links`, or `xsqlResultSet`.
296
+
297
+ ## Swarm scrape workflow (`swarm <subcommand>`)
298
+
299
+ The `swarm` subcommands support a swarm scrape workflow where one CLI session
300
+ coordinates multiple browser contexts in the Browser4 backend.
301
+
302
+ Use the spaced `swarm <subcommand>` form:
303
+
304
+ ```shell
305
+ browser4-cli swarm create
306
+ browser4-cli swarm submit https://example.com
307
+ ```
308
+
309
+ ### Command lifecycle
310
+
311
+ | Step | Command | What it does |
312
+ |---|---|---|
313
+ | 1 | `swarm create` | Opens a swarm scrape session and persists the returned session ID in the current CLI slot |
314
+ | 2 | `swarm submit [url]` | Submits one direct URL plus any URLs from `--seed-file` as scrape jobs through `ScrapeController.submit(payload)` |
315
+ | 3 | `swarm status <id>` | Calls `ScrapeController.getStatus(id)` and prints the returned scrape job status JSON |
316
+ | 4 | `swarm result <id>` | Calls `ScrapeController.getResult(id)` and prints the returned scrape job result JSON |
317
+
318
+ ### Notes
319
+
320
+ - `swarm create` accepts backend capability hints such as `--profile-mode`,
321
+ `--max-open-tabs`, `--max-browser-contexts`, and `--display-mode`.
322
+ - `swarm submit` accepts either a direct positional URL, `--seed-file`, or both.
323
+ Seed files are plain text files with one URL per line; blank lines and lines
324
+ starting with `#` are ignored.
325
+ - `swarm submit` maps CLI flags like `--deadline`, `--expires`, `--refresh`,
326
+ `--parse`, and `--store-content` into the raw submission payload sent to the
327
+ scrape REST API.
328
+ - `swarm status` and `swarm result` are read-only follow-up commands; keep the job ID
329
+ printed by `swarm submit`.
330
+
331
+ ### Use cases
332
+
333
+ #### 1. Create a supervised swarm scrape session for manual monitoring
334
+
335
+ ```shell
336
+ browser4-cli swarm create \
337
+ --profile-mode=TEMPORARY \
338
+ --max-open-tabs=12 \
339
+ --max-browser-contexts=3 \
340
+ --display-mode=HEADLESS
341
+ ```
342
+
343
+ Use this when you want multiple isolated browser contexts and you still want to
344
+ watch the run visually.
345
+
346
+ #### 2. Submit a seed crawl as scrape jobs
347
+
348
+ ```shell
349
+ browser4-cli swarm submit https://example.com/direct \
350
+ --seed-file=./swarm-seeds.txt \
351
+ --deadline=2026-03-30T00:00:00Z \
352
+ --expires=1d \
353
+ --refresh \
354
+ --parse \
355
+ --store-content
356
+ ```
357
+
358
+ Example `swarm-seeds.txt`:
359
+
360
+ ```text
361
+ # campaign landing pages
362
+ https://example.com/seed-1
363
+ https://example.com/seed-2
364
+ ```
365
+
366
+ This pattern is useful for warming caches, refreshing a URL list, or launching
367
+ parallel collection across a curated seed set.
368
+
369
+ #### 3. Poll and fetch the result
370
+
371
+ ```shell
372
+ browser4-cli swarm status scrape-task-4
373
+ browser4-cli swarm result scrape-task-4
374
+ ```
375
+
376
+ The status and result commands print the scrape job response payload as-is. In
377
+ the current backend, `getResult(id)` returns the same response envelope type as
378
+ `getStatus(id)`.
189
379
 
190
380
  ## Element References
191
381
 
@@ -228,16 +418,16 @@ gate for commands that require an active Browser4 session.
228
418
  | `open -s=<name>` | Reads/writes the named session state file | Opens, reuses, or refreshes the named session for that slot; subsequent `-s=<name>` commands use the same slot |
229
419
  | Command succeeds through `with_session()` | `sessionId` stays unchanged | The command uses the persisted session normally |
230
420
  | Command fails because the server reports a stale / expired session and `recover_stale = false` | `invalidate_session()` clears `sessionId`, `activeSelector`, and `lastMousePosition`, while keeping `baseUrl` | The command fails with `Saved session expired. Run "browser4-cli open" first.` |
231
- | `goto` is invoked but the saved session is missing or no longer `active` in the backend | `invalidate_session()` clears the saved `sessionId`, `activeSelector`, and `lastMousePosition` | The command fails with `No active session for "goto". Run "browser4-cli open" to create or refresh the session first.` |
421
+ | `goto` is invoked but the saved session is missing or no longer `active` in the backend | `invalidate_session()` clears any stale saved `sessionId`, then `create_session()` writes a fresh session before navigation continues | `goto` automatically refreshes the session and proceeds to the requested URL |
232
422
  | `close` with an active session | `clear_state()` removes only the current session state file after best-effort remote close | The selected default or named session is fully cleared |
233
423
  | `close` with no persisted `sessionId` | `clear_state()` best-effort removes the current session slot | Prints `No active session. Run "browser4-cli open" first.` and exits successfully as a no-op |
234
424
  | `close-all` / `kill-all` | `clear_all_state()` removes the default state file and all named session files | All persisted CLI session files are cleared |
235
425
 
236
426
  Notes:
237
427
 
238
- - `goto` reuses only the current backend-`active` session. It does not create a
239
- new session automatically; run `browser4-cli open` first if the saved session
240
- is missing or stale.
428
+ - `goto` first tries to reuse the current backend-`active` session. If the saved
429
+ session is missing, stale, or the backend had been stopped, it automatically
430
+ opens a fresh session for the current slot before navigating.
241
431
  - `open` first checks whether the saved session for the current slot is still
242
432
  backend-`active`. It reuses active sessions and refreshes stale ones by
243
433
  creating a new session for the same slot.
@@ -365,6 +555,41 @@ cargo test --test e2e -- --nocapture
365
555
  cargo test --test e2e -- --nocapture --scenario=test_e2e_batch_form_submission
366
556
  ```
367
557
 
558
+ ## Publishing the CLI package
559
+
560
+ For maintainers, the CLI package now uses an npm version guard before publish.
561
+
562
+ The GitHub release workflow publishes the npm package via npm trusted publishing
563
+ (GitHub Actions OIDC) instead of `NODE_AUTH_TOKEN`. This avoids CI failures caused
564
+ by npm one-time-password challenges (`EOTP`).
565
+
566
+ - Local release entrypoint: `npm run release`
567
+ - Direct guarded publish entrypoint: `npm run publish:if-needed`
568
+ - GitHub release workflow: re-checks npm immediately before the publish step
569
+
570
+ If the local version in `cli/package.json` already matches the version currently
571
+ published on npm, the publish step is skipped automatically.
572
+
573
+ Examples:
574
+
575
+ ```bash
576
+ # Check whether npm publish is needed
577
+ node scripts/check-npm-publish-needed.js --json
578
+
579
+ # Publish only when the local version differs from npm
580
+ npm run publish:if-needed
581
+
582
+ # Standard maintainer release command (also guarded)
583
+ npm run release
584
+ ```
585
+
586
+ For local testing, you can override the detected remote version:
587
+
588
+ ```bash
589
+ BROWSER4_CLI_NPM_REMOTE_VERSION=0.1.7 node scripts/check-npm-publish-needed.js --json
590
+ BROWSER4_CLI_NPM_REMOTE_VERSION=0.1.7 node scripts/publish-if-needed.js --dry-run
591
+ ```
592
+
368
593
  ## License
369
594
 
370
595
  Apache-2.0
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.6",
3
+ "version": "0.1.8",
4
4
  "description": "Browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "files": [
@@ -15,14 +15,15 @@
15
15
  },
16
16
  "scripts": {
17
17
  "version:sync": "node scripts/sync-version.js",
18
- "version": "npm run version:sync && git add browser4-cli/Cargo.toml",
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",
21
21
  "build:macos": "npm run version:sync && (cargo build --release --manifest-path browser4-cli/Cargo.toml --target aarch64-apple-darwin & cargo build --release --manifest-path browser4-cli/Cargo.toml --target x86_64-apple-darwin & wait) && cp cli/target/aarch64-apple-darwin/release/browser4 bin/browser4-darwin-arm64 && cp cli/target/x86_64-apple-darwin/release/browser4 bin/browser4-darwin-x64",
22
22
  "build:windows": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-windows",
23
23
  "build:all-platforms": "npm run version:sync && (npm run build:linux & npm run build:windows & wait)",
24
24
  "build:docker": "docker build -t browser4-builder -f docker/Dockerfile.build .",
25
- "release": "npm run version:sync && npm run build:all-platforms && npm publish",
25
+ "publish:if-needed": "node scripts/publish-if-needed.js",
26
+ "release": "npm run publish:if-needed",
26
27
  "postinstall": "node scripts/postinstall.js"
27
28
  },
28
29
  "keywords": [
@@ -45,8 +46,5 @@
45
46
  "url": "https://github.com/platonai/Browser4/issues"
46
47
  },
47
48
  "homepage": "https://browser4.io",
48
- "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
49
- "dependencies": {
50
- "browser4-cli": "^0.1.5"
51
- }
49
+ "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
52
50
  }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { appendFileSync } from 'fs';
4
+ import { getPublishDecision, logPublishDecision } from './npm-publish-check.js';
5
+
6
+ const args = new Set(process.argv.slice(2));
7
+ const shouldWriteGithubOutput = args.has('--github-output');
8
+ const shouldPrintJson = args.has('--json');
9
+ const shouldPrintJsonOnly = args.has('--json-only');
10
+ const shouldPrintShell = args.has('--shell');
11
+ const decision = getPublishDecision();
12
+
13
+ if (!shouldPrintJsonOnly && !shouldPrintShell) {
14
+ logPublishDecision('check', decision);
15
+ }
16
+
17
+ if (shouldPrintJson) {
18
+ console.log(JSON.stringify(decision));
19
+ }
20
+
21
+ if (shouldPrintJsonOnly) {
22
+ console.log(JSON.stringify(decision));
23
+ }
24
+
25
+ if (shouldPrintShell) {
26
+ console.log(`PACKAGE_NAME='${decision.packageName}'`);
27
+ console.log(`CLI_VERSION='${decision.cliVersion}'`);
28
+ console.log(`REMOTE_VERSION='${decision.remoteVersion}'`);
29
+ console.log(`LOOKUP_STATUS='${decision.lookupStatus}'`);
30
+ console.log(`SHOULD_PUBLISH='${decision.shouldPublish ? 'true' : 'false'}'`);
31
+ }
32
+
33
+ if (shouldWriteGithubOutput) {
34
+ const githubOutputPath = process.env.GITHUB_OUTPUT;
35
+ if (!githubOutputPath) {
36
+ console.error('GITHUB_OUTPUT is required when using --github-output');
37
+ process.exit(1);
38
+ }
39
+
40
+ appendFileSync(
41
+ githubOutputPath,
42
+ [
43
+ `package_name=${decision.packageName}`,
44
+ `cli_version=${decision.cliVersion}`,
45
+ `remote_version=${decision.remoteVersion}`,
46
+ `lookup_status=${decision.lookupStatus}`,
47
+ `should_publish=${decision.shouldPublish ? 'true' : 'false'}`,
48
+ '',
49
+ ].join('\n')
50
+ );
51
+ }
52
+
53
+ if (!decision.shouldPublish && !shouldPrintJsonOnly && !shouldPrintShell) {
54
+ console.log(`Skipping publish because ${decision.packageName}@${decision.cliVersion} already exists on npm.`);
55
+ }
56
+
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import { readFileSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ export const cliRootDir = join(__dirname, '..');
10
+ const packageJsonPath = join(cliRootDir, 'package.json');
11
+
12
+ /**
13
+ * Reads browser4-cli package metadata.
14
+ *
15
+ * @returns {{name: string, version: string}}
16
+ */
17
+ export function readPackageMetadata() {
18
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
19
+ return {
20
+ name: packageJson.name,
21
+ version: packageJson.version,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Queries npm for the published version of the package.
27
+ *
28
+ * @param {string} packageName Package name to inspect.
29
+ * @returns {{status: string, version: string}}
30
+ */
31
+ export function getRemoteVersion(packageName) {
32
+ const overriddenVersion = process.env.BROWSER4_CLI_NPM_REMOTE_VERSION;
33
+ if (overriddenVersion) {
34
+ console.log(`Using overridden npm version from BROWSER4_CLI_NPM_REMOTE_VERSION=${overriddenVersion}`);
35
+ return {
36
+ status: 'overridden',
37
+ version: overriddenVersion,
38
+ };
39
+ }
40
+
41
+ try {
42
+ const version = execSync(`npm view "${packageName}" version`, {
43
+ cwd: cliRootDir,
44
+ stdio: ['ignore', 'pipe', 'pipe'],
45
+ encoding: 'utf-8',
46
+ }).trim();
47
+
48
+ return {
49
+ status: 'success',
50
+ version,
51
+ };
52
+ } catch (error) {
53
+ const stderr = error.stderr?.toString().trim();
54
+ const stdout = error.stdout?.toString().trim();
55
+ const message = stderr || stdout || error.message;
56
+
57
+ console.warn(`Warning: unable to query npm version for ${packageName}: ${message}`);
58
+ return {
59
+ status: 'failed',
60
+ version: 'unknown',
61
+ };
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Computes whether npm publish should proceed.
67
+ *
68
+ * @returns {{packageName: string, cliVersion: string, remoteVersion: string, lookupStatus: string, shouldPublish: boolean}}
69
+ */
70
+ export function getPublishDecision() {
71
+ const metadata = readPackageMetadata();
72
+ const remoteInfo = getRemoteVersion(metadata.name);
73
+
74
+ return {
75
+ packageName: metadata.name,
76
+ cliVersion: metadata.version,
77
+ remoteVersion: remoteInfo.version,
78
+ lookupStatus: remoteInfo.status,
79
+ shouldPublish: remoteInfo.version === 'unknown' || metadata.version !== remoteInfo.version,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Logs a human-readable publish decision.
85
+ *
86
+ * @param {string} phase Current phase label.
87
+ * @param {{packageName: string, cliVersion: string, remoteVersion: string, lookupStatus: string, shouldPublish: boolean}} decision Publish decision.
88
+ */
89
+ export function logPublishDecision(phase, decision) {
90
+ console.log(
91
+ `[${phase}] package=${decision.packageName} local=${decision.cliVersion} remote=${decision.remoteVersion} status=${decision.lookupStatus} shouldPublish=${decision.shouldPublish}`
92
+ );
93
+ }
94
+
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Publishes browser4-cli only when the local package version differs from npm.
5
+ *
6
+ * Usage:
7
+ * node scripts/publish-if-needed.js
8
+ * node scripts/publish-if-needed.js --dry-run
9
+ *
10
+ * Optional env for testing:
11
+ * BROWSER4_CLI_NPM_REMOTE_VERSION=<version>
12
+ */
13
+
14
+ import { execSync } from 'child_process';
15
+ import { cliRootDir, getPublishDecision, logPublishDecision } from './npm-publish-check.js';
16
+
17
+ const args = new Set(process.argv.slice(2));
18
+ const isDryRun = args.has('--dry-run');
19
+
20
+ function runCommand(command) {
21
+ console.log(`> ${command}`);
22
+ execSync(command, {
23
+ cwd: cliRootDir,
24
+ stdio: 'inherit',
25
+ });
26
+ }
27
+
28
+ function main() {
29
+ const initialDecision = getPublishDecision();
30
+ logPublishDecision('pre-check', initialDecision);
31
+
32
+ if (!initialDecision.shouldPublish) {
33
+ console.log(`Skipping publish because ${initialDecision.packageName}@${initialDecision.cliVersion} already exists on npm.`);
34
+ return;
35
+ }
36
+
37
+ if (isDryRun) {
38
+ console.log('Dry run enabled; version differs, so publish would proceed.');
39
+ return;
40
+ }
41
+
42
+ runCommand('npm run version:sync');
43
+
44
+ const postSyncDecision = getPublishDecision();
45
+ logPublishDecision('post-sync-check', postSyncDecision);
46
+
47
+ if (!postSyncDecision.shouldPublish) {
48
+ console.log(`Skipping publish after sync because ${postSyncDecision.packageName}@${postSyncDecision.cliVersion} already exists on npm.`);
49
+ return;
50
+ }
51
+
52
+ runCommand('npm run build:all-platforms');
53
+
54
+ const prePublishDecision = getPublishDecision();
55
+ logPublishDecision('pre-publish-check', prePublishDecision);
56
+
57
+ if (!prePublishDecision.shouldPublish) {
58
+ console.log(`Skipping publish before npm publish because ${prePublishDecision.packageName}@${prePublishDecision.cliVersion} already exists on npm.`);
59
+ return;
60
+ }
61
+
62
+ runCommand('npm publish');
63
+ }
64
+
65
+ main();
66
+
@@ -5,10 +5,10 @@
5
5
  * Run this script before building or releasing.
6
6
  */
7
7
 
8
- import { execSync } from "child_process";
9
- import { readFileSync, writeFileSync } from "fs";
10
- import { dirname, join } from "path";
11
- import { fileURLToPath } from "url";
8
+ import {execSync} from "child_process";
9
+ import {readFileSync, writeFileSync} from "fs";
10
+ import {dirname, join} from "path";
11
+ import {fileURLToPath} from "url";
12
12
 
13
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
14
  const rootDir = join(__dirname, "..");
@@ -47,7 +47,7 @@ if (cargoVersionRegex.test(cargoToml)) {
47
47
  // Update Cargo.lock to match Cargo.toml
48
48
  if (cargoTomlUpdated) {
49
49
  try {
50
- execSync("cargo update -p browser4 --offline", {
50
+ execSync("cargo update -p browser4-cli --offline", {
51
51
  cwd: cliDir,
52
52
  stdio: "pipe",
53
53
  });
@@ -55,7 +55,7 @@ if (cargoTomlUpdated) {
55
55
  } catch {
56
56
  // --offline may fail if package not in cache, try without it
57
57
  try {
58
- execSync("cargo update -p browser4", {
58
+ execSync("cargo update -p browser4-cli", {
59
59
  cwd: cliDir,
60
60
  stdio: "pipe",
61
61
  });