@phnx-labs/agents-cli 1.22.117 → 1.22.119
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/CHANGELOG.md +301 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/accounts.js +7 -4
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +53 -1
- package/dist/commands/projects.js +92 -7
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup.js +9 -9
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/add.d.ts +3 -1
- package/dist/lib/accounts/add.js +24 -13
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +7 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-account-token.d.ts +7 -1
- package/dist/lib/claude-account-token.js +26 -4
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +10 -13
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/github/pr-mergeable.d.ts +4 -2
- package/dist/lib/github/pr-mergeable.js +5 -3
- package/dist/lib/github/project-prs.d.ts +58 -3
- package/dist/lib/github/project-prs.js +205 -7
- package/dist/lib/harness/adapters/cursor.js +14 -4
- package/dist/lib/harness-auth-capabilities.d.ts +4 -3
- package/dist/lib/harness-auth-capabilities.js +1 -2
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +32 -12
- package/dist/lib/installations/store.d.ts +9 -0
- package/dist/lib/installations/store.js +24 -0
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +15 -3
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/projects.d.ts +16 -0
- package/dist/lib/projects.js +31 -0
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/active.d.ts +17 -6
- package/dist/lib/session/active.js +105 -49
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/pid-registry.d.ts +6 -3
- package/dist/lib/session/pid-registry.js +18 -8
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +7 -3
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +5 -10
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +16 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
|
@@ -1,704 +0,0 @@
|
|
|
1
|
-
import { spawn, execFileSync } from 'child_process';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
import { getProfileRuntimeDir } from './profiles.js';
|
|
6
|
-
import { discoverBrowserWsUrl, registerPipeTransport } from './cdp.js';
|
|
7
|
-
import { readAndResolveBundleEnv, bundleExists } from '../secrets-client.js';
|
|
8
|
-
import { writeProfileRuntime, readProfileRuntime } from './runtime-state.js';
|
|
9
|
-
// Windows install roots. Resolve from the environment (fall back to the usual
|
|
10
|
-
// defaults) so per-user installs under %LOCALAPPDATA% and 64-bit Program Files
|
|
11
|
-
// are found, not just the hardcoded x86 path. Only the `win32` entries below use
|
|
12
|
-
// these; on other platforms they compute unused placeholder strings.
|
|
13
|
-
const WIN_PROGRAMFILES = process.env.ProgramFiles || 'C:\\Program Files';
|
|
14
|
-
const WIN_PROGRAMFILES_X86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
15
|
-
const WIN_LOCALAPPDATA = process.env.LOCALAPPDATA || `${os.homedir()}\\AppData\\Local`;
|
|
16
|
-
const BROWSER_PATHS = {
|
|
17
|
-
darwin: {
|
|
18
|
-
chrome: [
|
|
19
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
20
|
-
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
21
|
-
],
|
|
22
|
-
comet: ['/Applications/Comet.app/Contents/MacOS/Comet'],
|
|
23
|
-
chromium: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
|
|
24
|
-
brave: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
|
|
25
|
-
edge: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
|
|
26
|
-
arc: ['/Applications/Arc.app/Contents/MacOS/Arc'],
|
|
27
|
-
firefox: [
|
|
28
|
-
'/Applications/Firefox.app/Contents/MacOS/firefox',
|
|
29
|
-
'/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox',
|
|
30
|
-
],
|
|
31
|
-
custom: [],
|
|
32
|
-
},
|
|
33
|
-
linux: {
|
|
34
|
-
chrome: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'],
|
|
35
|
-
comet: [],
|
|
36
|
-
chromium: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
|
|
37
|
-
brave: ['/usr/bin/brave-browser', '/usr/bin/brave'],
|
|
38
|
-
edge: ['/usr/bin/microsoft-edge'],
|
|
39
|
-
// Arc has no Linux build (macOS + Windows only).
|
|
40
|
-
arc: [],
|
|
41
|
-
// `/usr/bin/firefox` on Ubuntu is the snap wrapper script: it execs the snap
|
|
42
|
-
// in place, so the pid survives and `--remote-debugging-port` works through
|
|
43
|
-
// it. The driver reads the real pid from the BiDi handshake anyway.
|
|
44
|
-
firefox: ['/usr/bin/firefox', '/snap/bin/firefox', '/usr/lib/firefox/firefox', '/opt/firefox/firefox'],
|
|
45
|
-
custom: [],
|
|
46
|
-
},
|
|
47
|
-
win32: {
|
|
48
|
-
chrome: [
|
|
49
|
-
`${WIN_PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
50
|
-
`${WIN_PROGRAMFILES_X86}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
51
|
-
`${WIN_LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
52
|
-
],
|
|
53
|
-
comet: [
|
|
54
|
-
`${WIN_PROGRAMFILES}\\Perplexity\\Comet\\Application\\comet.exe`,
|
|
55
|
-
`${WIN_PROGRAMFILES_X86}\\Perplexity\\Comet\\Application\\comet.exe`,
|
|
56
|
-
`${WIN_LOCALAPPDATA}\\Perplexity\\Comet\\Application\\comet.exe`,
|
|
57
|
-
],
|
|
58
|
-
chromium: [
|
|
59
|
-
`${WIN_LOCALAPPDATA}\\Chromium\\Application\\chrome.exe`,
|
|
60
|
-
],
|
|
61
|
-
brave: [
|
|
62
|
-
`${WIN_PROGRAMFILES}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
63
|
-
`${WIN_PROGRAMFILES_X86}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
64
|
-
`${WIN_LOCALAPPDATA}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
65
|
-
],
|
|
66
|
-
edge: [
|
|
67
|
-
`${WIN_PROGRAMFILES}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
68
|
-
`${WIN_PROGRAMFILES_X86}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
69
|
-
`${WIN_LOCALAPPDATA}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
70
|
-
],
|
|
71
|
-
// Arc ships a Windows build, but its install path is not yet verified here;
|
|
72
|
-
// leave unlisted (undetected) rather than guess a path that false-positives.
|
|
73
|
-
arc: [],
|
|
74
|
-
firefox: [
|
|
75
|
-
`${WIN_PROGRAMFILES}\\Mozilla Firefox\\firefox.exe`,
|
|
76
|
-
`${WIN_PROGRAMFILES_X86}\\Mozilla Firefox\\firefox.exe`,
|
|
77
|
-
`${WIN_LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
|
|
78
|
-
],
|
|
79
|
-
custom: [],
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* On Debian/Ubuntu the canonical launchers under `/usr/bin`
|
|
84
|
-
* (`brave-browser`, `google-chrome`, `chromium`) are not the browser ELF —
|
|
85
|
-
* they're `#!/bin/bash` wrapper scripts (the upstream Chromium wrapper) that,
|
|
86
|
-
* as their final step, run the real binary as a NON-exec child:
|
|
87
|
-
*
|
|
88
|
-
* exec < /dev/null
|
|
89
|
-
* exec > >(exec cat)
|
|
90
|
-
* exec 2> >(exec cat >&2)
|
|
91
|
-
* "$HERE/brave" "$@" || true
|
|
92
|
-
*
|
|
93
|
-
* That breaks `launchBrowser`'s `--remote-debugging-pipe` transport two ways:
|
|
94
|
-
* the std-fd sanitization (and the extra `cat` process-substitution children)
|
|
95
|
-
* disturbs the inherited CDP pipe on fd 3/4, and the pid we record is the
|
|
96
|
-
* wrapper's, not the browser's. The symptom is `read ECONNRESET` /
|
|
97
|
-
* `CDP connection closed` right after spawn (issue #229).
|
|
98
|
-
*
|
|
99
|
-
* Follow the wrapper to the ELF it execs. The wrapper sets
|
|
100
|
-
* `HERE="dirname(readlink -f "$0")"` and invokes `"$HERE/<name>"`, so we
|
|
101
|
-
* resolve the script path, scan for that invocation line, and join the two.
|
|
102
|
-
* Returns the original path untouched when it's already an ELF, when it's not
|
|
103
|
-
* a resolvable wrapper, or on any non-Linux platform.
|
|
104
|
-
*/
|
|
105
|
-
function readsAsShebangScript(binaryPath) {
|
|
106
|
-
let fd;
|
|
107
|
-
try {
|
|
108
|
-
fd = fs.openSync(binaryPath, 'r');
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const head = Buffer.alloc(2);
|
|
115
|
-
fs.readSync(fd, head, 0, 2, 0);
|
|
116
|
-
// ELF binaries start with 0x7f 'E'; shebang scripts with '#!'.
|
|
117
|
-
return head[0] === 0x23 && head[1] === 0x21;
|
|
118
|
-
}
|
|
119
|
-
finally {
|
|
120
|
-
fs.closeSync(fd);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* True when `binaryPath` is a shebang script rather than a native browser
|
|
125
|
-
* executable. The Linux distro launchers (`/usr/bin/brave-browser`, …) are such
|
|
126
|
-
* scripts; `launchBrowser` can't drive one over `--remote-debugging-pipe` (see
|
|
127
|
-
* resolveBrowserBinary). `profiles doctor` uses this to flag a profile whose
|
|
128
|
-
* binary resolves to a wrapper we couldn't unwrap. Shebang scripts are a
|
|
129
|
-
* Linux/Unix concept — returns false on Windows/macOS app bundles.
|
|
130
|
-
*/
|
|
131
|
-
export function isLauncherScript(binaryPath) {
|
|
132
|
-
if (os.platform() === 'win32')
|
|
133
|
-
return false;
|
|
134
|
-
return readsAsShebangScript(binaryPath);
|
|
135
|
-
}
|
|
136
|
-
export function resolveBrowserBinary(binaryPath) {
|
|
137
|
-
if (os.platform() !== 'linux')
|
|
138
|
-
return binaryPath;
|
|
139
|
-
// Only shebang scripts need unwrapping; a real ELF passes straight through.
|
|
140
|
-
if (!readsAsShebangScript(binaryPath))
|
|
141
|
-
return binaryPath;
|
|
142
|
-
let script;
|
|
143
|
-
let realScriptPath;
|
|
144
|
-
try {
|
|
145
|
-
realScriptPath = fs.realpathSync(binaryPath);
|
|
146
|
-
script = fs.readFileSync(realScriptPath, 'utf8');
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return binaryPath;
|
|
150
|
-
}
|
|
151
|
-
// Match the Chromium wrapper's launch line: `"$HERE/<name>" "$@"`, optionally
|
|
152
|
-
// prefixed with `exec -a "$0"`. The captured name is the real ELF, sitting in
|
|
153
|
-
// the same directory as the resolved wrapper.
|
|
154
|
-
const match = script.match(/"\$HERE\/([A-Za-z0-9._-]+)"\s+"\$@"/);
|
|
155
|
-
if (!match)
|
|
156
|
-
return binaryPath;
|
|
157
|
-
const realBinary = path.join(path.dirname(realScriptPath), match[1]);
|
|
158
|
-
return fs.existsSync(realBinary) ? realBinary : binaryPath;
|
|
159
|
-
}
|
|
160
|
-
export function findBrowserPath(browserType, customBinary) {
|
|
161
|
-
if (customBinary) {
|
|
162
|
-
if (!fs.existsSync(customBinary)) {
|
|
163
|
-
throw new Error(`Custom binary not found: ${customBinary}`);
|
|
164
|
-
}
|
|
165
|
-
return resolveBrowserBinary(customBinary);
|
|
166
|
-
}
|
|
167
|
-
if (browserType === 'custom') {
|
|
168
|
-
throw new Error('browser: custom requires a binary path in the profile');
|
|
169
|
-
}
|
|
170
|
-
const platform = os.platform();
|
|
171
|
-
const platformPaths = BROWSER_PATHS[platform];
|
|
172
|
-
if (!platformPaths) {
|
|
173
|
-
throw new Error(`Unsupported platform: ${platform}`);
|
|
174
|
-
}
|
|
175
|
-
const candidates = platformPaths[browserType] || [];
|
|
176
|
-
for (const p of candidates) {
|
|
177
|
-
if (fs.existsSync(p)) {
|
|
178
|
-
return resolveBrowserBinary(p);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (browserType === 'comet' && platform === 'linux') {
|
|
182
|
-
throw new Error('Browser "comet" is not available on Linux (Comet ships macOS and Windows builds only). Use chrome, chromium, brave, or edge on this platform.');
|
|
183
|
-
}
|
|
184
|
-
if (browserType === 'arc' && platform === 'linux') {
|
|
185
|
-
throw new Error('Browser "arc" is not available on Linux (Arc ships macOS and Windows builds only). Use chrome, chromium, brave, or edge on this platform.');
|
|
186
|
-
}
|
|
187
|
-
throw new Error(`Browser "${browserType}" not found. Install it first.`);
|
|
188
|
-
}
|
|
189
|
-
// Per-platform Chromium-family priority list for "no --profile" auto-pick.
|
|
190
|
-
// Order is: most-likely-installed-and-stable first. Firefox is intentionally
|
|
191
|
-
// absent: it has its own WebDriver BiDi driver (drivers/firefox.ts) and its
|
|
192
|
-
// profiles come from profiles.ini discovery, never from this binary pick.
|
|
193
|
-
// Safari has no driver at all.
|
|
194
|
-
const DEFAULT_BROWSER_PRIORITY = {
|
|
195
|
-
// macOS: Chrome leads (>70% of dev machines), then the rest of the family.
|
|
196
|
-
// Arc is last: it's in maintenance mode and needs a blank tab to drive, so it
|
|
197
|
-
// shouldn't win auto-pick over a mainstream browser.
|
|
198
|
-
darwin: ['chrome', 'brave', 'edge', 'chromium', 'comet', 'arc'],
|
|
199
|
-
// Linux: Chrome/Chromium first (apt/snap), then Brave/Edge if present.
|
|
200
|
-
linux: ['chrome', 'chromium', 'brave', 'edge'],
|
|
201
|
-
// Windows: Edge is preinstalled on every supported build, so it's the
|
|
202
|
-
// reliable always-there default.
|
|
203
|
-
win32: ['edge', 'chrome', 'brave', 'comet'],
|
|
204
|
-
};
|
|
205
|
-
/**
|
|
206
|
-
* Walk the per-platform priority list and return the first browser that's
|
|
207
|
-
* actually installed on disk. Returns null if none of them are present.
|
|
208
|
-
*
|
|
209
|
-
* This is the auto-pick the `agents browser start` command uses when the user
|
|
210
|
-
* doesn't pass `--profile`. The intent matches "use whatever's preinstalled,"
|
|
211
|
-
* but constrained to Chromium-family binaries so CDP works without a new
|
|
212
|
-
* driver layer.
|
|
213
|
-
*/
|
|
214
|
-
export function findFirstInstalledBrowser(platform = os.platform()) {
|
|
215
|
-
const priority = DEFAULT_BROWSER_PRIORITY[platform];
|
|
216
|
-
if (!priority)
|
|
217
|
-
return null;
|
|
218
|
-
const platformPaths = BROWSER_PATHS[platform];
|
|
219
|
-
if (!platformPaths)
|
|
220
|
-
return null;
|
|
221
|
-
for (const browserType of priority) {
|
|
222
|
-
const candidates = platformPaths[browserType] || [];
|
|
223
|
-
for (const p of candidates) {
|
|
224
|
-
if (fs.existsSync(p)) {
|
|
225
|
-
return { browserType, binary: resolveBrowserBinary(p) };
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* List every installed Chromium-family browser on this machine, in the platform
|
|
233
|
-
* priority order. Used by `agents setup browser` to offer a pick when more than
|
|
234
|
-
* one is present. Returns [] if none are installed.
|
|
235
|
-
*/
|
|
236
|
-
export function listInstalledBrowsers(platform = os.platform()) {
|
|
237
|
-
const priority = DEFAULT_BROWSER_PRIORITY[platform];
|
|
238
|
-
const platformPaths = BROWSER_PATHS[platform];
|
|
239
|
-
if (!priority || !platformPaths)
|
|
240
|
-
return [];
|
|
241
|
-
const found = [];
|
|
242
|
-
for (const browserType of priority) {
|
|
243
|
-
const candidates = platformPaths[browserType] || [];
|
|
244
|
-
for (const p of candidates) {
|
|
245
|
-
if (fs.existsSync(p)) {
|
|
246
|
-
found.push({ browserType, binary: resolveBrowserBinary(p) });
|
|
247
|
-
break; // one install path per browser type is enough
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return found;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* The process that holds a Chromium user-data dir, read from the `SingletonLock`
|
|
255
|
-
* symlink Chromium keeps inside the dir on macOS and Linux (`<host>-<pid>`,
|
|
256
|
-
* PHNX-4042). A live pid means a browser already owns that store: a launch on it
|
|
257
|
-
* would only hand its arguments to the running instance and the requested debug
|
|
258
|
-
* port would never bind. Windows keeps no lock file, so this reports null there
|
|
259
|
-
* and `launchBrowser` catches the hand-off by the child's early exit instead.
|
|
260
|
-
*/
|
|
261
|
-
export function storeOccupant(userDataDir) {
|
|
262
|
-
let target;
|
|
263
|
-
try {
|
|
264
|
-
target = fs.readlinkSync(path.join(userDataDir, 'SingletonLock'));
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
const pid = Number(/-(\d+)$/.exec(target)?.[1]);
|
|
270
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
271
|
-
return null;
|
|
272
|
-
try {
|
|
273
|
-
process.kill(pid, 0);
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
return error.code === 'EPERM' ? { pid } : null;
|
|
277
|
-
}
|
|
278
|
-
return { pid };
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* The relaunch that makes a browser holding `userDataDir` attachable: the
|
|
282
|
-
* ownership guard reads `--user-data-dir` off the running process, so the
|
|
283
|
-
* owner's normal launch (no flag) can never be verified.
|
|
284
|
-
*/
|
|
285
|
-
export function storeRelaunchCommand(browserType, port, userDataDir, profileDirectory) {
|
|
286
|
-
const app = browserType === 'comet' ? 'Comet' : browserType;
|
|
287
|
-
const profileFlag = profileDirectory ? ` --profile-directory=${profileDirectory}` : '';
|
|
288
|
-
return `open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${userDataDir}${profileFlag}`;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Resolve a browser-profile secrets bundle into an env map for the child, or an
|
|
292
|
-
* EMPTY map when the bundle is absent, locked, or otherwise unreadable — never a
|
|
293
|
-
* throw and never a prompt. Injecting profile secrets on launch is a BACKGROUND
|
|
294
|
-
* read (the agent is spawning the browser, not a human at a Touch ID sheet), so
|
|
295
|
-
* the read is `agentOnly` (SEC-13: never pop biometry on its own): a `never`/no-ACL
|
|
296
|
-
* or broker-held bundle resolves silently; a locked `hold`/`always` bundle throws,
|
|
297
|
-
* and we swallow it so the launch proceeds without those secrets (an agent that
|
|
298
|
-
* needs them can `agents secrets unlock <bundle>`). Exported so the no-prompt
|
|
299
|
-
* behavior is testable on the real keychain path without spawning a browser.
|
|
300
|
-
*/
|
|
301
|
-
export async function resolveProfileSecretsEnv(secrets) {
|
|
302
|
-
if (!secrets || !(await bundleExists(secrets)))
|
|
303
|
-
return {};
|
|
304
|
-
try {
|
|
305
|
-
const { env } = await readAndResolveBundleEnv(secrets, { caller: 'browser profile', agentOnly: true });
|
|
306
|
-
return env;
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
// Bundle locked or failed to resolve — launch without secrets (no prompt).
|
|
310
|
-
return {};
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
export async function launchBrowser(profileName, browserType, port, options = {}, secrets, customBinary,
|
|
314
|
-
// `electron: true` distinguishes Notion / VS Code-style apps from
|
|
315
|
-
// regular Chrome — purely informational, stored in meta.json so the
|
|
316
|
-
// orphan reaper and `agents browser status` can label processes.
|
|
317
|
-
isElectron = false) {
|
|
318
|
-
const browserPath = findBrowserPath(browserType, customBinary);
|
|
319
|
-
// A profile discovered from the browser's OWN store (PHNX-4042) launches on
|
|
320
|
-
// that store — the owner's real user-data dir and profile directory — so the
|
|
321
|
-
// window agents open is the window the owner uses. Its Preferences are the
|
|
322
|
-
// owner's and are never rewritten. Anything else runs under a managed dir.
|
|
323
|
-
const ownerStore = options.userDataDir !== undefined;
|
|
324
|
-
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
325
|
-
const userDataDir = options.userDataDir ?? path.join(runtimeDir, 'chrome-data');
|
|
326
|
-
if (!ownerStore) {
|
|
327
|
-
fs.mkdirSync(userDataDir, { recursive: true });
|
|
328
|
-
// Pre-launch Preferences pass: first-launch profile-name stamp, plus (for
|
|
329
|
-
// real browsers, not Electron apps) the session-cookie persistence pin.
|
|
330
|
-
// Electron apps manage their own storage and don't read Chromium's
|
|
331
|
-
// `session.*` prefs, so they get the name stamp only.
|
|
332
|
-
ensureProfilePreferences(userDataDir, profileName, !isElectron);
|
|
333
|
-
}
|
|
334
|
-
// The owner's store is attached over a TCP port rather than the daemon's
|
|
335
|
-
// private pipe: the instance outlives any one daemon and must stay reachable
|
|
336
|
-
// (and verifiable by the ownership guard) after a daemon restart.
|
|
337
|
-
const transport = ownerStore ? 'port' : 'pipe';
|
|
338
|
-
// Chromium on macOS coordinates instances via the SingletonLock file
|
|
339
|
-
// *inside* each user-data-dir. Direct binary spawn with a fresh
|
|
340
|
-
// --user-data-dir creates a fully independent process — the user's
|
|
341
|
-
// normal browser (running under their default user-data-dir) and our
|
|
342
|
-
// sandboxed one coexist as two real processes.
|
|
343
|
-
//
|
|
344
|
-
// These are TWO dock tiles, not one. Measured on macOS 26 (zion, 2026-09-04):
|
|
345
|
-
// a second Comet spawned this way registers its OWN LaunchServices ASN, so
|
|
346
|
-
// `lsappinfo list` reports two `ai.perplexity.comet` entries and the Dock shows
|
|
347
|
-
// two Comet icons — one of them the logged-out sandbox. The Dock does NOT
|
|
348
|
-
// collapse same-bundle processes into one tile. Spawning a rival window is
|
|
349
|
-
// exactly the failure PHNX-3967 set out to end, which is why an attach-only
|
|
350
|
-
// profile never reaches this launcher (see connectLocal / isAttachOnlyProfile).
|
|
351
|
-
const viewport = options.viewport ?? { width: 1512, height: 982 };
|
|
352
|
-
const args = [
|
|
353
|
-
transport === 'pipe' ? '--remote-debugging-pipe' : `--remote-debugging-port=${port}`,
|
|
354
|
-
`--user-data-dir=${userDataDir}`,
|
|
355
|
-
...(options.profileDirectory ? [`--profile-directory=${options.profileDirectory}`] : []),
|
|
356
|
-
'--disable-background-timer-throttling',
|
|
357
|
-
'--disable-backgrounding-occluded-windows',
|
|
358
|
-
'--disable-renderer-backgrounding',
|
|
359
|
-
// First-run + default-browser modals block automation: when targetFilter
|
|
360
|
-
// matches by URL, the onboarding page (`chrome://welcome/`) isn't a
|
|
361
|
-
// match and start fails with "no page target". Suppress them.
|
|
362
|
-
'--no-first-run',
|
|
363
|
-
'--no-default-browser-check',
|
|
364
|
-
'--disable-features=DefaultBrowserSetting,ChromeWhatsNewUI',
|
|
365
|
-
'--disable-crash-reporter',
|
|
366
|
-
// Suppress navigator.webdriver = true, which Chromium sets whenever a
|
|
367
|
-
// remote-debugging transport is active. That property is the loudest
|
|
368
|
-
// signal Cloudflare Turnstile, hCaptcha, and similar checks read.
|
|
369
|
-
'--disable-blink-features=AutomationControlled',
|
|
370
|
-
// Companion to `session.restore_on_startup: 1` (see
|
|
371
|
-
// ensureProfilePreferences): the pref keeps session cookies alive across
|
|
372
|
-
// restarts, but on its own it would also reopen last session's tabs at
|
|
373
|
-
// startup. Suppressing the startup window leaves restore nothing to fill —
|
|
374
|
-
// cookies survive, no ghost tabs — and the task flow creates its own tab
|
|
375
|
-
// over CDP anyway. Electron apps need their window to appear (the CDP
|
|
376
|
-
// driver binds to it), so they skip the flag.
|
|
377
|
-
...(isElectron || ownerStore ? [] : ['--no-startup-window']),
|
|
378
|
-
...(options.headless ? ['--headless=new'] : []),
|
|
379
|
-
`--window-size=${viewport.width},${viewport.height}`,
|
|
380
|
-
...(viewport.x !== undefined && viewport.y !== undefined
|
|
381
|
-
? [`--window-position=${viewport.x},${viewport.y}`]
|
|
382
|
-
: []),
|
|
383
|
-
...(options.args || []),
|
|
384
|
-
];
|
|
385
|
-
// Profile secrets: agentOnly (SEC-13) — a locked bundle resolves to an empty map
|
|
386
|
-
// (launch proceeds without them), never a Touch ID prompt. See resolveProfileSecretsEnv.
|
|
387
|
-
const env = { ...process.env, ...(await resolveProfileSecretsEnv(secrets)) };
|
|
388
|
-
const child = spawn(browserPath, args, {
|
|
389
|
-
detached: true,
|
|
390
|
-
stdio: transport === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
391
|
-
env,
|
|
392
|
-
});
|
|
393
|
-
child.unref();
|
|
394
|
-
child.stdout?.resume();
|
|
395
|
-
child.stderr?.resume();
|
|
396
|
-
const pid = child.pid;
|
|
397
|
-
let wsUrl;
|
|
398
|
-
if (transport === 'pipe') {
|
|
399
|
-
const writePipe = child.stdio[3];
|
|
400
|
-
const readPipe = child.stdio[4];
|
|
401
|
-
if (!writePipe || !readPipe) {
|
|
402
|
-
throw new Error('Chrome failed to expose CDP pipe file descriptors');
|
|
403
|
-
}
|
|
404
|
-
wsUrl = registerPipeTransport({ read: readPipe, write: writePipe });
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
wsUrl = (await waitForDevToolsPort(port, profileName, child, { browserType, userDataDir, profileDirectory: options.profileDirectory })).wsUrl;
|
|
408
|
-
}
|
|
409
|
-
writeProfileRuntime(profileName, {
|
|
410
|
-
pid,
|
|
411
|
-
command: path.basename(browserPath),
|
|
412
|
-
userDataDir,
|
|
413
|
-
kind: isElectron ? 'electron' : 'browser',
|
|
414
|
-
});
|
|
415
|
-
return { pid, port: transport === 'pipe' ? 0 : port, wsUrl };
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Poll the DevTools endpoint of a browser just launched with
|
|
419
|
-
* `--remote-debugging-port` until it answers, or fail loud naming the pid.
|
|
420
|
-
* Chromium binds the port only after its profile has loaded, which on a real
|
|
421
|
-
* signed-in store takes a few seconds. A child that exits first never bound
|
|
422
|
-
* it: on an owner's store that is the singleton hand-off to a browser already
|
|
423
|
-
* open there (PHNX-4042), so the failure names that instance's relaunch
|
|
424
|
-
* instead of waiting out the deadline.
|
|
425
|
-
*/
|
|
426
|
-
async function waitForDevToolsPort(port, profileName, child, store, timeoutMs = 20_000) {
|
|
427
|
-
const pid = child.pid;
|
|
428
|
-
let exited;
|
|
429
|
-
child.once('exit', (code, signal) => {
|
|
430
|
-
exited = code !== null ? `code ${code}` : `signal ${signal}`;
|
|
431
|
-
});
|
|
432
|
-
const deadline = Date.now() + timeoutMs;
|
|
433
|
-
let lastError = '';
|
|
434
|
-
while (Date.now() < deadline) {
|
|
435
|
-
if (exited) {
|
|
436
|
-
const app = store.browserType === 'comet' ? 'Comet' : store.browserType;
|
|
437
|
-
throw new Error(`${app} (pid ${pid}) exited (${exited}) before serving the DevTools protocol on port ${port} ` +
|
|
438
|
-
`for profile "${profileName}". A ${app} already open on ${store.userDataDir} takes a new launch ` +
|
|
439
|
-
`as an argument hand-off and never binds the port; quit it, then relaunch it with remote debugging:\n` +
|
|
440
|
-
` ${storeRelaunchCommand(store.browserType, port, store.userDataDir, store.profileDirectory)}\n` +
|
|
441
|
-
`and retry. If none is open, the browser crashed on start.`);
|
|
442
|
-
}
|
|
443
|
-
try {
|
|
444
|
-
return await discoverBrowserWsUrl(port, 'localhost', profileName);
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
448
|
-
}
|
|
449
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
450
|
-
}
|
|
451
|
-
throw new Error(`Browser for profile "${profileName}" (pid ${pid}) did not serve the DevTools protocol on ` +
|
|
452
|
-
`port ${port} within ${Math.round(timeoutMs / 1000)}s: ${lastError}`);
|
|
453
|
-
}
|
|
454
|
-
export function killChrome(pid) {
|
|
455
|
-
if (process.platform === 'win32') {
|
|
456
|
-
// On Windows `process.kill(pid, 'SIGINT')` maps to TerminateProcess — a
|
|
457
|
-
// hard kill that skips Chromium's on-exit profile flush (leaving a dirty
|
|
458
|
-
// "Chrome didn't shut down correctly" state). Ask taskkill for a graceful
|
|
459
|
-
// stop first (no /F → posts WM_CLOSE), and only force-kill the tree if that
|
|
460
|
-
// fails or the process is still around.
|
|
461
|
-
try {
|
|
462
|
-
execFileSync('taskkill', ['/pid', String(pid)], { stdio: 'ignore', windowsHide: true });
|
|
463
|
-
}
|
|
464
|
-
catch {
|
|
465
|
-
try {
|
|
466
|
-
execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
|
|
467
|
-
}
|
|
468
|
-
catch {
|
|
469
|
-
// Process already dead
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
try {
|
|
475
|
-
process.kill(pid, 'SIGINT');
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
// Process already dead
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
export function getRunningChromeInfo(profileName) {
|
|
482
|
-
// Delegate to runtime-state, which auto-cleans stale files and verifies
|
|
483
|
-
// the live pid still runs the command we recorded — so a recycled pid
|
|
484
|
-
// doesn't masquerade as our browser.
|
|
485
|
-
const rt = readProfileRuntime(profileName);
|
|
486
|
-
if (!rt)
|
|
487
|
-
return null;
|
|
488
|
-
if (rt.port === undefined)
|
|
489
|
-
return null;
|
|
490
|
-
return { pid: rt.pid, port: rt.port };
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Prepare `<userDataDir>/Default/Preferences` before launch.
|
|
494
|
-
*
|
|
495
|
-
* Two concerns, one write:
|
|
496
|
-
* - First launch (file absent): stamp the agents-cli profile name so
|
|
497
|
-
* Chromium's UI shows "<profile>" instead of its default "Person 1".
|
|
498
|
-
* Cosmetic; existing files keep whatever Chrome wrote in the meantime.
|
|
499
|
-
* - Every launch (when `persistSessionCookies`): pin
|
|
500
|
-
* `session.restore_on_startup: 1` ("continue where you left off").
|
|
501
|
-
* Chromium purges memory-only session cookies at startup UNLESS this
|
|
502
|
-
* preference says the session will be restored — it keys the purge off
|
|
503
|
-
* the pref, not off tabs actually reopening. Sites like idealista issue
|
|
504
|
-
* login cookies with `expires=-1`, so without this every browser restart
|
|
505
|
-
* silently logs the profile out. The visible tab-restore side effect is
|
|
506
|
-
* suppressed separately via `--no-startup-window` (see launchBrowser).
|
|
507
|
-
*
|
|
508
|
-
* Runs only while the browser is down (called before spawn), so Chromium
|
|
509
|
-
* can't overwrite the patch on exit. Best-effort: a malformed existing file
|
|
510
|
-
* is left untouched (Chromium recovers its own state better than we can),
|
|
511
|
-
* and any I/O hiccup is silently ignored.
|
|
512
|
-
*/
|
|
513
|
-
export function ensureProfilePreferences(userDataDir, profileName, persistSessionCookies) {
|
|
514
|
-
const defaultDir = path.join(userDataDir, 'Default');
|
|
515
|
-
const prefsPath = path.join(defaultDir, 'Preferences');
|
|
516
|
-
let prefs;
|
|
517
|
-
try {
|
|
518
|
-
prefs = JSON.parse(fs.readFileSync(prefsPath, 'utf8'));
|
|
519
|
-
if (typeof prefs !== 'object' || prefs === null)
|
|
520
|
-
return; // not ours to fix
|
|
521
|
-
}
|
|
522
|
-
catch (err) {
|
|
523
|
-
if (err?.code !== 'ENOENT')
|
|
524
|
-
return; // unreadable/malformed: leave alone
|
|
525
|
-
}
|
|
526
|
-
const firstLaunch = prefs === undefined;
|
|
527
|
-
if (firstLaunch)
|
|
528
|
-
prefs = { profile: { name: profileName } };
|
|
529
|
-
let dirty = firstLaunch;
|
|
530
|
-
if (persistSessionCookies && prefs.session?.restore_on_startup !== 1) {
|
|
531
|
-
prefs.session = { ...prefs.session, restore_on_startup: 1 };
|
|
532
|
-
dirty = true;
|
|
533
|
-
}
|
|
534
|
-
if (!dirty)
|
|
535
|
-
return;
|
|
536
|
-
try {
|
|
537
|
-
fs.mkdirSync(defaultDir, { recursive: true });
|
|
538
|
-
fs.writeFileSync(prefsPath, JSON.stringify(prefs));
|
|
539
|
-
}
|
|
540
|
-
catch { /* not critical */ }
|
|
541
|
-
}
|
|
542
|
-
function sleep(ms) {
|
|
543
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Is a TCP port currently bound? `lsof` on POSIX, `netstat -ano` on Windows
|
|
547
|
-
* (lsof doesn't exist there). Returns false on any tooling error so port
|
|
548
|
-
* allocation degrades to "assume free" rather than throwing.
|
|
549
|
-
*
|
|
550
|
-
* Exported as the canonical cross-platform probe — `findFreeProfilePort`
|
|
551
|
-
* (profiles.ts) must use this rather than shelling out to lsof directly,
|
|
552
|
-
* or every port scans as free on Windows.
|
|
553
|
-
*/
|
|
554
|
-
export function isPortInUse(port) {
|
|
555
|
-
if (process.platform === 'win32') {
|
|
556
|
-
try {
|
|
557
|
-
const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], {
|
|
558
|
-
encoding: 'utf8',
|
|
559
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
560
|
-
windowsHide: true,
|
|
561
|
-
});
|
|
562
|
-
// Lines look like: " TCP 0.0.0.0:9200 0.0.0.0:0 LISTENING 1234"
|
|
563
|
-
return out.split('\n').some((line) => {
|
|
564
|
-
const f = line.trim().split(/\s+/);
|
|
565
|
-
return f[0] === 'TCP' && f[3] === 'LISTENING' && !!f[1]?.endsWith(`:${port}`);
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
return false;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
try {
|
|
573
|
-
execFileSync('lsof', ['-i', `:${port}`], { stdio: 'ignore' });
|
|
574
|
-
return true; // lsof found a binding
|
|
575
|
-
}
|
|
576
|
-
catch {
|
|
577
|
-
return false; // nothing on the port
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
export function allocatePort() {
|
|
581
|
-
const base = 9200;
|
|
582
|
-
const max = 9300;
|
|
583
|
-
for (let port = base; port < max; port++) {
|
|
584
|
-
if (!isPortInUse(port)) {
|
|
585
|
-
return port;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
throw new Error('No available ports in range 9200-9300');
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Read the `--user-data-dir` a running browser process was launched with, by
|
|
592
|
-
* inspecting its command line (PHNX-3967). This is the ownership signal the
|
|
593
|
-
* attach-only guard uses to tell the credentialed canonical browser apart from a
|
|
594
|
-
* foreign port-squatter serving CDP on the same port (e.g. a logged-out
|
|
595
|
-
* `/tmp/...` Comet). Returns null when the pid is gone, exposes no
|
|
596
|
-
* `--user-data-dir`, or the platform probe fails.
|
|
597
|
-
*
|
|
598
|
-
* POSIX: `ps -ww -o command=` (full, un-truncated argv). Windows: the
|
|
599
|
-
* `CommandLine` from `Win32_Process` via PowerShell. The value is captured up to
|
|
600
|
-
* the next ` --<flag>` (or end of line) so a data dir that itself contains a
|
|
601
|
-
* space is not truncated at the space.
|
|
602
|
-
*/
|
|
603
|
-
export function getProcessUserDataDir(pid) {
|
|
604
|
-
if (!pid || pid <= 0)
|
|
605
|
-
return null;
|
|
606
|
-
let cmdline = '';
|
|
607
|
-
try {
|
|
608
|
-
if (process.platform === 'win32') {
|
|
609
|
-
cmdline = execFileSync('powershell', [
|
|
610
|
-
'-NoProfile',
|
|
611
|
-
'-Command',
|
|
612
|
-
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`,
|
|
613
|
-
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
cmdline = execFileSync('ps', ['-ww', '-o', 'command=', '-p', String(pid)], {
|
|
617
|
-
encoding: 'utf8',
|
|
618
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
catch {
|
|
623
|
-
return null;
|
|
624
|
-
}
|
|
625
|
-
return parseUserDataDirFromCommandLine(cmdline);
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Extract the `--user-data-dir` value from a browser command line. Handles both
|
|
629
|
-
* `--user-data-dir=<path>` and `--user-data-dir <path>` and stops the value at
|
|
630
|
-
* the next ` --<flag>` so a path containing spaces survives. Exported for unit
|
|
631
|
-
* testing without a live process.
|
|
632
|
-
*/
|
|
633
|
-
export function parseUserDataDirFromCommandLine(cmdline) {
|
|
634
|
-
const line = cmdline.replace(/\r?\n/g, ' ').trim();
|
|
635
|
-
const m = line.match(/--user-data-dir[=\s]+(.*?)(?=\s--[A-Za-z]|$)/);
|
|
636
|
-
if (!m)
|
|
637
|
-
return null;
|
|
638
|
-
const value = m[1].trim().replace(/^["']|["']$/g, '');
|
|
639
|
-
return value.length > 0 ? value : null;
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Identify the process listening on a TCP port. Returns null when nothing is bound.
|
|
643
|
-
* Used for clearer error messages when a profile's configured port is taken by a
|
|
644
|
-
* non-debug process (e.g. Comet running without --remote-debugging-port).
|
|
645
|
-
* `lsof` on POSIX; `netstat -ano` + `tasklist` on Windows.
|
|
646
|
-
*/
|
|
647
|
-
export function getPortOccupant(port) {
|
|
648
|
-
if (process.platform === 'win32') {
|
|
649
|
-
try {
|
|
650
|
-
const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], {
|
|
651
|
-
encoding: 'utf8',
|
|
652
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
653
|
-
windowsHide: true,
|
|
654
|
-
});
|
|
655
|
-
let pid = 0;
|
|
656
|
-
for (const line of out.split('\n')) {
|
|
657
|
-
const f = line.trim().split(/\s+/);
|
|
658
|
-
if (f[0] === 'TCP' && f[3] === 'LISTENING' && f[1]?.endsWith(`:${port}`)) {
|
|
659
|
-
pid = parseInt(f[4], 10) || 0;
|
|
660
|
-
break;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if (!pid)
|
|
664
|
-
return null;
|
|
665
|
-
let command = 'unknown';
|
|
666
|
-
try {
|
|
667
|
-
// tasklist CSV row: "image.exe","1234","Console","1","12,345 K"
|
|
668
|
-
const tl = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], {
|
|
669
|
-
encoding: 'utf8',
|
|
670
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
671
|
-
windowsHide: true,
|
|
672
|
-
});
|
|
673
|
-
const m = tl.match(/^"([^"]+)"/);
|
|
674
|
-
if (m)
|
|
675
|
-
command = m[1];
|
|
676
|
-
}
|
|
677
|
-
catch { /* keep 'unknown' */ }
|
|
678
|
-
return { pid, command };
|
|
679
|
-
}
|
|
680
|
-
catch {
|
|
681
|
-
return null;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
try {
|
|
685
|
-
const out = execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpcn'], {
|
|
686
|
-
encoding: 'utf8',
|
|
687
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
688
|
-
});
|
|
689
|
-
let pid = 0;
|
|
690
|
-
let command = '';
|
|
691
|
-
for (const line of out.split('\n')) {
|
|
692
|
-
if (line.startsWith('p'))
|
|
693
|
-
pid = parseInt(line.slice(1), 10) || 0;
|
|
694
|
-
else if (line.startsWith('c') && !command)
|
|
695
|
-
command = line.slice(1);
|
|
696
|
-
}
|
|
697
|
-
if (!pid)
|
|
698
|
-
return null;
|
|
699
|
-
return { pid, command: command || 'unknown' };
|
|
700
|
-
}
|
|
701
|
-
catch {
|
|
702
|
-
return null;
|
|
703
|
-
}
|
|
704
|
-
}
|