@phnx-labs/agents-cli 1.22.93 → 1.22.95
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 +20 -0
- package/dist/commands/browser.d.ts +1 -0
- package/dist/commands/browser.js +156 -11
- package/dist/lib/browser/arc-discovery.d.ts +60 -0
- package/dist/lib/browser/arc-discovery.js +187 -0
- package/dist/lib/browser/arc-dom.d.ts +14 -0
- package/dist/lib/browser/arc-dom.js +121 -0
- package/dist/lib/browser/chrome.d.ts +17 -0
- package/dist/lib/browser/chrome.js +121 -16
- package/dist/lib/browser/chromium-discovery.d.ts +27 -0
- package/dist/lib/browser/chromium-discovery.js +96 -0
- package/dist/lib/browser/drivers/arc.d.ts +57 -0
- package/dist/lib/browser/drivers/arc.js +281 -0
- package/dist/lib/browser/drivers/firefox.d.ts +103 -0
- package/dist/lib/browser/drivers/firefox.js +377 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +38 -3
- package/dist/lib/browser/firefox-discovery.d.ts +68 -0
- package/dist/lib/browser/firefox-discovery.js +162 -0
- package/dist/lib/browser/profiles.d.ts +39 -1
- package/dist/lib/browser/profiles.js +239 -9
- package/dist/lib/browser/refs.d.ts +2 -0
- package/dist/lib/browser/refs.js +2 -1
- package/dist/lib/browser/resolve-target.d.ts +2 -0
- package/dist/lib/browser/resolve-target.js +14 -0
- package/dist/lib/browser/runtime-state.js +9 -1
- package/dist/lib/browser/service.d.ts +86 -3
- package/dist/lib/browser/service.js +1045 -42
- package/dist/lib/browser/types.d.ts +85 -2
- package/dist/lib/daemon/usage-sync-service.js +12 -0
- package/dist/lib/open-url.js +6 -0
- package/dist/lib/types.d.ts +20 -1
- package/package.json +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Firefox profile discovery from `profiles.ini` (PHNX-4043).
|
|
3
|
+
*
|
|
4
|
+
* One agents-cli profile per `[ProfileN]` entry, named `firefox-<name-slug>`
|
|
5
|
+
* and pinned to that entry's directory. The profile already carries its
|
|
6
|
+
* cookies and logins, so it IS the browser profile agents pick with
|
|
7
|
+
* `--profile` — the same shape Arc Space discovery uses (`arc-discovery.ts`).
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
/**
|
|
13
|
+
* Every directory a `profiles.ini` may live in on this machine. Ubuntu's snap
|
|
14
|
+
* Firefox keeps its whole `.mozilla` under `~/snap/firefox/common/`, a deb or
|
|
15
|
+
* tarball Firefox uses `~/.mozilla/firefox/` — a box can carry both, so both
|
|
16
|
+
* are read. `AGENTS_FIREFOX_DIRS` (path-separator list) points tests and
|
|
17
|
+
* non-default installs elsewhere.
|
|
18
|
+
*/
|
|
19
|
+
export function firefoxProfileRoots() {
|
|
20
|
+
const override = process.env.AGENTS_FIREFOX_DIRS;
|
|
21
|
+
if (override)
|
|
22
|
+
return override.split(path.delimiter).filter(Boolean).map((p) => path.resolve(p));
|
|
23
|
+
const home = os.homedir();
|
|
24
|
+
switch (process.platform) {
|
|
25
|
+
case 'darwin':
|
|
26
|
+
return [path.join(home, 'Library', 'Application Support', 'Firefox')];
|
|
27
|
+
case 'win32':
|
|
28
|
+
return [path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Mozilla', 'Firefox')];
|
|
29
|
+
default:
|
|
30
|
+
return [
|
|
31
|
+
path.join(home, '.mozilla', 'firefox'),
|
|
32
|
+
path.join(home, 'snap', 'firefox', 'common', '.mozilla', 'firefox'),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse one `profiles.ini`. Only `[Profile<n>]` sections are profiles;
|
|
38
|
+
* `[General]` and `[Install<hash>]` (which record the per-install default) are
|
|
39
|
+
* skipped. A `[Profile]` section that lacks `Name` or `Path` is a malformed
|
|
40
|
+
* file, not a profile to guess at.
|
|
41
|
+
*/
|
|
42
|
+
export function parseProfilesIni(iniPath) {
|
|
43
|
+
let text;
|
|
44
|
+
try {
|
|
45
|
+
text = fs.readFileSync(iniPath, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return { error: `Cannot read ${iniPath}: ${error instanceof Error ? error.message : String(error)}` };
|
|
49
|
+
}
|
|
50
|
+
const root = path.dirname(iniPath);
|
|
51
|
+
const sections = [];
|
|
52
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
53
|
+
const line = raw.trim();
|
|
54
|
+
if (!line || line.startsWith(';') || line.startsWith('#'))
|
|
55
|
+
continue;
|
|
56
|
+
const header = line.match(/^\[(.+)\]$/);
|
|
57
|
+
if (header) {
|
|
58
|
+
sections.push({ header: header[1], values: {} });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const eq = line.indexOf('=');
|
|
62
|
+
if (eq === -1 || sections.length === 0) {
|
|
63
|
+
return { error: `${iniPath} has a malformed line: ${JSON.stringify(raw)}` };
|
|
64
|
+
}
|
|
65
|
+
sections[sections.length - 1].values[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
66
|
+
}
|
|
67
|
+
const profiles = [];
|
|
68
|
+
for (const section of sections) {
|
|
69
|
+
if (!/^Profile\d+$/.test(section.header))
|
|
70
|
+
continue;
|
|
71
|
+
const name = section.values.Name;
|
|
72
|
+
const rel = section.values.Path;
|
|
73
|
+
if (!name || !rel) {
|
|
74
|
+
return { error: `${iniPath} [${section.header}] has no Name or Path` };
|
|
75
|
+
}
|
|
76
|
+
const isRelative = section.values.IsRelative !== '0';
|
|
77
|
+
const dir = isRelative ? path.resolve(root, rel) : path.resolve(rel);
|
|
78
|
+
profiles.push({ name, dir, isDefault: section.values.Default === '1', iniPath });
|
|
79
|
+
}
|
|
80
|
+
return profiles;
|
|
81
|
+
}
|
|
82
|
+
export function discoverFirefoxProfilesAt(roots) {
|
|
83
|
+
const iniPaths = roots.map((root) => path.join(root, 'profiles.ini')).filter((p) => fs.existsSync(p));
|
|
84
|
+
if (iniPaths.length === 0) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
kind: 'not-installed',
|
|
88
|
+
reason: `No Firefox profiles.ini under ${roots.join(', ')}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const profiles = [];
|
|
92
|
+
for (const iniPath of iniPaths) {
|
|
93
|
+
const parsed = parseProfilesIni(iniPath);
|
|
94
|
+
if ('error' in parsed)
|
|
95
|
+
return { ok: false, kind: 'invalid', reason: parsed.error };
|
|
96
|
+
profiles.push(...parsed);
|
|
97
|
+
}
|
|
98
|
+
if (profiles.length === 0) {
|
|
99
|
+
return { ok: false, kind: 'invalid', reason: `No [Profile] entries in ${iniPaths.join(', ')}` };
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, profiles, iniPaths };
|
|
102
|
+
}
|
|
103
|
+
export function discoverFirefoxProfiles() {
|
|
104
|
+
return discoverFirefoxProfilesAt(firefoxProfileRoots());
|
|
105
|
+
}
|
|
106
|
+
function slugify(value) {
|
|
107
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
108
|
+
}
|
|
109
|
+
/** FNV-1a over the path, folded into the range. Stable across runs and daemons. */
|
|
110
|
+
function stableHash(value) {
|
|
111
|
+
let hash = 0x811c9dc5;
|
|
112
|
+
for (let i = 0; i < value.length; i++) {
|
|
113
|
+
hash ^= value.charCodeAt(i);
|
|
114
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
115
|
+
}
|
|
116
|
+
return hash;
|
|
117
|
+
}
|
|
118
|
+
/** Firefox BiDi ports live in their own range so they never collide with the 9222-9399 CDP profiles. */
|
|
119
|
+
export const FIREFOX_PORT_RANGE = Object.freeze({ base: 9600, size: 100 });
|
|
120
|
+
/**
|
|
121
|
+
* The port a discovered Firefox profile listens on: derived from its directory,
|
|
122
|
+
* so the relaunch hint in an attach error is the same string every time and a
|
|
123
|
+
* daemon restart reattaches to the Firefox it launched earlier. Collisions
|
|
124
|
+
* between two discovered profiles resolve by linear probing within the range.
|
|
125
|
+
*/
|
|
126
|
+
export function firefoxProfilePort(profileDir, taken = new Set()) {
|
|
127
|
+
const { base, size } = FIREFOX_PORT_RANGE;
|
|
128
|
+
let port = base + (stableHash(profileDir) % size);
|
|
129
|
+
for (let i = 0; i < size && taken.has(port); i++) {
|
|
130
|
+
port = base + ((port - base + 1) % size);
|
|
131
|
+
}
|
|
132
|
+
return port;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Flatten discovery into one agents-cli profile per `profiles.ini` entry, named
|
|
136
|
+
* `firefox-<name>` (`firefox-default`, `firefox-default-release`, …). Two
|
|
137
|
+
* entries with the same name (one per ini, say) get the first six hex
|
|
138
|
+
* characters of their directory hash appended so the names stay distinct.
|
|
139
|
+
*/
|
|
140
|
+
export function firefoxDiscoveredProfiles(result) {
|
|
141
|
+
if (!result.ok)
|
|
142
|
+
return [];
|
|
143
|
+
const taken = new Set();
|
|
144
|
+
const flat = result.profiles.map((profile) => {
|
|
145
|
+
const port = firefoxProfilePort(profile.dir, taken);
|
|
146
|
+
taken.add(port);
|
|
147
|
+
return {
|
|
148
|
+
name: `firefox-${slugify(profile.name) || 'profile'}`,
|
|
149
|
+
profileName: profile.name,
|
|
150
|
+
profileDir: profile.dir,
|
|
151
|
+
iniPath: profile.iniPath,
|
|
152
|
+
isDefault: profile.isDefault,
|
|
153
|
+
port,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
const counts = new Map();
|
|
157
|
+
for (const entry of flat)
|
|
158
|
+
counts.set(entry.name, (counts.get(entry.name) ?? 0) + 1);
|
|
159
|
+
return flat.map((entry) => (counts.get(entry.name) ?? 0) > 1
|
|
160
|
+
? { ...entry, name: `${entry.name}-${stableHash(entry.profileDir).toString(16).padStart(8, '0').slice(0, 6)}` }
|
|
161
|
+
: entry);
|
|
162
|
+
}
|
|
@@ -85,8 +85,46 @@ export declare function getProfileDownloadsDir(name: string): string;
|
|
|
85
85
|
* one-shot migration in migrate.ts folds old captures into here.
|
|
86
86
|
*/
|
|
87
87
|
export declare function getProfileSessionsDir(name: string, task: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Every browser profile this Mac already has, as agents-cli profiles:
|
|
90
|
+
*
|
|
91
|
+
* - each Arc Space (PHNX-2399): a Space carries its Arc profile's cookies and
|
|
92
|
+
* logins, so it IS the browser profile agents pick with `--profile`;
|
|
93
|
+
* - each Comet profile from Comet's own `Local State` (PHNX-4042): pinned to
|
|
94
|
+
* Comet's user-data dir and profile directory, so the window agents open is
|
|
95
|
+
* the owner's window and a sign-in done once serves both.
|
|
96
|
+
*
|
|
97
|
+
* No second concept: agents see one `--profile` list. Discovery reads files
|
|
98
|
+
* the browsers rewrite on every change, so a torn read is an ordinary event,
|
|
99
|
+
* not corruption. It is reported as `errors` rather than thrown: the profile
|
|
100
|
+
* store serves every browser on this machine, and a momentary Arc read must
|
|
101
|
+
* never take the Comet or Chrome rows down with it. An error surfaces where a
|
|
102
|
+
* profile of that browser is actually asked for by name.
|
|
103
|
+
*/
|
|
104
|
+
export interface DiscoveredProfiles {
|
|
105
|
+
profiles: BrowserProfileWithDeclarations[];
|
|
106
|
+
/** Per-browser discovery failures, keyed by the profile-name prefix (`arc`, `comet`). */
|
|
107
|
+
errors: Record<string, string>;
|
|
108
|
+
}
|
|
109
|
+
export declare function discoverNativeProfiles(): DiscoveredProfiles;
|
|
110
|
+
/**
|
|
111
|
+
* Publish every discovered native profile that this device has not declared
|
|
112
|
+
* yet into its device declaration (`~/.agents/devices/<device>/agents.yaml`),
|
|
113
|
+
* which the fleet shared-store sync carries to every other box (PHNX-4042).
|
|
114
|
+
* That is what lets `agents browser profiles list` on a worker show this
|
|
115
|
+
* Mac's `arc-*` and `comet-*` rows with WHERE=<this device>, and a start from
|
|
116
|
+
* there route here. Idempotent; a discovery error skips that browser.
|
|
117
|
+
*/
|
|
118
|
+
export declare function publishDiscoveredProfiles(): Promise<{
|
|
119
|
+
published: string[];
|
|
120
|
+
errors: Record<string, string>;
|
|
121
|
+
}>;
|
|
122
|
+
/** Whether this name has persisted agents-cli metadata on the current device. */
|
|
123
|
+
export declare function isProfileDeclaredHere(name: string): boolean;
|
|
88
124
|
export declare function listProfiles(): Promise<BrowserProfileWithDeclarations[]>;
|
|
89
125
|
export declare function getProfile(name: string): Promise<BrowserProfileWithDeclarations | null>;
|
|
126
|
+
/** Persist only the agents-cli alias for an auto-discovered native profile; never the browser's own data. */
|
|
127
|
+
export declare function persistDiscoveredProfile(name: string): Promise<void>;
|
|
90
128
|
/**
|
|
91
129
|
* True when `profile` can actually launch or attach on THIS machine.
|
|
92
130
|
*
|
|
@@ -245,7 +283,7 @@ export declare function updateProfile(profile: BrowserProfile): Promise<void>;
|
|
|
245
283
|
* ({@link getProfileRuntimeDir}) and any live `<name>@<endpoint>` connection, so
|
|
246
284
|
* changing either orphans the cached browser data. Delete and recreate instead.
|
|
247
285
|
*/
|
|
248
|
-
export type EditableProfileFields = Partial<Pick<BrowserProfile, 'description' | 'binary' | 'electron' | 'targetFilter' | 'endpoints' | 'launchPolicy' | 'userDataDir' | 'chrome' | 'secrets' | 'viewport'>>;
|
|
286
|
+
export type EditableProfileFields = Partial<Pick<BrowserProfile, 'description' | 'binary' | 'electron' | 'targetFilter' | 'endpoints' | 'launchPolicy' | 'userDataDir' | 'chrome' | 'secrets' | 'viewport' | 'arc'>>;
|
|
249
287
|
export interface EditProfileResult {
|
|
250
288
|
profile: BrowserProfile;
|
|
251
289
|
devices: string[];
|
|
@@ -5,6 +5,9 @@ import { getConfigValue } from '../device-config.js';
|
|
|
5
5
|
import { machineId } from '../machine-id.js';
|
|
6
6
|
import { declaringDevices, profileRegistry, } from './registry.js';
|
|
7
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';
|
|
8
11
|
export { declaringDevices, profileKind, profileRegistry, } from './registry.js';
|
|
9
12
|
/**
|
|
10
13
|
* Name of the profile the setup wizards pin as this machine's default browser
|
|
@@ -124,11 +127,14 @@ function configToProfile(name, config, devices = []) {
|
|
|
124
127
|
defaultEndpoint: config.defaultEndpoint,
|
|
125
128
|
launchPolicy: config.launchPolicy,
|
|
126
129
|
userDataDir: config.userDataDir,
|
|
130
|
+
profileDirectory: config.profileDirectory,
|
|
127
131
|
chrome: config.chrome,
|
|
128
132
|
secrets: config.secrets,
|
|
129
133
|
viewport: config.viewport,
|
|
130
134
|
logDir: config.logDir,
|
|
131
135
|
logHost: config.logHost,
|
|
136
|
+
arc: config.arc,
|
|
137
|
+
firefox: config.firefox,
|
|
132
138
|
devices,
|
|
133
139
|
};
|
|
134
140
|
}
|
|
@@ -152,6 +158,8 @@ function profileToConfig(profile) {
|
|
|
152
158
|
config.launchPolicy = profile.launchPolicy;
|
|
153
159
|
if (profile.userDataDir)
|
|
154
160
|
config.userDataDir = profile.userDataDir;
|
|
161
|
+
if (profile.profileDirectory)
|
|
162
|
+
config.profileDirectory = profile.profileDirectory;
|
|
155
163
|
if (profile.chrome)
|
|
156
164
|
config.chrome = profile.chrome;
|
|
157
165
|
if (profile.secrets)
|
|
@@ -162,26 +170,214 @@ function profileToConfig(profile) {
|
|
|
162
170
|
config.logDir = profile.logDir;
|
|
163
171
|
if (profile.logHost)
|
|
164
172
|
config.logHost = profile.logHost;
|
|
173
|
+
if (profile.arc)
|
|
174
|
+
config.arc = profile.arc;
|
|
175
|
+
if (profile.firefox)
|
|
176
|
+
config.firefox = profile.firefox;
|
|
165
177
|
return config;
|
|
166
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
|
+
}
|
|
167
340
|
function selectedDeclaration(declarations) {
|
|
168
341
|
return declarations.find((declaration) => declaration.device === machineId()) ?? declarations[0];
|
|
169
342
|
}
|
|
170
343
|
function localDeclaration(name) {
|
|
171
344
|
return profileRegistry().get(name)?.find((declaration) => declaration.device === machineId());
|
|
172
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
|
+
}
|
|
173
350
|
export async function listProfiles() {
|
|
174
|
-
|
|
351
|
+
const native = discoverNativeProfiles();
|
|
352
|
+
const configured = [...profileRegistry()].map(([name, declarations]) => {
|
|
175
353
|
const selected = selectedDeclaration(declarations);
|
|
176
|
-
return configToProfile(name, selected.config, declarations.map((declaration) => declaration.device));
|
|
354
|
+
return refreshDiscoveredProfile(configToProfile(name, selected.config, declarations.map((declaration) => declaration.device)), native.profiles);
|
|
177
355
|
});
|
|
356
|
+
const names = new Set(configured.map((profile) => profile.name));
|
|
357
|
+
return [...configured, ...native.profiles.filter((profile) => !names.has(profile.name))];
|
|
178
358
|
}
|
|
179
359
|
export async function getProfile(name) {
|
|
360
|
+
const native = discoverNativeProfiles();
|
|
180
361
|
const declarations = profileRegistry().get(name);
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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);
|
|
185
381
|
}
|
|
186
382
|
/**
|
|
187
383
|
* True when `profile` can actually launch or attach on THIS machine.
|
|
@@ -194,6 +390,25 @@ export async function getProfile(name) {
|
|
|
194
390
|
* for the current machine.
|
|
195
391
|
*/
|
|
196
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
|
+
}
|
|
197
412
|
const remote = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('ssh://'));
|
|
198
413
|
if (remote)
|
|
199
414
|
return true;
|
|
@@ -334,7 +549,7 @@ export async function ensureDefaultBrowserProfile() {
|
|
|
334
549
|
const configured = getConfiguredDefaultProfileName();
|
|
335
550
|
if (configured && configured !== DEFAULT_PROFILE_ALIAS) {
|
|
336
551
|
const chosen = await getProfile(configured);
|
|
337
|
-
if (chosen && isProfileLaunchableHere(chosen))
|
|
552
|
+
if (chosen && (!chosen.devices.includes(machineId()) || isProfileLaunchableHere(chosen)))
|
|
338
553
|
return chosen;
|
|
339
554
|
if (!chosen) {
|
|
340
555
|
const central = readMeta().browser ?? {};
|
|
@@ -520,7 +735,21 @@ export async function createProfile(profile) {
|
|
|
520
735
|
// Skip for SSH profiles: the browser binary lives on the remote host, so a
|
|
521
736
|
// local lookup would validate the wrong machine. The remote launcher resolves
|
|
522
737
|
// it at connect time.
|
|
523
|
-
|
|
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) {
|
|
524
753
|
findBrowserPath(profile.browser, profile.binary);
|
|
525
754
|
}
|
|
526
755
|
const config = profileToConfig(profile);
|
|
@@ -575,7 +804,8 @@ export async function editProfile(name, patch) {
|
|
|
575
804
|
assertLocalPortFree(merged, { ignore: name });
|
|
576
805
|
// Same rule as create: the binary lives on the remote for an SSH profile, so a
|
|
577
806
|
// local lookup would validate the wrong machine.
|
|
578
|
-
|
|
807
|
+
const nativeArc = Object.values(getEndpointPresets(merged)).some((preset) => preset.target.startsWith('arc-native:'));
|
|
808
|
+
if (!hasSshEndpoint(merged.endpoints) && !nativeArc) {
|
|
579
809
|
findBrowserPath(merged.browser, merged.binary);
|
|
580
810
|
}
|
|
581
811
|
await updateProfile(merged);
|
package/dist/lib/browser/refs.js
CHANGED
|
@@ -6,6 +6,7 @@ export function describeRefs(nodeMap) {
|
|
|
6
6
|
role: n.role,
|
|
7
7
|
name: n.name,
|
|
8
8
|
attrs: n.attrs.slice(),
|
|
9
|
+
selector: n.selector,
|
|
9
10
|
}));
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
@@ -57,7 +58,7 @@ export function healRef(descriptor, freshNodeMap) {
|
|
|
57
58
|
// Lexicographic key, all "higher is better":
|
|
58
59
|
// [ DOM-backed, attribute similarity, -distance-to-original-ref ].
|
|
59
60
|
const key = [
|
|
60
|
-
node.backendNodeId !== undefined ? 1 : 0,
|
|
61
|
+
node.backendNodeId !== undefined || node.selector !== undefined ? 1 : 0,
|
|
61
62
|
attrScore(node),
|
|
62
63
|
-Math.abs(node.ref - ref),
|
|
63
64
|
];
|
|
@@ -24,6 +24,8 @@ export interface ResolvedBrowserTarget {
|
|
|
24
24
|
* surfaces this so the caller knows WHERE the browser actually lives.
|
|
25
25
|
*/
|
|
26
26
|
picked?: string;
|
|
27
|
+
/** Native endpoints cannot be tunnelled; the CLI must re-exec on this device. */
|
|
28
|
+
commandDispatch?: boolean;
|
|
27
29
|
}
|
|
28
30
|
/** Fork only fungible Electron profiles; identity-bearing ones share one connection. */
|
|
29
31
|
export declare function shouldForkProfile(kind: 'identity' | 'fungible', conn: {
|
|
@@ -35,11 +35,15 @@ export function profileFromDeclaration(name, declaration) {
|
|
|
35
35
|
targetFilter: config.targetFilter,
|
|
36
36
|
endpoints: config.endpoints,
|
|
37
37
|
defaultEndpoint: config.defaultEndpoint,
|
|
38
|
+
launchPolicy: config.launchPolicy,
|
|
39
|
+
userDataDir: config.userDataDir,
|
|
38
40
|
chrome: config.chrome,
|
|
39
41
|
secrets: config.secrets,
|
|
40
42
|
viewport: config.viewport,
|
|
41
43
|
logDir: config.logDir,
|
|
42
44
|
logHost: config.logHost,
|
|
45
|
+
arc: config.arc,
|
|
46
|
+
firefox: config.firefox,
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
/** Runtime key: profile name plus the resolved device. Drops `@endpoint-N`. */
|
|
@@ -125,6 +129,15 @@ export function sshEndpointForDeclaration(device, config, endpointName, os) {
|
|
|
125
129
|
const resolved = resolveEndpoint(profile, endpointName);
|
|
126
130
|
if (resolved.target.startsWith('ssh:'))
|
|
127
131
|
return resolved.target;
|
|
132
|
+
// Native Arc endpoints (arc-native:) must NOT be rewritten into SSH tunnels
|
|
133
|
+
// (PHNX-2399). They dispatch the whole command to the owner device instead.
|
|
134
|
+
if (resolved.target.startsWith('arc-native:'))
|
|
135
|
+
return resolved.target;
|
|
136
|
+
// Firefox BiDi endpoints (firefox-bidi:) are a local WebSocket bound to a
|
|
137
|
+
// specific profile directory on the owner box; they dispatch the whole
|
|
138
|
+
// command to that box rather than tunnelling (PHNX-4043).
|
|
139
|
+
if (resolved.target.startsWith('firefox-bidi:'))
|
|
140
|
+
return resolved.target;
|
|
128
141
|
const parsed = parseEndpointUrl(resolved.target);
|
|
129
142
|
const port = parsed?.port ?? 9222;
|
|
130
143
|
const osQuery = isWindowsOs(os) ? '&os=windows' : '';
|
|
@@ -202,6 +215,7 @@ export function resolveBrowserTarget(name, opts = {}) {
|
|
|
202
215
|
targetFilter: resolved.targetFilter,
|
|
203
216
|
},
|
|
204
217
|
picked,
|
|
218
|
+
commandDispatch: resolved.target.startsWith('arc-native:') || resolved.target.startsWith('firefox-bidi:'),
|
|
205
219
|
};
|
|
206
220
|
}
|
|
207
221
|
throw unreachableDeclaringDevicesError(name, devices, failures);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
-
import { getProfileRuntimeDir, getBrowserRuntimeDir, getEndpointPresets, listProfiles, getConfiguredDefaultProfileName, isProfileLaunchableHere, deleteProfile, DEFAULT_BROWSER_PROFILE_NAME, LEGACY_DEFAULT_BROWSER_PROFILE_NAME, } from './profiles.js';
|
|
4
|
+
import { getProfileRuntimeDir, getBrowserRuntimeDir, getEndpointPresets, listProfiles, getConfiguredDefaultProfileName, isProfileLaunchableHere, isProfileDeclaredHere, deleteProfile, DEFAULT_BROWSER_PROFILE_NAME, LEGACY_DEFAULT_BROWSER_PROFILE_NAME, } from './profiles.js';
|
|
5
5
|
import { keyBelongsToProfile } from './types.js';
|
|
6
6
|
import { declaringDevices, profileKind } from './registry.js';
|
|
7
7
|
import { machineId } from '../machine-id.js';
|
|
@@ -405,6 +405,14 @@ export async function buildProfilePrunePlan() {
|
|
|
405
405
|
for (const profile of profiles) {
|
|
406
406
|
const scope = profile.devices.length > 1 ? 'fungible' : 'identity';
|
|
407
407
|
const misfiled = identityLoopbackMismatch(profile);
|
|
408
|
+
if (profile.arc && !isProfileDeclaredHere(profile.name)) {
|
|
409
|
+
peerKept.push({
|
|
410
|
+
name: profile.name,
|
|
411
|
+
scope,
|
|
412
|
+
why: 'read-only native Arc discovery; no agents-cli alias to prune',
|
|
413
|
+
});
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
408
416
|
if (profile.devices.includes(current)) {
|
|
409
417
|
declaredHere.push({
|
|
410
418
|
name: profile.name,
|