@phnx-labs/agents-cli 1.22.117 → 1.22.118
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 +235 -0
- package/dist/bootstrap.js +4 -11
- 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/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/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 +6 -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-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 +0 -11
- 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/harness-auth-capabilities.js +0 -1
- 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.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- 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/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/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/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 +10 -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,1157 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, getBrowserDurableDir, readMeta, updateMeta, } from '../state.js';
|
|
4
|
-
import { getConfigValue } from '../device-config.js';
|
|
5
|
-
import { machineId } from '../machine-id.js';
|
|
6
|
-
import { declaringDevices, profileRegistry, } from './registry.js';
|
|
7
|
-
import { findBrowserPath, isPortInUse } from './chrome.js';
|
|
8
|
-
import { arcSpaceProfiles, discoverArcProfiles } from './arc-discovery.js';
|
|
9
|
-
import { discoverChromiumProfiles, discoverableChromiumBrowsers } from './chromium-discovery.js';
|
|
10
|
-
import { firefoxDiscoveredProfiles, discoverFirefoxProfiles } from './firefox-discovery.js';
|
|
11
|
-
export { declaringDevices, profileKind, profileRegistry, } from './registry.js';
|
|
12
|
-
/**
|
|
13
|
-
* Name of the profile the setup wizards pin as this machine's default browser
|
|
14
|
-
* (`agents setup`, `agents setup browser`). Older builds also auto-created it
|
|
15
|
-
* silently on the first `agents browser start`; PHNX-3296 removed that — see
|
|
16
|
-
* {@link ensureDefaultBrowserProfile}.
|
|
17
|
-
*
|
|
18
|
-
* It is `auto-chrome`, NOT `default`, since RUSH-2709: `default` used to be
|
|
19
|
-
* both this concrete profile AND the alias meaning "whatever profile the user
|
|
20
|
-
* configured", so `--profile default` landed on a literal Chrome on one command
|
|
21
|
-
* and on the user's configured Comet on another. The alias now lives alone in
|
|
22
|
-
* {@link DEFAULT_PROFILE_ALIAS} and resolves in exactly one place
|
|
23
|
-
* ({@link resolveProfileRef}).
|
|
24
|
-
*/
|
|
25
|
-
export const DEFAULT_BROWSER_PROFILE_NAME = 'auto-chrome';
|
|
26
|
-
/**
|
|
27
|
-
* What older builds named the auto-detected profile. Still honored everywhere:
|
|
28
|
-
* a user whose machine already carries a `default` profile keeps using it (and
|
|
29
|
-
* its running browser + runtime dirs) rather than having a second one created
|
|
30
|
-
* beside it.
|
|
31
|
-
*/
|
|
32
|
-
export const LEGACY_DEFAULT_BROWSER_PROFILE_NAME = 'default';
|
|
33
|
-
/**
|
|
34
|
-
* The reserved ALIAS. `--profile default` (and a `browser.profile` config value
|
|
35
|
-
* of `default`) means "the configured default profile", not a profile called
|
|
36
|
-
* `default` — unless a profile literally named `default` exists, which still
|
|
37
|
-
* wins, so no existing config breaks.
|
|
38
|
-
*/
|
|
39
|
-
export const DEFAULT_PROFILE_ALIAS = 'default';
|
|
40
|
-
/**
|
|
41
|
-
* The device-local configured default profile name (set via
|
|
42
|
-
* `agents browser use`), or undefined when unset. When set, it
|
|
43
|
-
* is the profile `agents browser start` resolves to for BOTH the no-`--profile`
|
|
44
|
-
* path and an explicit `--profile default`. Stored as this machine's
|
|
45
|
-
* `browser.profile` device-config key (the per-device doc's `config:` block —
|
|
46
|
-
* see lib/device-config.ts).
|
|
47
|
-
*/
|
|
48
|
-
export function getConfiguredDefaultProfileName() {
|
|
49
|
-
return getConfigValue('browser.profile').value || undefined;
|
|
50
|
-
}
|
|
51
|
-
export function getBrowserRuntimeDir() {
|
|
52
|
-
return getBrowserRuntimeDirRoot();
|
|
53
|
-
}
|
|
54
|
-
export function getProfileRuntimeDir(name) {
|
|
55
|
-
return path.join(getBrowserRuntimeDir(), name);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* True when a profile attaches to a browser the user already launched and MUST
|
|
59
|
-
* NOT spawn its own (PHNX-3967). Arc is inherently attach-only — it is
|
|
60
|
-
* single-instance and relaunching it just produces a stray window, never a
|
|
61
|
-
* second debuggable process (issue #2779) — regardless of an explicit policy.
|
|
62
|
-
* Any other browser is attach-only only when the profile says so.
|
|
63
|
-
*/
|
|
64
|
-
export function isAttachOnlyProfile(profile) {
|
|
65
|
-
return profile.browser === 'arc' || profile.launchPolicy === 'attach-only';
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* The DURABLE `--user-data-dir` for a profile (PHNX-3967). An explicit
|
|
69
|
-
* `profile.userDataDir` wins; otherwise a per-profile dir under
|
|
70
|
-
* `~/.agents/.history/browser-profiles/<name>/chrome-data`, which is outside
|
|
71
|
-
* `~/.agents/.cache` so a one-time sign-in survives quit+relaunch and the
|
|
72
|
-
* `profiles remove` cache sweep. This is the dir the canonical Comet is launched
|
|
73
|
-
* with and the value the ownership guard compares a running instance against.
|
|
74
|
-
*
|
|
75
|
-
* Note this is distinct from {@link getProfileRuntimeDir}'s cache `chrome-data`,
|
|
76
|
-
* which `launchBrowser` uses for a `launch`-policy profile it spawns itself.
|
|
77
|
-
*/
|
|
78
|
-
export function resolveProfileDataDir(profile) {
|
|
79
|
-
if (profile.userDataDir)
|
|
80
|
-
return profile.userDataDir;
|
|
81
|
-
return path.join(getBrowserDurableDir(), profile.name, 'chrome-data');
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Normalize a `--user-data-dir` path for the attach-only ownership comparison
|
|
85
|
-
* (PHNX-3967): the real path when it resolves on this box, else the literal with
|
|
86
|
-
* trailing slashes stripped. The single source both the runtime guard
|
|
87
|
-
* (`drivers/local.ts` `verifyEndpointOwnership`) and `browser profiles doctor`
|
|
88
|
-
* use, so they can never disagree about whether a running instance is foreign.
|
|
89
|
-
*/
|
|
90
|
-
export function normalizeDataDir(dir) {
|
|
91
|
-
let out = dir;
|
|
92
|
-
try {
|
|
93
|
-
out = fs.realpathSync(dir);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
/* dir may not exist on this box; compare the literal */
|
|
97
|
-
}
|
|
98
|
-
return out.replace(/\/+$/, '');
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Default destination for browser downloads for a profile. Set browser-global at
|
|
102
|
-
* connect time (see BrowserService), so downloads land here even when the agent
|
|
103
|
-
* never calls `browser download --path`. Keyed by the same composite name as the
|
|
104
|
-
* runtime dir, so every profile is one self-contained tree.
|
|
105
|
-
*/
|
|
106
|
-
export function getProfileDownloadsDir(name) {
|
|
107
|
-
return path.join(getProfileRuntimeDir(name), 'downloads');
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Per-profile directory for a task's captured artifacts (screenshots, PDFs,
|
|
111
|
-
* recordings). Replaces the legacy global `browser/sessions/<task>/` root — the
|
|
112
|
-
* one-shot migration in migrate.ts folds old captures into here.
|
|
113
|
-
*/
|
|
114
|
-
export function getProfileSessionsDir(name, task) {
|
|
115
|
-
return path.join(getProfileRuntimeDir(name), 'sessions', task);
|
|
116
|
-
}
|
|
117
|
-
function configToProfile(name, config, devices = []) {
|
|
118
|
-
validateRemoteBrowserBinaries(config);
|
|
119
|
-
return {
|
|
120
|
-
name,
|
|
121
|
-
description: config.description,
|
|
122
|
-
browser: config.browser,
|
|
123
|
-
binary: config.binary,
|
|
124
|
-
electron: config.electron,
|
|
125
|
-
targetFilter: config.targetFilter,
|
|
126
|
-
endpoints: config.endpoints,
|
|
127
|
-
defaultEndpoint: config.defaultEndpoint,
|
|
128
|
-
launchPolicy: config.launchPolicy,
|
|
129
|
-
userDataDir: config.userDataDir,
|
|
130
|
-
profileDirectory: config.profileDirectory,
|
|
131
|
-
chrome: config.chrome,
|
|
132
|
-
secrets: config.secrets,
|
|
133
|
-
viewport: config.viewport,
|
|
134
|
-
logDir: config.logDir,
|
|
135
|
-
logHost: config.logHost,
|
|
136
|
-
arc: config.arc,
|
|
137
|
-
firefox: config.firefox,
|
|
138
|
-
devices,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
function profileToConfig(profile) {
|
|
142
|
-
validateRemoteBrowserBinaries(profile);
|
|
143
|
-
const config = {
|
|
144
|
-
browser: profile.browser,
|
|
145
|
-
endpoints: profile.endpoints,
|
|
146
|
-
};
|
|
147
|
-
if (profile.description)
|
|
148
|
-
config.description = profile.description;
|
|
149
|
-
if (profile.binary)
|
|
150
|
-
config.binary = profile.binary;
|
|
151
|
-
if (profile.electron)
|
|
152
|
-
config.electron = profile.electron;
|
|
153
|
-
if (profile.targetFilter)
|
|
154
|
-
config.targetFilter = profile.targetFilter;
|
|
155
|
-
if (profile.defaultEndpoint)
|
|
156
|
-
config.defaultEndpoint = profile.defaultEndpoint;
|
|
157
|
-
if (profile.launchPolicy)
|
|
158
|
-
config.launchPolicy = profile.launchPolicy;
|
|
159
|
-
if (profile.userDataDir)
|
|
160
|
-
config.userDataDir = profile.userDataDir;
|
|
161
|
-
if (profile.profileDirectory)
|
|
162
|
-
config.profileDirectory = profile.profileDirectory;
|
|
163
|
-
if (profile.chrome)
|
|
164
|
-
config.chrome = profile.chrome;
|
|
165
|
-
if (profile.secrets)
|
|
166
|
-
config.secrets = profile.secrets;
|
|
167
|
-
if (profile.viewport)
|
|
168
|
-
config.viewport = profile.viewport;
|
|
169
|
-
if (profile.logDir)
|
|
170
|
-
config.logDir = profile.logDir;
|
|
171
|
-
if (profile.logHost)
|
|
172
|
-
config.logHost = profile.logHost;
|
|
173
|
-
if (profile.arc)
|
|
174
|
-
config.arc = profile.arc;
|
|
175
|
-
if (profile.firefox)
|
|
176
|
-
config.firefox = profile.firefox;
|
|
177
|
-
return config;
|
|
178
|
-
}
|
|
179
|
-
export function discoverNativeProfiles() {
|
|
180
|
-
const profiles = [];
|
|
181
|
-
const errors = {};
|
|
182
|
-
const arc = discoverArcProfiles();
|
|
183
|
-
if (arc.ok) {
|
|
184
|
-
for (const space of arcSpaceProfiles(arc)) {
|
|
185
|
-
profiles.push({
|
|
186
|
-
name: space.name,
|
|
187
|
-
description: `Arc Space "${space.spaceTitle}" (${space.profileName})`,
|
|
188
|
-
browser: 'arc',
|
|
189
|
-
endpoints: { native: { target: 'arc-native://local' } },
|
|
190
|
-
defaultEndpoint: 'native',
|
|
191
|
-
launchPolicy: 'attach-only',
|
|
192
|
-
arc: {
|
|
193
|
-
profileId: space.profileId,
|
|
194
|
-
profileName: space.profileName,
|
|
195
|
-
spaceId: space.spaceId,
|
|
196
|
-
spaceTitle: space.spaceTitle,
|
|
197
|
-
},
|
|
198
|
-
devices: [machineId()],
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
else if (arc.kind === 'invalid') {
|
|
203
|
-
errors.arc = `Cannot discover Arc Spaces: ${arc.reason}`;
|
|
204
|
-
}
|
|
205
|
-
// Ports handed out earlier in this pass: two unpublished profiles must never
|
|
206
|
-
// derive the same port before either is declared.
|
|
207
|
-
const reserved = new Set();
|
|
208
|
-
for (const browser of discoverableChromiumBrowsers()) {
|
|
209
|
-
const found = discoverChromiumProfiles(browser);
|
|
210
|
-
if (!found.ok) {
|
|
211
|
-
if (found.kind === 'invalid')
|
|
212
|
-
errors[browser] = `Cannot discover ${browser} profiles: ${found.reason}`;
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
for (const native of found.profiles) {
|
|
216
|
-
const port = nativeChromiumPort(native.name, reserved);
|
|
217
|
-
reserved.add(port);
|
|
218
|
-
profiles.push({
|
|
219
|
-
name: native.name,
|
|
220
|
-
description: `${browser} profile "${native.displayName}" (${native.profileDirectory})`,
|
|
221
|
-
browser,
|
|
222
|
-
endpoints: [`cdp://127.0.0.1:${port}`],
|
|
223
|
-
userDataDir: native.userDataDir,
|
|
224
|
-
profileDirectory: native.profileDirectory,
|
|
225
|
-
devices: [machineId()],
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
// Firefox (PHNX-4043): one profiles.ini entry is one `firefox-<slug>` profile,
|
|
230
|
-
// pinned to that directory and a stable WebDriver BiDi port. Firefox dropped
|
|
231
|
-
// CDP in 129, so these use a `firefox-bidi:` endpoint, not `cdp://`.
|
|
232
|
-
const firefox = discoverFirefoxProfiles();
|
|
233
|
-
if (firefox.ok) {
|
|
234
|
-
for (const entry of firefoxDiscoveredProfiles(firefox)) {
|
|
235
|
-
profiles.push({
|
|
236
|
-
name: entry.name,
|
|
237
|
-
description: `Firefox profile "${entry.profileName}"${entry.isDefault ? ' (default)' : ''}`,
|
|
238
|
-
browser: 'firefox',
|
|
239
|
-
endpoints: { bidi: { target: `firefox-bidi://127.0.0.1:${entry.port}` } },
|
|
240
|
-
defaultEndpoint: 'bidi',
|
|
241
|
-
userDataDir: entry.profileDir,
|
|
242
|
-
firefox: {
|
|
243
|
-
profileName: entry.profileName,
|
|
244
|
-
iniPath: entry.iniPath,
|
|
245
|
-
isDefault: entry.isDefault,
|
|
246
|
-
},
|
|
247
|
-
devices: [machineId()],
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
else if (firefox.kind === 'invalid') {
|
|
252
|
-
errors.firefox = `Cannot discover Firefox profiles: ${firefox.reason}`;
|
|
253
|
-
}
|
|
254
|
-
return { profiles, errors };
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* The CDP port a discovered Chromium profile attaches on. Once the profile is
|
|
258
|
-
* declared its port lives in the declaration; before that, the port is the
|
|
259
|
-
* first free one from the canonical Comet port (9333, PHNX-3967), stepping
|
|
260
|
-
* past ports other local declarations hold and past `reserved`, the ports
|
|
261
|
-
* already given to sibling profiles in the same discovery pass.
|
|
262
|
-
*/
|
|
263
|
-
function nativeChromiumPort(name, reserved) {
|
|
264
|
-
const declared = localDeclaration(name)?.config;
|
|
265
|
-
const declaredPort = declared ? parseEndpointPort(declared) : undefined;
|
|
266
|
-
if (declaredPort)
|
|
267
|
-
return declaredPort;
|
|
268
|
-
const taken = new Set(reserved);
|
|
269
|
-
for (const [other, declarations] of profileRegistry()) {
|
|
270
|
-
if (other === name)
|
|
271
|
-
continue;
|
|
272
|
-
for (const declaration of declarations) {
|
|
273
|
-
if (declaration.device !== machineId())
|
|
274
|
-
continue;
|
|
275
|
-
const port = parseEndpointPort(declaration.config);
|
|
276
|
-
if (port)
|
|
277
|
-
taken.add(port);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
let port = 9333;
|
|
281
|
-
while (taken.has(port))
|
|
282
|
-
port += 1;
|
|
283
|
-
return port;
|
|
284
|
-
}
|
|
285
|
-
function parseEndpointPort(config) {
|
|
286
|
-
const endpoints = config.endpoints;
|
|
287
|
-
const first = Array.isArray(endpoints)
|
|
288
|
-
? endpoints[0]
|
|
289
|
-
: endpoints && typeof endpoints === 'object'
|
|
290
|
-
? Object.values(endpoints)[0]?.target
|
|
291
|
-
: undefined;
|
|
292
|
-
if (typeof first !== 'string')
|
|
293
|
-
return undefined;
|
|
294
|
-
const match = /^cdp:\/\/[^:/?]+(?::(\d+)|\?port=(\d+))/.exec(first);
|
|
295
|
-
const raw = match?.[1] ?? match?.[2];
|
|
296
|
-
return raw ? Number(raw) : undefined;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Publish every discovered native profile that this device has not declared
|
|
300
|
-
* yet into its device declaration (`~/.agents/devices/<device>/agents.yaml`),
|
|
301
|
-
* which the fleet shared-store sync carries to every other box (PHNX-4042).
|
|
302
|
-
* That is what lets `agents browser profiles list` on a worker show this
|
|
303
|
-
* Mac's `arc-*` and `comet-*` rows with WHERE=<this device>, and a start from
|
|
304
|
-
* there route here. Idempotent; a discovery error skips that browser.
|
|
305
|
-
*/
|
|
306
|
-
export async function publishDiscoveredProfiles() {
|
|
307
|
-
const discovered = discoverNativeProfiles();
|
|
308
|
-
const published = [];
|
|
309
|
-
for (const profile of discovered.profiles) {
|
|
310
|
-
if (localDeclaration(profile.name))
|
|
311
|
-
continue;
|
|
312
|
-
await createProfile(profile);
|
|
313
|
-
published.push(profile.name);
|
|
314
|
-
}
|
|
315
|
-
return { published, errors: discovered.errors };
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Bring a persisted discovered profile up to date with its live source (an Arc
|
|
319
|
-
* Space title rename, a Comet profile rename). A source that is no longer
|
|
320
|
-
* discoverable leaves the declaration as stored: listing still works, and using
|
|
321
|
-
* the profile fails loud where the browser is actually reached.
|
|
322
|
-
*/
|
|
323
|
-
function refreshDiscoveredProfile(profile, live) {
|
|
324
|
-
if (!profile.devices.includes(machineId()))
|
|
325
|
-
return profile;
|
|
326
|
-
if (profile.arc) {
|
|
327
|
-
const current = live.find((candidate) => candidate.arc?.spaceId === profile.arc?.spaceId);
|
|
328
|
-
if (!current)
|
|
329
|
-
return profile;
|
|
330
|
-
return { ...profile, arc: current.arc, description: profile.description ?? current.description };
|
|
331
|
-
}
|
|
332
|
-
if (profile.userDataDir && profile.profileDirectory) {
|
|
333
|
-
const current = live.find((candidate) => candidate.userDataDir === profile.userDataDir && candidate.profileDirectory === profile.profileDirectory);
|
|
334
|
-
if (!current)
|
|
335
|
-
return profile;
|
|
336
|
-
return { ...profile, description: profile.description ?? current.description };
|
|
337
|
-
}
|
|
338
|
-
return profile;
|
|
339
|
-
}
|
|
340
|
-
function selectedDeclaration(declarations) {
|
|
341
|
-
return declarations.find((declaration) => declaration.device === machineId()) ?? declarations[0];
|
|
342
|
-
}
|
|
343
|
-
function localDeclaration(name) {
|
|
344
|
-
return profileRegistry().get(name)?.find((declaration) => declaration.device === machineId());
|
|
345
|
-
}
|
|
346
|
-
/** Whether this name has persisted agents-cli metadata on the current device. */
|
|
347
|
-
export function isProfileDeclaredHere(name) {
|
|
348
|
-
return localDeclaration(name) !== undefined;
|
|
349
|
-
}
|
|
350
|
-
export async function listProfiles() {
|
|
351
|
-
const native = discoverNativeProfiles();
|
|
352
|
-
const configured = [...profileRegistry()].map(([name, declarations]) => {
|
|
353
|
-
const selected = selectedDeclaration(declarations);
|
|
354
|
-
return refreshDiscoveredProfile(configToProfile(name, selected.config, declarations.map((declaration) => declaration.device)), native.profiles);
|
|
355
|
-
});
|
|
356
|
-
const names = new Set(configured.map((profile) => profile.name));
|
|
357
|
-
return [...configured, ...native.profiles.filter((profile) => !names.has(profile.name))];
|
|
358
|
-
}
|
|
359
|
-
export async function getProfile(name) {
|
|
360
|
-
const native = discoverNativeProfiles();
|
|
361
|
-
const declarations = profileRegistry().get(name);
|
|
362
|
-
if (declarations?.length) {
|
|
363
|
-
const selected = selectedDeclaration(declarations);
|
|
364
|
-
return refreshDiscoveredProfile(configToProfile(name, selected.config, declarations.map((declaration) => declaration.device)), native.profiles);
|
|
365
|
-
}
|
|
366
|
-
const discovered = native.profiles.find((profile) => profile.name === name) ?? null;
|
|
367
|
-
// Only a profile of that browser asked for by name pays for its failed read.
|
|
368
|
-
const prefix = name.split('-')[0];
|
|
369
|
-
if (!discovered && native.errors[prefix])
|
|
370
|
-
throw new Error(native.errors[prefix]);
|
|
371
|
-
return discovered;
|
|
372
|
-
}
|
|
373
|
-
/** Persist only the agents-cli alias for an auto-discovered native profile; never the browser's own data. */
|
|
374
|
-
export async function persistDiscoveredProfile(name) {
|
|
375
|
-
if (localDeclaration(name))
|
|
376
|
-
return;
|
|
377
|
-
const profile = discoverNativeProfiles().profiles.find((candidate) => candidate.name === name);
|
|
378
|
-
if (!profile)
|
|
379
|
-
return;
|
|
380
|
-
await createProfile(profile);
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* True when `profile` can actually launch or attach on THIS machine.
|
|
384
|
-
*
|
|
385
|
-
* A remote (`ssh://`) profile runs its browser on the far host, so a local
|
|
386
|
-
* binary check doesn't apply — assume launchable. A local profile must have its
|
|
387
|
-
* configured browser/binary present on disk here; a `default` profile
|
|
388
|
-
* auto-created on a different OS (e.g. a macOS Chrome path reused on Linux — the
|
|
389
|
-
* "Custom binary not found" failure) fails this check so the caller re-detects
|
|
390
|
-
* for the current machine.
|
|
391
|
-
*/
|
|
392
|
-
export function isProfileLaunchableHere(profile) {
|
|
393
|
-
const nativeArc = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('arc-native:'));
|
|
394
|
-
if (nativeArc) {
|
|
395
|
-
return !!profile.arc && discoverNativeProfiles().profiles.some((candidate) => candidate.arc?.spaceId === profile.arc?.spaceId);
|
|
396
|
-
}
|
|
397
|
-
const bidi = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('firefox-bidi:'));
|
|
398
|
-
if (bidi) {
|
|
399
|
-
// Launchable here only when Firefox is installed AND this box holds the
|
|
400
|
-
// pinned profile directory (a synced-in firefox- profile from another box
|
|
401
|
-
// has that box's path, so it command-dispatches instead).
|
|
402
|
-
if (!profile.userDataDir || !fs.existsSync(profile.userDataDir))
|
|
403
|
-
return false;
|
|
404
|
-
try {
|
|
405
|
-
findBrowserPath('firefox', profile.binary);
|
|
406
|
-
return true;
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
const remote = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('ssh://'));
|
|
413
|
-
if (remote)
|
|
414
|
-
return true;
|
|
415
|
-
try {
|
|
416
|
-
findBrowserPath(profile.browser, profile.binary);
|
|
417
|
-
return true;
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* The auto-detected profile this machine already carries, under either name —
|
|
425
|
-
* the current `auto-chrome` or the `default` an older build wrote. Callers that
|
|
426
|
-
* ask "is a default profile set up here?" MUST use this rather than probing one
|
|
427
|
-
* name, or a machine that predates the RUSH-2709 rename grows a second one.
|
|
428
|
-
*/
|
|
429
|
-
export async function getAutoDetectedProfile() {
|
|
430
|
-
const current = machineId();
|
|
431
|
-
const registry = profileRegistry();
|
|
432
|
-
for (const name of [DEFAULT_BROWSER_PROFILE_NAME, LEGACY_DEFAULT_BROWSER_PROFILE_NAME]) {
|
|
433
|
-
const declaration = registry.get(name)?.find((entry) => entry.device === current);
|
|
434
|
-
if (declaration)
|
|
435
|
-
return configToProfile(name, declaration.config, [current]);
|
|
436
|
-
}
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Resolve what a caller typed after `--profile` to a real, BARE profile name.
|
|
441
|
-
* Every command — stop, status, navigate, tab, use, and (through
|
|
442
|
-
* {@link resolveProfileRefForStart}) start — routes through this one function,
|
|
443
|
-
* which is the whole point: before RUSH-2709 only `start` honored the reserved
|
|
444
|
-
* `default` alias, so `--profile default` meant three different profiles across
|
|
445
|
-
* three commands.
|
|
446
|
-
*
|
|
447
|
-
* Order:
|
|
448
|
-
* 1. A profile that literally bears the given name always wins — including
|
|
449
|
-
* one a user genuinely named `default`, which resolves to itself.
|
|
450
|
-
* 2. `default` (the alias) or no argument at all → the configured
|
|
451
|
-
* `browser.profile` for this machine, if it names a profile that exists.
|
|
452
|
-
* 3. → the auto-detected profile ({@link DEFAULT_BROWSER_PROFILE_NAME}), or
|
|
453
|
-
* its legacy `default`-named predecessor when that is what is on disk.
|
|
454
|
-
* 4. Otherwise the input is returned unchanged (so the caller reports its own
|
|
455
|
-
* `Profile "x" not found`), or `undefined` when nothing was passed.
|
|
456
|
-
*
|
|
457
|
-
* It never creates a profile, never warns, and never checks whether the profile
|
|
458
|
-
* can launch HERE. That is deliberate: for most callers `--profile` is a FILTER
|
|
459
|
-
* (`status`, `tasks`, `navigate --task`), where an absent flag means "no filter"
|
|
460
|
-
* and rewriting or warning about the user's config would be wrong. Note the
|
|
461
|
-
* corollary at those call sites — pass a ref only when the user gave one; do not
|
|
462
|
-
* resolve `undefined` into a concrete profile, or an unscoped listing silently
|
|
463
|
-
* becomes a scoped one.
|
|
464
|
-
*/
|
|
465
|
-
export async function resolveProfileRef(ref) {
|
|
466
|
-
if (ref && ref !== DEFAULT_PROFILE_ALIAS) {
|
|
467
|
-
return ref;
|
|
468
|
-
}
|
|
469
|
-
if (ref === DEFAULT_PROFILE_ALIAS && (await getProfile(ref))) {
|
|
470
|
-
// A profile the user literally named `default` outranks the alias.
|
|
471
|
-
return ref;
|
|
472
|
-
}
|
|
473
|
-
const configured = getConfiguredDefaultProfileName();
|
|
474
|
-
if (configured && configured !== DEFAULT_PROFILE_ALIAS && (await getProfile(configured))) {
|
|
475
|
-
return configured;
|
|
476
|
-
}
|
|
477
|
-
if (await getProfile(DEFAULT_BROWSER_PROFILE_NAME))
|
|
478
|
-
return DEFAULT_BROWSER_PROFILE_NAME;
|
|
479
|
-
if (await getProfile(LEGACY_DEFAULT_BROWSER_PROFILE_NAME)) {
|
|
480
|
-
return LEGACY_DEFAULT_BROWSER_PROFILE_NAME;
|
|
481
|
-
}
|
|
482
|
-
return ref;
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* `start`'s resolver. Same order as {@link resolveProfileRef} for an explicit
|
|
486
|
-
* name, but the IMPLICIT path (no `--profile`, or the bare `default` alias with
|
|
487
|
-
* no profile of that name) goes through {@link ensureDefaultBrowserProfile} —
|
|
488
|
-
* which additionally verifies the resolved default can launch on THIS machine.
|
|
489
|
-
* An undeclared configured default is an error. A declared default whose
|
|
490
|
-
* browser isn't installed here warns and falls through to an existing profile,
|
|
491
|
-
* else the actionable throw below.
|
|
492
|
-
*
|
|
493
|
-
* `start` is the only command that launches a browser, so it is the only one
|
|
494
|
-
* that may do those things; routing a filter-only command through this would
|
|
495
|
-
* warn about config the user never asked it to touch.
|
|
496
|
-
*
|
|
497
|
-
* Throws ({@link noDefaultBrowserError}) when the configured default is
|
|
498
|
-
* undeclared, or when no profile exists that can launch here.
|
|
499
|
-
*/
|
|
500
|
-
export async function resolveProfileRefForStart(ref) {
|
|
501
|
-
if (ref && ref !== DEFAULT_PROFILE_ALIAS)
|
|
502
|
-
return ref;
|
|
503
|
-
// A profile the user literally named `default` still outranks the alias.
|
|
504
|
-
if (ref === DEFAULT_PROFILE_ALIAS && (await getProfile(ref)))
|
|
505
|
-
return ref;
|
|
506
|
-
return (await ensureDefaultBrowserProfile()).name;
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* The error a bare `agents browser start` raises when this machine has no
|
|
510
|
-
* launchable default browser. Its own function so the wording — the one thing
|
|
511
|
-
* the user reads when a browser won't start — stays in one place and is
|
|
512
|
-
* testable without spawning anything.
|
|
513
|
-
*
|
|
514
|
-
* Since PHNX-3296 this is a hard stop, NOT a silent auto-create. The old
|
|
515
|
-
* behavior probed the installed Chromium-family browsers and minted a
|
|
516
|
-
* logged-out `auto-chrome` profile on the spot; agents then drove a signed-out
|
|
517
|
-
* Chrome that popped up on the user's Mac unbidden. Which browser agents drive
|
|
518
|
-
* is a choice the user makes once, in `agents setup` — never one this code
|
|
519
|
-
* makes for them.
|
|
520
|
-
*/
|
|
521
|
-
export function noDefaultBrowserError() {
|
|
522
|
-
return new Error('No default browser is configured on this machine. ' +
|
|
523
|
-
'Run `agents setup` (or `agents browser use <name>`) to pick the browser agents should drive. ' +
|
|
524
|
-
'If this is a headless worker, use the fleet hub instead: `agents config set browser.device <host>`.');
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Resolve the profile a bare `agents browser start` uses.
|
|
528
|
-
*
|
|
529
|
-
* Order: (1) the device-local configured default (`agents browser use <name>`)
|
|
530
|
-
* when it names a profile that exists and can launch here; (2) an existing
|
|
531
|
-
* auto-detected profile (`auto-chrome`, or a legacy `default`) that can launch
|
|
532
|
-
* here. When neither resolves, THROW ({@link noDefaultBrowserError}) rather than
|
|
533
|
-
* detect-and-create — see that function for why (PHNX-3296).
|
|
534
|
-
*
|
|
535
|
-
* Two failure modes at the configured-default step are not the same:
|
|
536
|
-
* - No device declares the name (including a leftover central `browser:`
|
|
537
|
-
* entry that was never claimed) → throw. Falling back to a minted profile
|
|
538
|
-
* would hand the agent a logged-out browser while `browser.profile` still
|
|
539
|
-
* names the credentialed one.
|
|
540
|
-
* - The name is declared, but its browser/binary is not installed HERE →
|
|
541
|
-
* warn and fall through to an existing profile, else the actionable throw.
|
|
542
|
-
* That is a missing binary on this box, not a missing identity.
|
|
543
|
-
*
|
|
544
|
-
* This RECOGNIZES a pre-existing `auto-chrome`/legacy `default` so installs that
|
|
545
|
-
* already carry one keep resolving it (and its running browser + runtime dirs),
|
|
546
|
-
* but it never CREATES one.
|
|
547
|
-
*/
|
|
548
|
-
export async function ensureDefaultBrowserProfile() {
|
|
549
|
-
const configured = getConfiguredDefaultProfileName();
|
|
550
|
-
if (configured && configured !== DEFAULT_PROFILE_ALIAS) {
|
|
551
|
-
const chosen = await getProfile(configured);
|
|
552
|
-
if (chosen && (!chosen.devices.includes(machineId()) || isProfileLaunchableHere(chosen)))
|
|
553
|
-
return chosen;
|
|
554
|
-
if (!chosen) {
|
|
555
|
-
const central = readMeta().browser ?? {};
|
|
556
|
-
if (central[configured]) {
|
|
557
|
-
throw new Error(`configured default browser profile "${configured}" is not declared by any device. ` +
|
|
558
|
-
`It still lives in the central agents.yaml browser: map. ` +
|
|
559
|
-
`Claim it on the machine that hosts that browser with: agents browser profiles claim ${configured}`);
|
|
560
|
-
}
|
|
561
|
-
throw new Error(`configured default browser profile "${configured}" is not declared by any device. ` +
|
|
562
|
-
`Create it with: agents browser profiles create ${configured} --browser <chrome|comet|chromium|brave|edge|arc|custom> ` +
|
|
563
|
-
`(or, if it is a leftover central profile, claim it with: agents browser profiles claim ${configured}). ` +
|
|
564
|
-
`Or unset the default with: agents browser use --unset`);
|
|
565
|
-
}
|
|
566
|
-
console.warn(`warning: configured default browser profile "${configured}" can't launch on this ` +
|
|
567
|
-
`machine (its browser/binary isn't installed here). ` +
|
|
568
|
-
`Fix with: agents browser use <name> (or --unset)`);
|
|
569
|
-
}
|
|
570
|
-
// Prefer whichever auto-detected profile this machine already carries: the
|
|
571
|
-
// current `auto-chrome`, else the `default` an older build wrote. Reusing the
|
|
572
|
-
// legacy one is what keeps a currently-running browser (and its runtime dirs,
|
|
573
|
-
// keyed `default@endpoint-0`) attached after the rename.
|
|
574
|
-
const existing = await getAutoDetectedProfile();
|
|
575
|
-
if (existing && isProfileLaunchableHere(existing))
|
|
576
|
-
return existing;
|
|
577
|
-
// No configured default resolves and no existing profile launches here. We
|
|
578
|
-
// used to auto-detect the first installed browser and silently mint (or
|
|
579
|
-
// regenerate) an `auto-chrome` profile at this point — that is exactly the
|
|
580
|
-
// logged-out-Chrome-on-your-Mac bug PHNX-3296 removed. Stop and tell the user
|
|
581
|
-
// how to choose a browser instead.
|
|
582
|
-
throw noDefaultBrowserError();
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Compute the LOCAL port a profile will occupy at runtime:
|
|
586
|
-
* - `cdp://127.0.0.1:N` → N (we listen on N directly)
|
|
587
|
-
* - `ssh://host?port=N` → N (the SSH tunnel binds local N → remote N now)
|
|
588
|
-
* - `ws[s]://`, `http[s]://` → undefined (we don't claim a local port)
|
|
589
|
-
*
|
|
590
|
-
* This is what callers should compare to detect collisions; the (host,
|
|
591
|
-
* port) tuple is no longer enough because SSH profiles do compete with
|
|
592
|
-
* cdp:// profiles for local ports under the new tunnel scheme.
|
|
593
|
-
*/
|
|
594
|
-
export function effectiveLocalPort(profile) {
|
|
595
|
-
const presets = getEndpointPresets(profile);
|
|
596
|
-
const firstName = profile.defaultEndpoint && presets[profile.defaultEndpoint]
|
|
597
|
-
? profile.defaultEndpoint
|
|
598
|
-
: Object.keys(presets)[0];
|
|
599
|
-
if (!firstName)
|
|
600
|
-
return undefined;
|
|
601
|
-
const target = presets[firstName].target;
|
|
602
|
-
let url;
|
|
603
|
-
try {
|
|
604
|
-
url = new URL(target);
|
|
605
|
-
}
|
|
606
|
-
catch {
|
|
607
|
-
return undefined;
|
|
608
|
-
}
|
|
609
|
-
if (url.protocol !== 'cdp:' && url.protocol !== 'ssh:')
|
|
610
|
-
return undefined;
|
|
611
|
-
return parseEndpointUrl(target)?.port;
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Find a port in 9222–9399 that is not already claimed by ANY existing
|
|
615
|
-
* profile (cdp:// or ssh://) and is not in use by any OS process. The
|
|
616
|
-
* SSH change to bind locally on `?port=N` means we no longer get to
|
|
617
|
-
* skip remote profiles in this scan.
|
|
618
|
-
*/
|
|
619
|
-
export async function findFreeProfilePort() {
|
|
620
|
-
const profiles = await listProfiles();
|
|
621
|
-
const usedByProfile = new Set();
|
|
622
|
-
for (const p of profiles) {
|
|
623
|
-
const port = effectiveLocalPort(p);
|
|
624
|
-
if (port !== undefined)
|
|
625
|
-
usedByProfile.add(port);
|
|
626
|
-
}
|
|
627
|
-
for (let port = 9222; port <= 9399; port++) {
|
|
628
|
-
if (usedByProfile.has(port))
|
|
629
|
-
continue;
|
|
630
|
-
// Platform-aware bound-port probe (lsof on POSIX, netstat on Windows).
|
|
631
|
-
// The old inline lsof call threw ENOENT on Windows — where lsof doesn't
|
|
632
|
-
// exist — so EVERY port scanned as "free", including one an already-
|
|
633
|
-
// running browser was listening on (typically 9222), and the new profile
|
|
634
|
-
// would silently attach to that browser instead of launching its own.
|
|
635
|
-
if (!isPortInUse(port))
|
|
636
|
-
return port;
|
|
637
|
-
}
|
|
638
|
-
throw new Error('No available ports in range 9222-9399');
|
|
639
|
-
}
|
|
640
|
-
function validateRemoteBrowserBinaries(profile) {
|
|
641
|
-
if (!hasSshEndpoint(profile.endpoints))
|
|
642
|
-
return;
|
|
643
|
-
validateRemoteBrowserBinary(profile.binary);
|
|
644
|
-
if (!Array.isArray(profile.endpoints)) {
|
|
645
|
-
for (const preset of Object.values(profile.endpoints)) {
|
|
646
|
-
validateRemoteBrowserBinary(preset.binary);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
function validateRemoteBrowserBinary(binary) {
|
|
651
|
-
if (!binary)
|
|
652
|
-
return;
|
|
653
|
-
if (/[\0\r\n;&|`$<>]/.test(binary)) {
|
|
654
|
-
throw new Error(`Remote browser binary contains shell metacharacters: ${binary}`);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
export function hasSshEndpoint(endpoints) {
|
|
658
|
-
const targets = Array.isArray(endpoints)
|
|
659
|
-
? endpoints
|
|
660
|
-
: Object.values(endpoints).map((preset) => preset.target);
|
|
661
|
-
return targets.some((target) => {
|
|
662
|
-
try {
|
|
663
|
-
return new URL(target).protocol === 'ssh:';
|
|
664
|
-
}
|
|
665
|
-
catch {
|
|
666
|
-
return false;
|
|
667
|
-
}
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Whether the automatic central-tombstone drain (PHNX-3315) may claim `config`
|
|
672
|
-
* into THIS device's doc during `agents sync`.
|
|
673
|
-
*
|
|
674
|
-
* Only REMOTE (`ssh://`) profiles qualify. An `ssh://` endpoint names a specific
|
|
675
|
-
* host, so the profile is fungible by design — any box resolves it to the same
|
|
676
|
-
* browser, and a concurrent double-claim across machines is harmless. A
|
|
677
|
-
* local/`cdp://` profile has NO per-machine ownership signal: "that browser is
|
|
678
|
-
* installed here" is not "I hold this profile's credentialed session", so two
|
|
679
|
-
* boxes with the same common browser installed would each auto-claim the same
|
|
680
|
-
* tombstone on their first post-merge sync and flip it identity->fungible
|
|
681
|
-
* fleet-wide (the exact logged-out-browser failure this module exists to prevent
|
|
682
|
-
* — PHNX-3315 review). Local/cdp tombstones stay central for the explicit
|
|
683
|
-
* `agents browser profiles claim`.
|
|
684
|
-
*/
|
|
685
|
-
export function shouldAutoClaimCentralProfile(config) {
|
|
686
|
-
if (!hasSshEndpoint(config.endpoints))
|
|
687
|
-
return false;
|
|
688
|
-
return isProfileLaunchableHere({
|
|
689
|
-
name: '_',
|
|
690
|
-
browser: config.browser,
|
|
691
|
-
binary: config.binary,
|
|
692
|
-
endpoints: config.endpoints,
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Refuse a profile whose LOCAL port another profile already owns.
|
|
697
|
-
*
|
|
698
|
-
* Every CDP/SSH profile ends up listening on (or tunneling to) the same LOCAL
|
|
699
|
-
* port number as the one configured in the endpoint URL — SSH profiles reuse
|
|
700
|
-
* `?port=N` locally, so we no longer scope by host. Two profiles that would
|
|
701
|
-
* need the same local port can't both run at the same time.
|
|
702
|
-
*
|
|
703
|
-
* `opts.ignore` is the profile's own name on an edit. Without it every edit
|
|
704
|
-
* collides with itself, since the stored copy still owns the port being kept.
|
|
705
|
-
*/
|
|
706
|
-
export function assertLocalPortFree(profile, opts = {}) {
|
|
707
|
-
const wanted = effectiveLocalPort(profile);
|
|
708
|
-
if (wanted === undefined)
|
|
709
|
-
return;
|
|
710
|
-
for (const [existingName, declarations] of profileRegistry()) {
|
|
711
|
-
if (existingName === opts.ignore)
|
|
712
|
-
continue;
|
|
713
|
-
const declaration = declarations.find((entry) => entry.device === machineId());
|
|
714
|
-
if (!declaration)
|
|
715
|
-
continue;
|
|
716
|
-
const existingProfile = configToProfile(existingName, declaration.config, [machineId()]);
|
|
717
|
-
if (effectiveLocalPort(existingProfile) === wanted) {
|
|
718
|
-
throw new Error(`Local port ${wanted} is already used by profile "${existingName}". ` +
|
|
719
|
-
`Each profile must own a unique local port (SSH tunnels now bind ` +
|
|
720
|
-
`to their configured port locally too). Pick a different port.`);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
/** Declare a profile on this device. The same name may be declared by peers. */
|
|
725
|
-
export async function createProfile(profile) {
|
|
726
|
-
if (localDeclaration(profile.name)) {
|
|
727
|
-
throw new Error(`Profile "${profile.name}" already exists on ${machineId()}`);
|
|
728
|
-
}
|
|
729
|
-
assertLocalPortFree(profile);
|
|
730
|
-
// Resolve the browser binary at create time. Fails fast with an actionable
|
|
731
|
-
// error ("Comet not installed at /Applications/Comet.app") rather than
|
|
732
|
-
// deferring the failure to the first task. `findBrowserPath` short-circuits
|
|
733
|
-
// for browser=custom without a binary by throwing — same outcome.
|
|
734
|
-
//
|
|
735
|
-
// Skip for SSH profiles: the browser binary lives on the remote host, so a
|
|
736
|
-
// local lookup would validate the wrong machine. The remote launcher resolves
|
|
737
|
-
// it at connect time.
|
|
738
|
-
const nativeArc = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('arc-native:'));
|
|
739
|
-
if (nativeArc && (!profile.arc || profile.browser !== 'arc')) {
|
|
740
|
-
throw new Error('An arc-native profile requires stable Arc Space metadata. Pick a discovered one from `agents browser profiles list`.');
|
|
741
|
-
}
|
|
742
|
-
const firefoxBidi = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('firefox-bidi:'));
|
|
743
|
-
if (firefoxBidi && (!profile.firefox || profile.browser !== 'firefox')) {
|
|
744
|
-
throw new Error('A firefox-bidi profile requires a discovered Firefox profile. Pick one from `agents browser profiles list`.');
|
|
745
|
-
}
|
|
746
|
-
// A discovered profile is pinned to a browser store on this disk
|
|
747
|
-
// (`userDataDir` + `profileDirectory`) and is published unattended by the
|
|
748
|
-
// daemon tick. Its binary is resolved where it is launched (`launchBrowser`),
|
|
749
|
-
// so an app that was removed after its store fails loud at `start` instead of
|
|
750
|
-
// blocking every other discovered row from publishing.
|
|
751
|
-
const storePinned = !!profile.userDataDir && !!profile.profileDirectory;
|
|
752
|
-
if (!hasSshEndpoint(profile.endpoints) && !nativeArc && !storePinned) {
|
|
753
|
-
findBrowserPath(profile.browser, profile.binary);
|
|
754
|
-
}
|
|
755
|
-
const config = profileToConfig(profile);
|
|
756
|
-
updateMeta((meta) => ({
|
|
757
|
-
...meta,
|
|
758
|
-
deviceBrowser: { ...meta.deviceBrowser, [profile.name]: config },
|
|
759
|
-
}));
|
|
760
|
-
}
|
|
761
|
-
export async function updateProfile(profile) {
|
|
762
|
-
const local = localDeclaration(profile.name);
|
|
763
|
-
if (!local) {
|
|
764
|
-
const devices = declaringDevices(profile.name);
|
|
765
|
-
const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
|
|
766
|
-
throw new Error(`Profile "${profile.name}" is not declared on ${machineId()}${where}`);
|
|
767
|
-
}
|
|
768
|
-
const config = profileToConfig(profile);
|
|
769
|
-
updateMeta((meta) => ({
|
|
770
|
-
...meta,
|
|
771
|
-
deviceBrowser: { ...meta.deviceBrowser, [profile.name]: config },
|
|
772
|
-
}));
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Merge `patch` onto the stored profile and persist it in the store it already
|
|
776
|
-
* lives in (see {@link updateProfile} for the scope rules).
|
|
777
|
-
*
|
|
778
|
-
* Runs {@link createProfile}'s validations against the MERGED record, not just
|
|
779
|
-
* the patched fields — a binary edit re-resolves the browser path, a
|
|
780
|
-
* targetFilter edit re-checks the electron gate, and the local-port scan runs
|
|
781
|
-
* with this profile excluded so an unchanged port is not a self-collision.
|
|
782
|
-
*/
|
|
783
|
-
export async function editProfile(name, patch) {
|
|
784
|
-
const local = localDeclaration(name);
|
|
785
|
-
if (!local) {
|
|
786
|
-
const devices = declaringDevices(name);
|
|
787
|
-
const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
|
|
788
|
-
throw new Error(`Profile "${name}" is not declared on ${machineId()}${where}`);
|
|
789
|
-
}
|
|
790
|
-
const devices = declaringDevices(name);
|
|
791
|
-
const current = configToProfile(name, local.config, devices);
|
|
792
|
-
const merged = { ...current, ...patch, name };
|
|
793
|
-
const changed = Object.keys(patch).filter((k) => JSON.stringify(current[k]) !== JSON.stringify(merged[k]));
|
|
794
|
-
// A target filter only means anything for a profile that reuses an existing
|
|
795
|
-
// page target rather than creating one — an Electron app or Arc (which crashes
|
|
796
|
-
// on tab creation, so it drives an open tab / Space, PHNX-2399). The gate must
|
|
797
|
-
// read the MERGED record: `--target-filter x` on a profile that is already
|
|
798
|
-
// electron/arc is valid, and `--no-electron` on a Chromium profile that still
|
|
799
|
-
// carries a filter is not. Checking the patch alone would accept both.
|
|
800
|
-
if (merged.targetFilter && !merged.electron && merged.browser !== 'arc') {
|
|
801
|
-
throw new Error(`--target-filter only applies to an Electron or Arc profile. ` +
|
|
802
|
-
`Pass --electron, use --browser arc, or clear the filter with --target-filter ''.`);
|
|
803
|
-
}
|
|
804
|
-
assertLocalPortFree(merged, { ignore: name });
|
|
805
|
-
// Same rule as create: the binary lives on the remote for an SSH profile, so a
|
|
806
|
-
// local lookup would validate the wrong machine.
|
|
807
|
-
const nativeArc = Object.values(getEndpointPresets(merged)).some((preset) => preset.target.startsWith('arc-native:'));
|
|
808
|
-
if (!hasSshEndpoint(merged.endpoints) && !nativeArc) {
|
|
809
|
-
findBrowserPath(merged.browser, merged.binary);
|
|
810
|
-
}
|
|
811
|
-
await updateProfile(merged);
|
|
812
|
-
return {
|
|
813
|
-
profile: merged,
|
|
814
|
-
devices,
|
|
815
|
-
changed,
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
/** Profile names an agent has to be able to type and an fs path can hold. */
|
|
819
|
-
const PROFILE_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
820
|
-
/**
|
|
821
|
-
* Throw if `name` cannot be used for a NEW profile.
|
|
822
|
-
*
|
|
823
|
-
* Shared by `profiles create` and {@link renameProfile} so the two cannot drift
|
|
824
|
-
* — the shape rule used to live inline in the command, which is how `rename`
|
|
825
|
-
* would have accepted a name `create` rejects.
|
|
826
|
-
*
|
|
827
|
-
* `default` is refused because it is the reserved ALIAS meaning "whatever
|
|
828
|
-
* profile this machine is configured to use" (RUSH-2709). A literal profile by
|
|
829
|
-
* that name makes `--profile default` mean two different things.
|
|
830
|
-
*/
|
|
831
|
-
export function assertRegistrableProfileName(name) {
|
|
832
|
-
if (!PROFILE_NAME_RE.test(name)) {
|
|
833
|
-
throw new Error(`Invalid profile name ${JSON.stringify(name)}. Use lowercase letters, digits and hyphens, ` +
|
|
834
|
-
`starting with a letter — e.g. 'agents'.`);
|
|
835
|
-
}
|
|
836
|
-
if (name === DEFAULT_PROFILE_ALIAS) {
|
|
837
|
-
throw new Error(`"${DEFAULT_PROFILE_ALIAS}" is the reserved alias for this machine's configured profile, ` +
|
|
838
|
-
`not a profile name. Pick another name.`);
|
|
839
|
-
}
|
|
840
|
-
// `browser.viewer: os` means "use the OS default handler". A profile by that
|
|
841
|
-
// name turns the opt-out into a pointer at a profile — silently, since both
|
|
842
|
-
// are just strings in the same key.
|
|
843
|
-
if (name === 'os') {
|
|
844
|
-
throw new Error(`"os" is the reserved browser.viewer value meaning the OS default handler, ` +
|
|
845
|
-
`not a profile name. Pick another name.`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Rename a profile, taking its on-disk state with it.
|
|
850
|
-
*
|
|
851
|
-
* `editProfile` deliberately refuses a name change, and this is why: the name
|
|
852
|
-
* keys the runtime dir ({@link getProfileRuntimeDir}), every fork/endpoint dir
|
|
853
|
-
* derived from it, and the `browser.profile` pointer. Delete-and-recreate — the
|
|
854
|
-
* only route before this — silently abandons the browser's `--user-data-dir`,
|
|
855
|
-
* which is where a profile's logins live. On a real agent browser that is
|
|
856
|
-
* gigabytes of session state and every account it has ever signed into.
|
|
857
|
-
*
|
|
858
|
-
* Refuses when the profile is in use: moving a `--user-data-dir` out from under
|
|
859
|
-
* a running browser corrupts it.
|
|
860
|
-
*/
|
|
861
|
-
export async function renameProfile(from, to) {
|
|
862
|
-
if (from === to)
|
|
863
|
-
throw new Error(`"${from}" is already its own name.`);
|
|
864
|
-
const local = localDeclaration(from);
|
|
865
|
-
if (!local) {
|
|
866
|
-
const devices = declaringDevices(from);
|
|
867
|
-
const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
|
|
868
|
-
throw new Error(`Profile "${from}" is not declared on ${machineId()}${where}`);
|
|
869
|
-
}
|
|
870
|
-
if (profileRegistry().has(to))
|
|
871
|
-
throw new Error(`Profile "${to}" already exists`);
|
|
872
|
-
assertRegistrableProfileName(to);
|
|
873
|
-
const { isProfileInUse, listProfileCacheDirs } = await import('./runtime-state.js');
|
|
874
|
-
if (isProfileInUse(from)) {
|
|
875
|
-
throw new Error(`"${from}" is in use (live browser, tunnel, or open task). Renaming would move its ` +
|
|
876
|
-
`user-data-dir out from under the running browser. Stop it first: agents browser stop --profile ${from}`);
|
|
877
|
-
}
|
|
878
|
-
const config = local.config;
|
|
879
|
-
const devices = declaringDevices(from);
|
|
880
|
-
// Move the on-disk state BEFORE the config, so a crash between the two leaves
|
|
881
|
-
// a profile whose dirs are already where the new name expects them rather than
|
|
882
|
-
// a config pointing at dirs that no longer exist.
|
|
883
|
-
// PRE-FLIGHT every destination before moving ANY of them. With the check
|
|
884
|
-
// inside the loop, dir N was validated only after dirs 0..N-1 had already
|
|
885
|
-
// moved: a collision on the second endpoint left the first one's logins
|
|
886
|
-
// stranded under a name with no config entry, and the error named only the
|
|
887
|
-
// squatter. Nothing repaired that, and the user was never told.
|
|
888
|
-
const plan = [];
|
|
889
|
-
for (const dir of listProfileCacheDirs(from)) {
|
|
890
|
-
const base = path.basename(dir);
|
|
891
|
-
const suffix = base.slice(from.length);
|
|
892
|
-
const dest = path.join(path.dirname(dir), `${to}${suffix}`);
|
|
893
|
-
if (fs.existsSync(dest)) {
|
|
894
|
-
throw new Error(`Cannot rename: ${dest} already exists. Nothing was moved. Remove or rename it first.`);
|
|
895
|
-
}
|
|
896
|
-
plan.push({ dir, dest });
|
|
897
|
-
}
|
|
898
|
-
const movedDirs = [];
|
|
899
|
-
for (const { dir, dest } of plan) {
|
|
900
|
-
fs.renameSync(dir, dest);
|
|
901
|
-
movedDirs.push(dest);
|
|
902
|
-
}
|
|
903
|
-
updateMeta((meta) => {
|
|
904
|
-
const next = { ...meta.deviceBrowser };
|
|
905
|
-
delete next[from];
|
|
906
|
-
return { ...meta, deviceBrowser: { ...next, [to]: config } };
|
|
907
|
-
});
|
|
908
|
-
// Both pointers are separate keys. Leaving `browser.profile` behind falls back
|
|
909
|
-
// to auto-detect on the next `browser start`; leaving `browser.viewer` behind
|
|
910
|
-
// sends every artifact back to the OS default handler — which is the exact bug
|
|
911
|
-
// the viewer seam was built to fix, reintroduced by a rename.
|
|
912
|
-
const { setConfigValue, getConfigValue } = await import('../device-config.js');
|
|
913
|
-
let repointedDefault = false;
|
|
914
|
-
if (getConfiguredDefaultProfileName() === from) {
|
|
915
|
-
setConfigValue('browser.profile', to);
|
|
916
|
-
repointedDefault = true;
|
|
917
|
-
}
|
|
918
|
-
let repointedViewer = false;
|
|
919
|
-
if (getConfigValue('browser.viewer').value === from) {
|
|
920
|
-
setConfigValue('browser.viewer', to);
|
|
921
|
-
repointedViewer = true;
|
|
922
|
-
}
|
|
923
|
-
// A rename changes only this device's declaration. If it was the only
|
|
924
|
-
// declaration, peer pins become stale and are reported rather than rewritten.
|
|
925
|
-
const { devicesPinningBrowserProfile } = await import('../device-config.js');
|
|
926
|
-
const stalePins = devices.length === 1
|
|
927
|
-
? devicesPinningBrowserProfile(from).filter((p) => p.device !== machineId())
|
|
928
|
-
: [];
|
|
929
|
-
return { devices, movedDirs, repointedDefault, repointedViewer, stalePins };
|
|
930
|
-
}
|
|
931
|
-
export async function deleteProfile(name) {
|
|
932
|
-
if (!localDeclaration(name)) {
|
|
933
|
-
const devices = declaringDevices(name);
|
|
934
|
-
const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
|
|
935
|
-
throw new Error(`Profile "${name}" is not declared on ${machineId()}${where}`);
|
|
936
|
-
}
|
|
937
|
-
updateMeta((meta) => {
|
|
938
|
-
const next = { ...meta.deviceBrowser };
|
|
939
|
-
delete next[name];
|
|
940
|
-
return { ...meta, deviceBrowser: Object.keys(next).length > 0 ? next : undefined };
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
/** Widest NAME column we will grow to before truncating instead. */
|
|
944
|
-
const MAX_NAME_COLUMN = 28;
|
|
945
|
-
/**
|
|
946
|
-
* Pad `text` to exactly `width` visible characters, truncating with an ellipsis
|
|
947
|
-
* when it does not fit. `padEnd` alone does NOT bound a long value — it returns
|
|
948
|
-
* the string unchanged when it already exceeds the width, which is what pushed
|
|
949
|
-
* every later column out of alignment on a name longer than 20 characters
|
|
950
|
-
* (RUSH-2710). Every column here goes through this, never a bare `padEnd`.
|
|
951
|
-
*/
|
|
952
|
-
export function padColumn(text, width) {
|
|
953
|
-
if (width <= 0)
|
|
954
|
-
return '';
|
|
955
|
-
if (text.length <= width)
|
|
956
|
-
return text.padEnd(width);
|
|
957
|
-
if (width === 1)
|
|
958
|
-
return '…';
|
|
959
|
-
return text.slice(0, width - 1) + '…';
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Render the `agents browser profiles list` table.
|
|
963
|
-
*
|
|
964
|
-
* Pure (no fs, no config reads) so the alignment and the default-marking rules
|
|
965
|
-
* are unit-testable without a configured machine.
|
|
966
|
-
*
|
|
967
|
-
* Two things used to be called "default" in this listing with no way to tell
|
|
968
|
-
* them apart (RUSH-2710): the profile literally NAMED `default` (the
|
|
969
|
-
* auto-detected one), and whichever profile this machine resolves a bare
|
|
970
|
-
* `agents browser start` to (`agents browser use <name>`). They
|
|
971
|
-
* are frequently different profiles. Now only the second is a marker — a `*` in
|
|
972
|
-
* a leading column, explained by a legend line — so the name column carries just
|
|
973
|
-
* the name and `default` in it always means the profile of that name.
|
|
974
|
-
*
|
|
975
|
-
* WHERE lists the devices whose own files declare the name.
|
|
976
|
-
*/
|
|
977
|
-
export function formatProfilesTable(rows, configuredDefault) {
|
|
978
|
-
const cells = rows.map((profile) => {
|
|
979
|
-
const presets = getEndpointPresets(profile);
|
|
980
|
-
const endpoints = Object.entries(presets)
|
|
981
|
-
.map(([name, ep]) => (name.startsWith('endpoint-') ? ep.target : `${name}=${ep.target}`))
|
|
982
|
-
.join(', ');
|
|
983
|
-
return {
|
|
984
|
-
marker: profile.name === configuredDefault ? '*' : ' ',
|
|
985
|
-
name: profile.name,
|
|
986
|
-
browser: profile.browser || '-',
|
|
987
|
-
where: profile.devices.join(', '),
|
|
988
|
-
description: profile.description ?? '',
|
|
989
|
-
endpoints,
|
|
990
|
-
};
|
|
991
|
-
});
|
|
992
|
-
const nameWidth = Math.min(MAX_NAME_COLUMN, Math.max('NAME'.length, ...cells.map((c) => c.name.length)));
|
|
993
|
-
const browserWidth = Math.max('BROWSER'.length, ...cells.map((c) => c.browser.length));
|
|
994
|
-
const whereWidth = Math.min(36, Math.max('WHERE'.length, ...cells.map((c) => c.where.length)));
|
|
995
|
-
const hasDescriptions = cells.some((c) => c.description);
|
|
996
|
-
const descWidth = hasDescriptions
|
|
997
|
-
? Math.min(36, Math.max('DESCRIPTION'.length, ...cells.map((c) => c.description.length)))
|
|
998
|
-
: 0;
|
|
999
|
-
const row = (marker, name, browser, where, desc, endpoints) => `${marker} ` +
|
|
1000
|
-
padColumn(name, nameWidth) + ' ' +
|
|
1001
|
-
padColumn(browser, browserWidth) + ' ' +
|
|
1002
|
-
padColumn(where, whereWidth) + ' ' +
|
|
1003
|
-
(hasDescriptions ? padColumn(desc, descWidth) + ' ' : '') +
|
|
1004
|
-
endpoints;
|
|
1005
|
-
const header = row(' ', 'NAME', 'BROWSER', 'WHERE', 'DESCRIPTION', 'ENDPOINTS');
|
|
1006
|
-
const lines = [header, '-'.repeat(header.length)];
|
|
1007
|
-
for (const c of cells) {
|
|
1008
|
-
lines.push(row(c.marker, c.name, c.browser, c.where, c.description, c.endpoints));
|
|
1009
|
-
}
|
|
1010
|
-
if (configuredDefault) {
|
|
1011
|
-
lines.push('');
|
|
1012
|
-
lines.push(`* = this machine's default profile (${configuredDefault}) — what a bare \`agents browser start\` uses.`);
|
|
1013
|
-
}
|
|
1014
|
-
return lines;
|
|
1015
|
-
}
|
|
1016
|
-
/**
|
|
1017
|
-
* Resolve a profile's endpoint presets into a normalized map regardless of
|
|
1018
|
-
* whether the YAML uses the legacy `string[]` shape or the new map shape.
|
|
1019
|
-
* The legacy entries get auto-named `endpoint-0`, `endpoint-1`, ... .
|
|
1020
|
-
*/
|
|
1021
|
-
export function getEndpointPresets(profile) {
|
|
1022
|
-
if (Array.isArray(profile.endpoints)) {
|
|
1023
|
-
const out = {};
|
|
1024
|
-
profile.endpoints.forEach((target, i) => {
|
|
1025
|
-
out[`endpoint-${i}`] = { target };
|
|
1026
|
-
});
|
|
1027
|
-
return out;
|
|
1028
|
-
}
|
|
1029
|
-
return profile.endpoints;
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Pick the endpoint preset to use. Order:
|
|
1033
|
-
* 1. Explicit name passed in (errors if unknown)
|
|
1034
|
-
* 2. `profile.defaultEndpoint` if set
|
|
1035
|
-
* 3. First entry (preserves legacy string[] behavior)
|
|
1036
|
-
*
|
|
1037
|
-
* Returns the resolved name + the preset (with per-endpoint overrides
|
|
1038
|
-
* already applied to binary / targetFilter), so callers don't have to
|
|
1039
|
-
* remember the precedence rules.
|
|
1040
|
-
*/
|
|
1041
|
-
export function resolveEndpoint(profile, endpointName) {
|
|
1042
|
-
const presets = getEndpointPresets(profile);
|
|
1043
|
-
const names = Object.keys(presets);
|
|
1044
|
-
if (names.length === 0) {
|
|
1045
|
-
throw new Error(`Profile "${profile.name}" has no endpoints configured`);
|
|
1046
|
-
}
|
|
1047
|
-
let chosenName;
|
|
1048
|
-
if (endpointName) {
|
|
1049
|
-
if (!presets[endpointName]) {
|
|
1050
|
-
throw new Error(`Endpoint "${endpointName}" not found on profile "${profile.name}". ` +
|
|
1051
|
-
`Available: ${names.join(', ')}`);
|
|
1052
|
-
}
|
|
1053
|
-
chosenName = endpointName;
|
|
1054
|
-
}
|
|
1055
|
-
else if (profile.defaultEndpoint && presets[profile.defaultEndpoint]) {
|
|
1056
|
-
chosenName = profile.defaultEndpoint;
|
|
1057
|
-
}
|
|
1058
|
-
else {
|
|
1059
|
-
chosenName = names[0];
|
|
1060
|
-
}
|
|
1061
|
-
const preset = presets[chosenName];
|
|
1062
|
-
return {
|
|
1063
|
-
name: chosenName,
|
|
1064
|
-
target: preset.target,
|
|
1065
|
-
binary: preset.binary ?? profile.binary,
|
|
1066
|
-
targetFilter: preset.targetFilter ?? profile.targetFilter,
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Extract the (host, port) pair intended by the profile's default endpoint.
|
|
1071
|
-
* Returns undefined for endpoint shapes that don't carry a port (e.g. ws:// without one).
|
|
1072
|
-
*
|
|
1073
|
-
* Ports are scoped by host: a `cdp://127.0.0.1:9222` profile (local Chrome on
|
|
1074
|
-
* this machine) and an `ssh://remote-host:9222` profile (Comet on a remote
|
|
1075
|
-
* host) point at different physical ports — the host disambiguates them.
|
|
1076
|
-
*
|
|
1077
|
-
* Accepts both `scheme://host:port` and `scheme://host?port=N` shapes (the
|
|
1078
|
-
* latter is the documented form in `types.ts` for `ssh://`). Without this,
|
|
1079
|
-
* `ssh://remote-host?port=18805` would silently fall back to 9222 and every
|
|
1080
|
-
* `?port=`-style SSH profile would collide on creation.
|
|
1081
|
-
*/
|
|
1082
|
-
export function extractConfiguredEndpoint(profile) {
|
|
1083
|
-
const presets = getEndpointPresets(profile);
|
|
1084
|
-
const firstName = profile.defaultEndpoint && presets[profile.defaultEndpoint]
|
|
1085
|
-
? profile.defaultEndpoint
|
|
1086
|
-
: Object.keys(presets)[0];
|
|
1087
|
-
if (!firstName)
|
|
1088
|
-
return undefined;
|
|
1089
|
-
return parseEndpointUrl(presets[firstName].target);
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Shared endpoint parser used by both the collision-detection code path and
|
|
1093
|
-
* the connection drivers. Returning a single normalized `(host, port)` here
|
|
1094
|
-
* keeps `extractConfiguredEndpoint` and the SSH driver from drifting on URL
|
|
1095
|
-
* conventions (which is how `?port=N` ended up being silently ignored).
|
|
1096
|
-
*/
|
|
1097
|
-
export function parseEndpointUrl(endpoint) {
|
|
1098
|
-
let url;
|
|
1099
|
-
try {
|
|
1100
|
-
url = new URL(endpoint);
|
|
1101
|
-
}
|
|
1102
|
-
catch {
|
|
1103
|
-
return undefined;
|
|
1104
|
-
}
|
|
1105
|
-
const host = normalizeHost(url.hostname, url.protocol);
|
|
1106
|
-
if (!host)
|
|
1107
|
-
return undefined;
|
|
1108
|
-
const port = extractPortFromUrl(url);
|
|
1109
|
-
if (port !== undefined)
|
|
1110
|
-
return { host, port };
|
|
1111
|
-
// SSH endpoints tunnel to a remote port AND bind that same port locally,
|
|
1112
|
-
// so they do "own" a local port — the host-scoped collision check used
|
|
1113
|
-
// to disagree, but we want the local-port-scoped semantics now.
|
|
1114
|
-
if (url.protocol === 'cdp:' || url.protocol === 'ssh:')
|
|
1115
|
-
return { host, port: 9222 };
|
|
1116
|
-
return undefined;
|
|
1117
|
-
}
|
|
1118
|
-
function extractPortFromUrl(url) {
|
|
1119
|
-
if (url.port) {
|
|
1120
|
-
const n = parseInt(url.port, 10);
|
|
1121
|
-
if (Number.isFinite(n))
|
|
1122
|
-
return n;
|
|
1123
|
-
}
|
|
1124
|
-
// `scheme://host?port=N` — the form documented for SSH endpoints in
|
|
1125
|
-
// `types.ts`. WHATWG URL parsing surfaces it via searchParams only.
|
|
1126
|
-
const qp = url.searchParams.get('port');
|
|
1127
|
-
if (qp) {
|
|
1128
|
-
const n = parseInt(qp, 10);
|
|
1129
|
-
if (Number.isFinite(n))
|
|
1130
|
-
return n;
|
|
1131
|
-
}
|
|
1132
|
-
return undefined;
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Extract the port intended by the profile's default endpoint.
|
|
1136
|
-
* Returns undefined for endpoint shapes that don't carry a port (e.g. ws:// without one).
|
|
1137
|
-
*
|
|
1138
|
-
* Note: this loses the host dimension — for collision detection use
|
|
1139
|
-
* `extractConfiguredEndpoint` instead, which returns the (host, port) pair.
|
|
1140
|
-
*/
|
|
1141
|
-
export function extractConfiguredPort(profile) {
|
|
1142
|
-
return extractConfiguredEndpoint(profile)?.port;
|
|
1143
|
-
}
|
|
1144
|
-
function normalizeHost(hostname, protocol) {
|
|
1145
|
-
if (!hostname) {
|
|
1146
|
-
// cdp:// and ssh:// without an explicit host imply localhost.
|
|
1147
|
-
if (protocol === 'cdp:' || protocol === 'ssh:')
|
|
1148
|
-
return '127.0.0.1';
|
|
1149
|
-
return undefined;
|
|
1150
|
-
}
|
|
1151
|
-
if (hostname === 'localhost')
|
|
1152
|
-
return '127.0.0.1';
|
|
1153
|
-
return hostname;
|
|
1154
|
-
}
|
|
1155
|
-
export function isLocalHost(host) {
|
|
1156
|
-
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
1157
|
-
}
|