@phnx-labs/agents-cli 1.22.41 → 1.22.43
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 +86 -0
- package/README.md +10 -4
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +25 -7
- package/dist/cli/command-registry.d.ts +2 -3
- package/dist/cli/command-registry.js +4 -8
- package/dist/commands/accounts.d.ts +29 -0
- package/dist/commands/accounts.js +220 -29
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +34 -9
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +108 -0
- package/dist/commands/browser-sessions-picker.js +42 -62
- package/dist/commands/browser.js +30 -38
- package/dist/commands/computer-actions.d.ts +1 -1
- package/dist/commands/computer-actions.js +1 -1
- package/dist/commands/computer-sessions-picker.js +32 -55
- package/dist/commands/computer.d.ts +1 -1
- package/dist/commands/computer.js +22 -8
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +5 -3
- package/dist/commands/doctor.js +23 -8
- package/dist/commands/exec.js +3 -2
- package/dist/commands/feed.js +2 -31
- package/dist/commands/harness.js +3 -3
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +1 -1
- package/dist/commands/insights.js +163 -85
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/mine.js +1 -1
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.js +175 -0
- package/dist/commands/packages.js +1 -1
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/prune.js +1 -1
- package/dist/commands/routines.js +44 -26
- package/dist/commands/run-account-picker.d.ts +19 -0
- package/dist/commands/run-account-picker.js +81 -0
- package/dist/commands/sessions-picker-factory.d.ts +40 -0
- package/dist/commands/sessions-picker-factory.js +67 -0
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/setup-browser.js +4 -4
- package/dist/commands/setup-mine.js +1 -1
- package/dist/commands/setup-preferences.js +2 -2
- package/dist/commands/setup.js +10 -4
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +30 -3
- package/dist/commands/versions.js +1 -1
- package/dist/commands/view.js +7 -1
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/account-capabilities.d.ts +10 -0
- package/dist/lib/account-capabilities.js +34 -0
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +584 -0
- package/dist/lib/agent-spec/agents.js +3127 -0
- package/dist/lib/agent-spec/provider.js +1 -1
- package/dist/lib/agents.d.ts +2 -584
- package/dist/lib/agents.js +2 -3127
- package/dist/lib/browser/drivers/local.d.ts +8 -2
- package/dist/lib/browser/drivers/local.js +8 -2
- package/dist/lib/browser/drivers/ssh.d.ts +8 -2
- package/dist/lib/browser/drivers/ssh.js +10 -4
- package/dist/lib/browser/hygiene.d.ts +1 -1
- package/dist/lib/browser/hygiene.js +5 -1
- package/dist/lib/browser/profiles.d.ts +74 -1
- package/dist/lib/browser/profiles.js +114 -5
- package/dist/lib/browser/runtime-state.d.ts +11 -5
- package/dist/lib/browser/runtime-state.js +13 -9
- package/dist/lib/browser/service.d.ts +68 -30
- package/dist/lib/browser/service.js +305 -194
- package/dist/lib/browser/types.d.ts +95 -3
- package/dist/lib/browser/types.js +51 -0
- package/dist/lib/catchup.js +1 -1
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/computer/actions.d.ts +1 -1
- package/dist/lib/computer/actions.js +1 -1
- package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
- package/dist/lib/computer/dispatch.d.ts +1 -1
- package/dist/lib/computer/download.d.ts +1 -1
- package/dist/lib/computer/download.js +3 -3
- package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
- package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +87 -59
- package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +28 -11
- package/dist/lib/{runner.js → daemon/runner.js} +143 -123
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +4 -4
- package/dist/lib/devices/fleet-inventory.js +1 -1
- package/dist/lib/devices/harness-inventory.js +1 -1
- package/dist/lib/devices/health-report.js +1 -1
- package/dist/lib/devices/registry.js +4 -22
- package/dist/lib/doctor-diff.d.ts +1 -1
- package/dist/lib/doctor-diff.js +1 -1
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/drift.js +1 -1
- package/dist/lib/entitlement.d.ts +31 -0
- package/dist/lib/entitlement.js +137 -0
- package/dist/lib/exec.js +52 -205
- package/dist/lib/feed/feed.js +2 -8
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/fs-atomic.d.ts +7 -0
- package/dist/lib/fs-atomic.js +9 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +161 -0
- package/dist/lib/harness/adapter.js +47 -0
- package/dist/lib/harness/adapters/claude.d.ts +2 -0
- package/dist/lib/harness/adapters/claude.js +110 -0
- package/dist/lib/harness/adapters/codex.d.ts +2 -0
- package/dist/lib/harness/adapters/codex.js +83 -0
- package/dist/lib/harness/adapters/copilot.d.ts +2 -0
- package/dist/lib/harness/adapters/copilot.js +23 -0
- package/dist/lib/harness/adapters/cursor.d.ts +2 -0
- package/dist/lib/harness/adapters/cursor.js +38 -0
- package/dist/lib/harness/adapters/droid.d.ts +2 -0
- package/dist/lib/harness/adapters/droid.js +17 -0
- package/dist/lib/harness/adapters/grok.d.ts +2 -0
- package/dist/lib/harness/adapters/grok.js +14 -0
- package/dist/lib/harness/adapters/kimi.d.ts +2 -0
- package/dist/lib/harness/adapters/kimi.js +49 -0
- package/dist/lib/harness/adapters/muse.d.ts +2 -0
- package/dist/lib/harness/adapters/muse.js +44 -0
- package/dist/lib/harness/adapters/opencode.d.ts +2 -0
- package/dist/lib/harness/adapters/opencode.js +14 -0
- package/dist/lib/harness/exec-config-version.d.ts +8 -0
- package/dist/lib/harness/exec-config-version.js +22 -0
- package/dist/lib/harness/index.d.ts +1 -0
- package/dist/lib/harness/index.js +34 -0
- package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
- package/dist/lib/{hooks.js → hooks/install.js} +11 -11
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -6
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/import.js +1 -1
- package/dist/lib/installations/index.d.ts +4 -4
- package/dist/lib/installations/index.js +3 -4
- package/dist/lib/installations/migrate.js +6 -6
- package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
- package/dist/lib/{shims.js → installations/shims.js} +13 -115
- package/dist/lib/installations/store.d.ts +141 -3
- package/dist/lib/installations/store.js +475 -5
- package/dist/lib/installations/versions.d.ts +40 -125
- package/dist/lib/installations/versions.js +57 -462
- package/dist/lib/isolation-boundary-report.d.ts +1 -1
- package/dist/lib/isolation-boundary-report.js +1 -1
- package/dist/lib/mailbox-gc.js +2 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +19 -8
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/monitors/dispatch.d.ts +1 -1
- package/dist/lib/monitors/dispatch.js +2 -2
- package/dist/lib/monitors/state.d.ts +1 -1
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/plugins/plugins.js +1 -1
- package/dist/lib/plugins/skills.js +1 -1
- package/dist/lib/prix-account.d.ts +158 -0
- package/dist/lib/prix-account.js +214 -0
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/project-key.js +2 -2
- package/dist/lib/project-launch.js +1 -1
- package/dist/lib/refresh.js +2 -2
- package/dist/lib/resource-inventory.d.ts +1 -1
- package/dist/lib/resource-inventory.js +1 -1
- package/dist/lib/rules/rules.js +1 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +17 -9
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
- package/dist/lib/self-heal/checks/path.js +1 -1
- package/dist/lib/self-heal/checks/shadowing.js +1 -1
- package/dist/lib/self-heal/checks/shims.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +3 -3
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/parse.js +24 -42
- package/dist/lib/session/recovery.js +1 -1
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote-list.js +1 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/staleness/prune.d.ts +1 -1
- package/dist/lib/staleness/prune.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +4 -1
- package/dist/lib/startup/command-registry.js +12 -5
- package/dist/lib/teams/agents.js +2 -19
- package/dist/lib/teams/parsers.js +20 -26
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/lib/teams/registry.js +2 -21
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +9 -1
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +3 -2
- package/dist/lib/uninstall.js +1 -1
- package/dist/lib/version.d.ts +9 -2
- package/dist/lib/version.js +11 -9
- package/dist/lib/view-types.d.ts +4 -0
- package/package.json +1 -1
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/tickets/list.d.ts +0 -53
- package/dist/lib/tickets/list.js +0 -153
- /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
|
@@ -8,7 +8,7 @@ import { connectLocal } from './drivers/local.js';
|
|
|
8
8
|
import { connectSSH, shellQuote } from './drivers/ssh.js';
|
|
9
9
|
import { clearProfileRuntime, listProfileCacheDirs, readProfileRuntimeMeta, isProcessAlive } from './runtime-state.js';
|
|
10
10
|
import { resolveDomainSkill } from './domain-skills.js';
|
|
11
|
-
import { generateTaskId, generateShortId, } from './types.js';
|
|
11
|
+
import { generateTaskId, generateShortId, connectionKey, asConnectionKey, parseConnectionKey, keyBelongsToProfile, } from './types.js';
|
|
12
12
|
import { reapAbandonedTasks, resolveLiveIdentities, taskOwnerIsGone, } from './hygiene.js';
|
|
13
13
|
import { taskMatchesCaller } from './caller-identity.js';
|
|
14
14
|
import { getRefs, resolveRefToCoords, describeRefs, healRef } from './refs.js';
|
|
@@ -272,14 +272,6 @@ export function deriveTaskLabel(opts) {
|
|
|
272
272
|
}
|
|
273
273
|
return opts.existing ?? 'untitled';
|
|
274
274
|
}
|
|
275
|
-
/**
|
|
276
|
-
* True when `key` is the bare profile name or one of its composite endpoint
|
|
277
|
-
* keys (`name@endpoint`). Used by status/stop so `comet-local` matches
|
|
278
|
-
* `comet-local@endpoint-0`.
|
|
279
|
-
*/
|
|
280
|
-
export function connectionKeyMatchesProfile(key, profileName) {
|
|
281
|
-
return key === profileName || key.startsWith(`${profileName}@`) || key.startsWith(`${profileName}.`);
|
|
282
|
-
}
|
|
283
275
|
/**
|
|
284
276
|
* Resolve the identity stamped on a task at start: WHO (`owner`) and WHICH run
|
|
285
277
|
* (`launchId`). The forwarded values come from the caller's own CLI process and
|
|
@@ -301,6 +293,12 @@ export class BrowserService {
|
|
|
301
293
|
'rush-app': 'rush-app-',
|
|
302
294
|
'rush-cli': 'rush-cli-',
|
|
303
295
|
};
|
|
296
|
+
/**
|
|
297
|
+
* Live browsers, keyed by {@link ConnectionKey} — NOT by profile name. The
|
|
298
|
+
* branded key type is what makes `connections.get(someProfileName)` (the
|
|
299
|
+
* RUSH-2709 miss that made `status --profile <name>` return empty for a
|
|
300
|
+
* running `<name>@endpoint-0`) fail to compile.
|
|
301
|
+
*/
|
|
304
302
|
connections = new Map();
|
|
305
303
|
forkingProfiles = new Set();
|
|
306
304
|
// Per-task storage for console, errors, network, downloads
|
|
@@ -319,19 +317,18 @@ export class BrowserService {
|
|
|
319
317
|
throw new Error(`Profile "${profileName}" not found`);
|
|
320
318
|
}
|
|
321
319
|
// Pick the endpoint preset. Throws with the candidate list if the user
|
|
322
|
-
// passed an unknown name. The
|
|
323
|
-
//
|
|
324
|
-
//
|
|
320
|
+
// passed an unknown name. The runtime key `<profile>@<endpoint>` is what
|
|
321
|
+
// the connection map + per-profile runtime dirs are keyed on, so a single
|
|
322
|
+
// YAML profile can run at multiple endpoints concurrently.
|
|
325
323
|
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
324
|
+
// `effectiveProfile.name` stays BARE. Overwriting it with the runtime key
|
|
325
|
+
// here (pre-RUSH-2709) is what leaked `comet-local@endpoint-0` into every
|
|
326
|
+
// listing and made `status --profile comet-local` miss its own browser.
|
|
327
|
+
// The key travels as its own argument instead.
|
|
328
328
|
const resolved = resolveEndpoint(profile, opts.endpointName);
|
|
329
|
-
const composite =
|
|
329
|
+
const composite = connectionKey(profileName, resolved.name);
|
|
330
330
|
const effectiveProfile = {
|
|
331
331
|
...profile,
|
|
332
|
-
// Runtime/connect path key — NOT the bare YAML name. Connections and
|
|
333
|
-
// tasks.json live under this composite so multi-endpoint is safe.
|
|
334
|
-
name: composite,
|
|
335
332
|
binary: resolved.binary,
|
|
336
333
|
targetFilter: resolved.targetFilter,
|
|
337
334
|
};
|
|
@@ -349,7 +346,7 @@ export class BrowserService {
|
|
|
349
346
|
}
|
|
350
347
|
const taskLabel = deriveTaskLabel({ title: opts.title, url: opts.url });
|
|
351
348
|
let conn = this.connections.get(composite);
|
|
352
|
-
let
|
|
349
|
+
let effectiveKey = composite;
|
|
353
350
|
// If we have a cached connection, confirm it's still usable before any
|
|
354
351
|
// caller relies on it. A browser killed externally (Cmd-Q, crash, or a
|
|
355
352
|
// user clearing the profile by hand) leaves a closed WebSocket here.
|
|
@@ -372,7 +369,7 @@ export class BrowserService {
|
|
|
372
369
|
const existingFork = this.findAvailableFork(composite);
|
|
373
370
|
if (existingFork) {
|
|
374
371
|
conn = existingFork.conn;
|
|
375
|
-
|
|
372
|
+
effectiveKey = existingFork.name;
|
|
376
373
|
}
|
|
377
374
|
else {
|
|
378
375
|
throw new Error(`Fork in progress but no available fork found for "${composite}"`);
|
|
@@ -381,9 +378,9 @@ export class BrowserService {
|
|
|
381
378
|
else {
|
|
382
379
|
this.forkingProfiles.add(composite);
|
|
383
380
|
try {
|
|
384
|
-
const { forkName, connection } = await this.forkElectronProfile(effectiveProfile);
|
|
381
|
+
const { forkName, connection } = await this.forkElectronProfile(effectiveProfile, composite);
|
|
385
382
|
conn = connection;
|
|
386
|
-
|
|
383
|
+
effectiveKey = forkName;
|
|
387
384
|
}
|
|
388
385
|
finally {
|
|
389
386
|
this.forkingProfiles.delete(composite);
|
|
@@ -391,14 +388,14 @@ export class BrowserService {
|
|
|
391
388
|
}
|
|
392
389
|
}
|
|
393
390
|
else if (!conn) {
|
|
394
|
-
conn = await this.connectProfile(effectiveProfile, resolved.target);
|
|
395
|
-
conn.
|
|
396
|
-
conn.
|
|
391
|
+
conn = await this.connectProfile(effectiveProfile, resolved.target, composite);
|
|
392
|
+
conn.key = composite;
|
|
393
|
+
conn.profile = profileName;
|
|
397
394
|
this.connections.set(composite, conn);
|
|
398
395
|
await this.applyDefaultDownloadBehavior(conn, composite);
|
|
399
396
|
}
|
|
400
|
-
if (conn && !conn.
|
|
401
|
-
conn.
|
|
397
|
+
if (conn && !conn.profile)
|
|
398
|
+
conn.profile = profileName;
|
|
402
399
|
// Browsers launch with --no-startup-window (session-cookie persistence,
|
|
403
400
|
// see launchBrowser), so a bare `start` with no --url would otherwise
|
|
404
401
|
// leave the user staring at a process with zero windows. Recreate the
|
|
@@ -427,7 +424,7 @@ export class BrowserService {
|
|
|
427
424
|
id: taskId,
|
|
428
425
|
name: taskName,
|
|
429
426
|
label: taskLabel,
|
|
430
|
-
profile:
|
|
427
|
+
profile: effectiveKey,
|
|
431
428
|
tabs: {},
|
|
432
429
|
currentTabId: undefined,
|
|
433
430
|
createdAt: now,
|
|
@@ -456,7 +453,7 @@ export class BrowserService {
|
|
|
456
453
|
}
|
|
457
454
|
}
|
|
458
455
|
conn.tasks.set(taskName, task);
|
|
459
|
-
await this.saveTaskState(
|
|
456
|
+
await this.saveTaskState(effectiveKey, conn.tasks);
|
|
460
457
|
// Durable identity, written ONCE at start and never deleted (RUSH-2549).
|
|
461
458
|
// tasks.json above stays live state: `stop` drops the task from that map, so
|
|
462
459
|
// anything recorded only there is gone the moment the task ends -- which is
|
|
@@ -470,22 +467,23 @@ export class BrowserService {
|
|
|
470
467
|
try {
|
|
471
468
|
recordBrowserSession({
|
|
472
469
|
task: taskName,
|
|
473
|
-
profile:
|
|
470
|
+
profile: effectiveKey,
|
|
474
471
|
sessionId: task.sessionId,
|
|
475
472
|
launchId: task.launchId,
|
|
476
473
|
actor: task.owner,
|
|
477
474
|
startedAt: task.createdAt,
|
|
478
|
-
captureDir: getProfileSessionsDir(
|
|
475
|
+
captureDir: getProfileSessionsDir(effectiveKey, taskName),
|
|
479
476
|
});
|
|
480
477
|
}
|
|
481
478
|
catch {
|
|
482
479
|
// Recording is best-effort; the task itself is live either way.
|
|
483
480
|
}
|
|
484
|
-
|
|
481
|
+
// Feed events and the usage rollup are read by humans: bare name only.
|
|
482
|
+
emit('browser.launch', { profile: profileName, task: taskName, pid: conn.pid });
|
|
485
483
|
void import('../analytics/usage-db.js').then(({ recordUsage }) => {
|
|
486
484
|
recordUsage({
|
|
487
485
|
kind: 'browser',
|
|
488
|
-
name:
|
|
486
|
+
name: profileName,
|
|
489
487
|
event: 'launch',
|
|
490
488
|
source: 'browser',
|
|
491
489
|
meta: { task: taskName },
|
|
@@ -501,11 +499,11 @@ export class BrowserService {
|
|
|
501
499
|
task.tabs[shortId] = targetId;
|
|
502
500
|
task.currentTabId = shortId;
|
|
503
501
|
this.invalidateTargetCache(conn);
|
|
504
|
-
await this.saveTaskState(
|
|
502
|
+
await this.saveTaskState(effectiveKey, conn.tasks);
|
|
505
503
|
tabId = shortId;
|
|
506
504
|
}
|
|
507
505
|
else if (opts.url && conn.electron) {
|
|
508
|
-
const result = await this.navigate(taskName, opts.url,
|
|
506
|
+
const result = await this.navigate(taskName, opts.url, effectiveKey);
|
|
509
507
|
tabId = result.tabId;
|
|
510
508
|
}
|
|
511
509
|
// Domain-skill discovery: when a URL is supplied, look up site-specific
|
|
@@ -517,7 +515,7 @@ export class BrowserService {
|
|
|
517
515
|
if (resolved)
|
|
518
516
|
skill = resolved;
|
|
519
517
|
}
|
|
520
|
-
return { task: taskId, name: taskName, tabId, profile:
|
|
518
|
+
return { task: taskId, name: taskName, tabId, profile: profileName, key: effectiveKey, skill };
|
|
521
519
|
}
|
|
522
520
|
/**
|
|
523
521
|
* Reclaim a live page already showing `url` from an ABANDONED task, instead
|
|
@@ -551,6 +549,50 @@ export class BrowserService {
|
|
|
551
549
|
* page that has since redirected elsewhere simply does not match, and the
|
|
552
550
|
* caller opens a tab as before.
|
|
553
551
|
*/
|
|
552
|
+
/** Is this CDP target already registered to any task on the connection? */
|
|
553
|
+
targetIsClaimed(conn, targetId) {
|
|
554
|
+
for (const t of conn.tasks.values()) {
|
|
555
|
+
if (Object.values(t.tabs).includes(targetId))
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* The CDP targets a task may CLOSE — its own tabs minus any it merely
|
|
562
|
+
* borrowed. A borrowed tab existed before the task did (see `Task.borrowedTabs`),
|
|
563
|
+
* so closing it would remove something the task never opened.
|
|
564
|
+
*/
|
|
565
|
+
closeableTargetIds(task) {
|
|
566
|
+
const borrowed = new Set(task.borrowedTabs ?? []);
|
|
567
|
+
return Object.entries(task.tabs)
|
|
568
|
+
.filter(([shortId]) => !borrowed.has(shortId))
|
|
569
|
+
.map(([, cdpId]) => cdpId);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* A page target that `navigate` may take over on a browser which cannot create
|
|
573
|
+
* tabs (Arc). Deliberately narrow: only a tab ALREADY showing the requested URL
|
|
574
|
+
* -- re-showing that document there is what the caller asked for -- or an empty
|
|
575
|
+
* new-tab page. A target any task already owns is left alone. Note the honest
|
|
576
|
+
* limit: reusing a matching tab still RELOADS it, discarding scroll position and
|
|
577
|
+
* unsaved form state. Arc cannot open a tab at all, so some intrusion is
|
|
578
|
+
* unavoidable to show the document; a reload is the smallest one, and the tab is
|
|
579
|
+
* borrowed rather than owned so it is never closed.
|
|
580
|
+
*/
|
|
581
|
+
async pickReusableTargetWithoutCreate(conn, url) {
|
|
582
|
+
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
583
|
+
const owned = new Set();
|
|
584
|
+
for (const t of conn.tasks.values()) {
|
|
585
|
+
for (const id of Object.values(t.tabs))
|
|
586
|
+
owned.add(id);
|
|
587
|
+
}
|
|
588
|
+
const free = targetInfos.filter((t) => t.type === 'page' && !owned.has(t.targetId));
|
|
589
|
+
const wanted = canonicalTabUrl(url);
|
|
590
|
+
const sameUrl = free.find((t) => canonicalTabUrl(t.url) === wanted);
|
|
591
|
+
if (sameUrl)
|
|
592
|
+
return sameUrl.targetId;
|
|
593
|
+
const BLANK = new Set(['', 'about:blank', 'about:newtab', 'chrome://newtab/', 'chrome://new-tab-page/']);
|
|
594
|
+
return free.find((t) => BLANK.has(t.url))?.targetId;
|
|
595
|
+
}
|
|
554
596
|
async adoptTabShowing(conn, url) {
|
|
555
597
|
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
556
598
|
const wanted = canonicalTabUrl(url);
|
|
@@ -561,7 +603,15 @@ export class BrowserService {
|
|
|
561
603
|
return undefined;
|
|
562
604
|
const candidates = [];
|
|
563
605
|
for (const task of conn.tasks.values()) {
|
|
606
|
+
const borrowed = new Set(task.borrowedTabs ?? []);
|
|
564
607
|
for (const [shortId, cdpId] of Object.entries(task.tabs)) {
|
|
608
|
+
// A BORROWED tab is not ours to hand on. The dead task never opened it
|
|
609
|
+
// (Task.borrowedTabs) -- reclaiming it would register a pre-existing tab
|
|
610
|
+
// as a fresh task's own, and that task's `done` would close it. Exactly
|
|
611
|
+
// the "adopting it would make this task's `done` close the user's tab"
|
|
612
|
+
// case above, arriving one hop later via reclaim instead of directly.
|
|
613
|
+
if (borrowed.has(shortId))
|
|
614
|
+
continue;
|
|
565
615
|
if (matching.has(cdpId))
|
|
566
616
|
candidates.push({ task, shortId, targetId: cdpId });
|
|
567
617
|
}
|
|
@@ -610,7 +660,7 @@ export class BrowserService {
|
|
|
610
660
|
const out = [];
|
|
611
661
|
for (const [key, conn] of this.connections) {
|
|
612
662
|
for (const task of conn.tasks.values()) {
|
|
613
|
-
out.push({ profile: conn.
|
|
663
|
+
out.push({ profile: conn.key ?? key, task });
|
|
614
664
|
}
|
|
615
665
|
}
|
|
616
666
|
return out;
|
|
@@ -631,7 +681,7 @@ export class BrowserService {
|
|
|
631
681
|
catch {
|
|
632
682
|
// findTask throws when truly gone; fall through to the miss return.
|
|
633
683
|
}
|
|
634
|
-
for (const [
|
|
684
|
+
for (const [key, conn] of this.connections) {
|
|
635
685
|
const task = this.lookupTaskOnConn(conn, taskName);
|
|
636
686
|
if (task) {
|
|
637
687
|
// Map key may be task.name, not the caller-supplied handle.
|
|
@@ -659,8 +709,9 @@ export class BrowserService {
|
|
|
659
709
|
}
|
|
660
710
|
// Save to history before closing
|
|
661
711
|
await this.saveToHistory(task, Array.from(domains));
|
|
662
|
-
// Close task's tabs (not the window - it's shared
|
|
663
|
-
|
|
712
|
+
// Close task's tabs (not the window - it's shared, and not a tab the
|
|
713
|
+
// task merely borrowed - that one was open before the task existed).
|
|
714
|
+
await Promise.all(this.closeableTargetIds(task).map((cdpId) => conn.cdp.send('Target.closeTarget', { targetId: cdpId }).catch(() => {
|
|
664
715
|
// Tab already closed
|
|
665
716
|
})));
|
|
666
717
|
for (const cdpId of Object.values(task.tabs)) {
|
|
@@ -668,12 +719,14 @@ export class BrowserService {
|
|
|
668
719
|
}
|
|
669
720
|
this.invalidateTargetCache(conn);
|
|
670
721
|
conn.tasks.delete(mapKey);
|
|
671
|
-
await this.saveTaskState(
|
|
672
|
-
|
|
722
|
+
await this.saveTaskState(key, conn.tasks);
|
|
723
|
+
// Feed + usage rows are read by humans: bare profile name (RUSH-2709).
|
|
724
|
+
const bareProfile = conn.profile ?? parseConnectionKey(key).profile;
|
|
725
|
+
emit('browser.close', { profile: bareProfile, task: task.name });
|
|
673
726
|
void import('../analytics/usage-db.js').then(({ recordUsage }) => {
|
|
674
727
|
recordUsage({
|
|
675
728
|
kind: 'browser',
|
|
676
|
-
name:
|
|
729
|
+
name: bareProfile,
|
|
677
730
|
event: 'close',
|
|
678
731
|
source: 'browser',
|
|
679
732
|
meta: { task: task.name },
|
|
@@ -683,10 +736,10 @@ export class BrowserService {
|
|
|
683
736
|
conn.cdp.close();
|
|
684
737
|
killChrome(conn.pid);
|
|
685
738
|
conn.cleanup?.();
|
|
686
|
-
this.connections.delete(
|
|
687
|
-
clearProfileRuntime(
|
|
739
|
+
this.connections.delete(key);
|
|
740
|
+
clearProfileRuntime(key);
|
|
688
741
|
}
|
|
689
|
-
return { ok: true, profile:
|
|
742
|
+
return { ok: true, profile: bareProfile };
|
|
690
743
|
}
|
|
691
744
|
}
|
|
692
745
|
return { ok: false };
|
|
@@ -694,16 +747,15 @@ export class BrowserService {
|
|
|
694
747
|
async done(taskName) {
|
|
695
748
|
return this.stop(taskName);
|
|
696
749
|
}
|
|
697
|
-
async stopProfile(
|
|
698
|
-
// Connections are keyed by the
|
|
699
|
-
//
|
|
700
|
-
// A plain `connections.get(
|
|
701
|
-
// connection, so `cleanup()` never ran — leaving the SSH tunnel
|
|
702
|
-
// Windows, the WMI-spawned browser orphaned on the remote host after
|
|
703
|
-
// `browser stop --profile` (#559).
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
const keys = [...this.connections.keys()].filter((k) => k === profileName || k.startsWith(`${profileName}@`));
|
|
750
|
+
async stopProfile(profileRef) {
|
|
751
|
+
// Connections are keyed by the runtime key `<profile>@<endpoint>` (see
|
|
752
|
+
// start()) while callers pass the bare profile name (or, occasionally, an
|
|
753
|
+
// exact key). A plain `connections.get(profileRef)` therefore missed every
|
|
754
|
+
// real remote connection, so `cleanup()` never ran — leaving the SSH tunnel
|
|
755
|
+
// and, on Windows, the WMI-spawned browser orphaned on the remote host after
|
|
756
|
+
// every `browser stop --profile` (#559). Same single rule as findTask and
|
|
757
|
+
// status: exact key, else every key belonging to that profile.
|
|
758
|
+
const keys = [...this.connections.keys()].filter((k) => k === profileRef || keyBelongsToProfile(k, profileRef));
|
|
707
759
|
for (const key of keys) {
|
|
708
760
|
const conn = this.connections.get(key);
|
|
709
761
|
if (!conn)
|
|
@@ -717,7 +769,7 @@ export class BrowserService {
|
|
|
717
769
|
// Kill stale processes and clean runtime dirs for every composite and
|
|
718
770
|
// fork entry belonging to this profile (including `.N` forks left by
|
|
719
771
|
// earlier daemon sessions that the connection loop above didn't cover).
|
|
720
|
-
for (const dir of listProfileCacheDirs(
|
|
772
|
+
for (const dir of listProfileCacheDirs(parseConnectionKey(profileRef).profile)) {
|
|
721
773
|
const dirName = path.basename(dir);
|
|
722
774
|
const meta = readProfileRuntimeMeta(dirName);
|
|
723
775
|
if (meta?.pid && meta.pid !== 0 && isProcessAlive(meta.pid, meta.command)) {
|
|
@@ -726,8 +778,8 @@ export class BrowserService {
|
|
|
726
778
|
clearProfileRuntime(dirName);
|
|
727
779
|
}
|
|
728
780
|
}
|
|
729
|
-
async navigate(taskId, url,
|
|
730
|
-
const { conn, task } = await this.findTask(taskId,
|
|
781
|
+
async navigate(taskId, url, profileRef) {
|
|
782
|
+
const { conn, task } = await this.findTask(taskId, profileRef);
|
|
731
783
|
this.maybeUpdateLabelFromUrl(conn, task, url);
|
|
732
784
|
// If we have a current tab, navigate in it (reuse)
|
|
733
785
|
const currentShortId = task.currentTabId;
|
|
@@ -736,7 +788,7 @@ export class BrowserService {
|
|
|
736
788
|
const sessionId = await this.getSessionId(conn, cdpTargetId);
|
|
737
789
|
await conn.cdp.send('Page.navigate', { url }, sessionId);
|
|
738
790
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
739
|
-
emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: currentShortId, created: false });
|
|
791
|
+
emit('browser.navigate', { profile: parseConnectionKey(task.profile).profile, task: task.name, url, tabId: currentShortId, created: false });
|
|
740
792
|
return { tabId: currentShortId, url, created: false };
|
|
741
793
|
}
|
|
742
794
|
// No current tab - create one
|
|
@@ -751,9 +803,61 @@ export class BrowserService {
|
|
|
751
803
|
task.tabs[shortId] = cdpTargetId;
|
|
752
804
|
task.currentTabId = shortId;
|
|
753
805
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
754
|
-
emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: shortId, created: true });
|
|
806
|
+
emit('browser.navigate', { profile: parseConnectionKey(task.profile).profile, task: task.name, url, tabId: shortId, created: true });
|
|
755
807
|
return { tabId: shortId, url, created: true };
|
|
756
808
|
}
|
|
809
|
+
// Arc exposes page targets and honors Page.navigate on them; what it cannot
|
|
810
|
+
// survive is Target.createTarget (#2778). createPageTarget below therefore
|
|
811
|
+
// refuses outright, so the document never gets shown AT ALL and callers fall
|
|
812
|
+
// back to a raw `open` -- a new tab every call, which is the tab-spam #2779
|
|
813
|
+
// set out to end. Reuse in place instead, the same shape as the Electron
|
|
814
|
+
// branch above. Measured against a live Arc (#2786): 33 page targets,
|
|
815
|
+
// Page.navigate reused one, tab count unchanged, no crash.
|
|
816
|
+
if (conn.browserType === 'arc') {
|
|
817
|
+
const reusable = await this.pickReusableTargetWithoutCreate(conn, url);
|
|
818
|
+
if (reusable) {
|
|
819
|
+
const shortId = generateShortId();
|
|
820
|
+
const sessionId = await this.getSessionId(conn, reusable);
|
|
821
|
+
// Check and CLAIM in one synchronous tick, with no await between them --
|
|
822
|
+
// the ordering `adoptTabShowing` uses (service.ts: the `task.tabs[shortId]
|
|
823
|
+
// !== targetId` re-check sits immediately before its `delete`). Checking
|
|
824
|
+
// and then awaiting `Page.navigate` before writing leaves exactly the gap
|
|
825
|
+
// the check exists to close: a concurrent navigate passes its own check in
|
|
826
|
+
// that window and both tasks end up driving one tab.
|
|
827
|
+
if (this.targetIsClaimed(conn, reusable)) {
|
|
828
|
+
throw arcNotDrivableError(conn.profile);
|
|
829
|
+
}
|
|
830
|
+
task.tabs[shortId] = reusable;
|
|
831
|
+
// Borrowed, not opened: this tab was in the browser before the task and
|
|
832
|
+
// must survive `done`. See Task.borrowedTabs.
|
|
833
|
+
task.borrowedTabs = [...(task.borrowedTabs ?? []), shortId];
|
|
834
|
+
task.currentTabId = shortId;
|
|
835
|
+
try {
|
|
836
|
+
await conn.cdp.send('Page.navigate', { url }, sessionId);
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
// Release the claim we took optimistically, so a failed navigate does
|
|
840
|
+
// not leave the task holding a tab it never drove.
|
|
841
|
+
delete task.tabs[shortId];
|
|
842
|
+
task.borrowedTabs = (task.borrowedTabs ?? []).filter((id) => id !== shortId);
|
|
843
|
+
if (task.currentTabId === shortId)
|
|
844
|
+
task.currentTabId = undefined;
|
|
845
|
+
throw err;
|
|
846
|
+
}
|
|
847
|
+
this.invalidateTargetCache(conn);
|
|
848
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
849
|
+
emit('browser.navigate', {
|
|
850
|
+
profile: task.profile,
|
|
851
|
+
task: task.name,
|
|
852
|
+
url,
|
|
853
|
+
tabId: shortId,
|
|
854
|
+
created: false,
|
|
855
|
+
});
|
|
856
|
+
return { tabId: shortId, url, created: false };
|
|
857
|
+
}
|
|
858
|
+
// Nothing safe to reuse -- fall through to the actionable refusal rather
|
|
859
|
+
// than taking over a page the user is reading.
|
|
860
|
+
}
|
|
757
861
|
// Chrome: create new tab
|
|
758
862
|
const result = await this.createPageTarget(conn, { url });
|
|
759
863
|
const shortId = generateShortId();
|
|
@@ -761,11 +865,11 @@ export class BrowserService {
|
|
|
761
865
|
task.currentTabId = shortId;
|
|
762
866
|
this.invalidateTargetCache(conn);
|
|
763
867
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
764
|
-
emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: shortId, created: true });
|
|
868
|
+
emit('browser.navigate', { profile: parseConnectionKey(task.profile).profile, task: task.name, url, tabId: shortId, created: true });
|
|
765
869
|
return { tabId: shortId, url, created: true };
|
|
766
870
|
}
|
|
767
|
-
async tabAdd(taskId, url,
|
|
768
|
-
const { conn, task } = await this.findTask(taskId,
|
|
871
|
+
async tabAdd(taskId, url, profileRef) {
|
|
872
|
+
const { conn, task } = await this.findTask(taskId, profileRef);
|
|
769
873
|
if (conn.electron) {
|
|
770
874
|
throw new Error('Electron apps do not support opening additional tabs');
|
|
771
875
|
}
|
|
@@ -841,9 +945,9 @@ export class BrowserService {
|
|
|
841
945
|
throw new Error(`Tab ${shortId} not found`);
|
|
842
946
|
return cdpId;
|
|
843
947
|
}
|
|
844
|
-
async tabs(taskId,
|
|
948
|
+
async tabs(taskId, profileRef) {
|
|
845
949
|
if (taskId) {
|
|
846
|
-
const { conn, task } = await this.findTask(taskId,
|
|
950
|
+
const { conn, task } = await this.findTask(taskId, profileRef);
|
|
847
951
|
return this.getTabsForTask(conn.cdp, task);
|
|
848
952
|
}
|
|
849
953
|
const allTabs = [];
|
|
@@ -861,9 +965,15 @@ export class BrowserService {
|
|
|
861
965
|
const shortId = await this.resolveTabHint(conn, task, tabHint);
|
|
862
966
|
const cdpId = task.tabs[shortId];
|
|
863
967
|
if (cdpId) {
|
|
864
|
-
|
|
968
|
+
// A borrowed tab is released, not closed — it outlives this task.
|
|
969
|
+
if (!(task.borrowedTabs ?? []).includes(shortId)) {
|
|
970
|
+
await conn.cdp.send('Target.closeTarget', { targetId: cdpId });
|
|
971
|
+
}
|
|
865
972
|
conn.sessionCache.delete(cdpId);
|
|
866
973
|
delete task.tabs[shortId];
|
|
974
|
+
// Drop the borrow record with the tab it names, or it outlives every tab
|
|
975
|
+
// it ever described and grows unbounded in the persisted task state.
|
|
976
|
+
task.borrowedTabs = (task.borrowedTabs ?? []).filter((id) => id !== shortId);
|
|
867
977
|
// Update currentTabId if we closed the current tab
|
|
868
978
|
if (task.currentTabId === shortId) {
|
|
869
979
|
const remaining = Object.keys(task.tabs);
|
|
@@ -872,12 +982,13 @@ export class BrowserService {
|
|
|
872
982
|
}
|
|
873
983
|
}
|
|
874
984
|
else {
|
|
875
|
-
// Close all tabs
|
|
876
|
-
await Promise.all(
|
|
985
|
+
// Close all tabs the task opened; borrowed ones are only released.
|
|
986
|
+
await Promise.all(this.closeableTargetIds(task).map((cdpId) => conn.cdp.send('Target.closeTarget', { targetId: cdpId }).catch(() => { })));
|
|
877
987
|
for (const cdpId of Object.values(task.tabs)) {
|
|
878
988
|
conn.sessionCache.delete(cdpId);
|
|
879
989
|
}
|
|
880
990
|
task.tabs = {};
|
|
991
|
+
task.borrowedTabs = [];
|
|
881
992
|
task.currentTabId = undefined;
|
|
882
993
|
}
|
|
883
994
|
this.invalidateTargetCache(conn);
|
|
@@ -914,7 +1025,7 @@ export class BrowserService {
|
|
|
914
1025
|
return result.result.value;
|
|
915
1026
|
}
|
|
916
1027
|
async screenshot(taskId, tabHint, outputPath, quality = 'compressed') {
|
|
917
|
-
const { conn, task,
|
|
1028
|
+
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
918
1029
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
919
1030
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
920
1031
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -947,7 +1058,7 @@ export class BrowserService {
|
|
|
947
1058
|
}
|
|
948
1059
|
extension = 'jpg';
|
|
949
1060
|
}
|
|
950
|
-
const sessionsDir = getProfileSessionsDir(
|
|
1061
|
+
const sessionsDir = getProfileSessionsDir(runtimeKey, task.name);
|
|
951
1062
|
const automaticPath = path.join(sessionsDir, `${Date.now()}.${extension}`);
|
|
952
1063
|
const finalPath = resolveScreenshotOutputPath(outputPath, automaticPath);
|
|
953
1064
|
await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
|
|
@@ -955,7 +1066,7 @@ export class BrowserService {
|
|
|
955
1066
|
const dims = (extension === 'png' ? readPngDimensions(buffer) : readJpegDimensions(buffer)) ??
|
|
956
1067
|
{ width: 0, height: 0 };
|
|
957
1068
|
emit('browser.screenshot', {
|
|
958
|
-
profile:
|
|
1069
|
+
profile: parseConnectionKey(runtimeKey).profile,
|
|
959
1070
|
task: task.name,
|
|
960
1071
|
tabId: shortId,
|
|
961
1072
|
path: finalPath,
|
|
@@ -974,7 +1085,7 @@ export class BrowserService {
|
|
|
974
1085
|
* — without it, dark-mode pages render on a blank sheet.
|
|
975
1086
|
*/
|
|
976
1087
|
async printToPdf(taskId, tabHint, outputPath) {
|
|
977
|
-
const { conn, task,
|
|
1088
|
+
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
978
1089
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
979
1090
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
980
1091
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -984,7 +1095,7 @@ export class BrowserService {
|
|
|
984
1095
|
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
985
1096
|
const { data } = (await conn.cdp.send('Page.printToPDF', { printBackground: true, preferCSSPageSize: true }, sessionId));
|
|
986
1097
|
const buffer = Buffer.from(data, 'base64');
|
|
987
|
-
const sessionsDir = getProfileSessionsDir(
|
|
1098
|
+
const sessionsDir = getProfileSessionsDir(runtimeKey, task.name);
|
|
988
1099
|
const automaticPath = path.join(sessionsDir, `${Date.now()}.pdf`);
|
|
989
1100
|
const finalPath = resolveScreenshotOutputPath(outputPath, automaticPath);
|
|
990
1101
|
await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
|
|
@@ -1002,7 +1113,7 @@ export class BrowserService {
|
|
|
1002
1113
|
if (this.recordings.has(taskId)) {
|
|
1003
1114
|
throw new Error(`Task "${taskId}" is already recording. Call record stop first.`);
|
|
1004
1115
|
}
|
|
1005
|
-
const { conn, task,
|
|
1116
|
+
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
1006
1117
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1007
1118
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1008
1119
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1018,7 +1129,7 @@ export class BrowserService {
|
|
|
1018
1129
|
throw new Error('--duration must be between 1 and 3600 seconds');
|
|
1019
1130
|
if (maxMb < 1 || maxMb > 500)
|
|
1020
1131
|
throw new Error('--max-mb must be between 1 and 500');
|
|
1021
|
-
const recordingsDir = path.join(getProfileSessionsDir(
|
|
1132
|
+
const recordingsDir = path.join(getProfileSessionsDir(runtimeKey, task.name), 'recordings');
|
|
1022
1133
|
await fs.promises.mkdir(recordingsDir, { recursive: true });
|
|
1023
1134
|
const outputPath = path.join(recordingsDir, `${Date.now()}.webm`);
|
|
1024
1135
|
// Resolve ffmpeg lazily so non-recording paths don't pay the import cost.
|
|
@@ -1412,34 +1523,37 @@ export class BrowserService {
|
|
|
1412
1523
|
stageUpload(source) {
|
|
1413
1524
|
return { path: stageUploadFile(source) };
|
|
1414
1525
|
}
|
|
1415
|
-
|
|
1526
|
+
/**
|
|
1527
|
+
* Status for one profile (bare name) or every live profile.
|
|
1528
|
+
*
|
|
1529
|
+
* `profileName` is a {@link ProfileName} — a BARE name, never a runtime key.
|
|
1530
|
+
* Every candidate is selected by the one rule ({@link keyBelongsToProfile}),
|
|
1531
|
+
* and a scoped query that finds no live connection falls through to disk, so
|
|
1532
|
+
* `status --profile comet-local` reports the LIVE `comet-local@endpoint-0`
|
|
1533
|
+
* instead of the empty list it used to return (RUSH-2709).
|
|
1534
|
+
*/
|
|
1535
|
+
async status(profileRef) {
|
|
1416
1536
|
// Reconnect live browsers after a daemon restart so status is not empty
|
|
1417
1537
|
// while tasks.json still holds open work.
|
|
1418
1538
|
await this.rehydrateAllFromDisk();
|
|
1419
|
-
|
|
1539
|
+
// A caller who pasted a runtime key out of an older listing still gets the
|
|
1540
|
+
// profile it belongs to, rather than an empty result.
|
|
1541
|
+
const profileName = profileRef ? parseConnectionKey(profileRef).profile : undefined;
|
|
1542
|
+
const seenProfiles = new Set();
|
|
1420
1543
|
const statuses = [];
|
|
1421
|
-
// Connections are keyed by composite `<profile>@<endpoint>`. A bare
|
|
1422
|
-
// `--profile comet-local` must still list those tasks (same rule as
|
|
1423
|
-
// stopProfile). Match exact key, bareName, and composite/fork prefixes.
|
|
1424
1544
|
const candidates = profileName
|
|
1425
|
-
? [...this.connections.keys()].filter((k) =>
|
|
1426
|
-
this.connections.get(k)?.bareName === profileName)
|
|
1545
|
+
? [...this.connections.keys()].filter((k) => keyBelongsToProfile(k, profileName))
|
|
1427
1546
|
: Array.from(this.connections.keys());
|
|
1428
|
-
for (const
|
|
1429
|
-
const status = await this.getProfileStatus(
|
|
1547
|
+
for (const key of candidates) {
|
|
1548
|
+
const status = await this.getProfileStatus(key);
|
|
1430
1549
|
if (status) {
|
|
1431
1550
|
statuses.push(status);
|
|
1432
|
-
|
|
1433
|
-
if (status.name)
|
|
1434
|
-
seen.add(status.name);
|
|
1551
|
+
seenProfiles.add(status.name);
|
|
1435
1552
|
}
|
|
1436
1553
|
}
|
|
1437
1554
|
if (!profileName) {
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
if (seen.has(profile.name))
|
|
1441
|
-
continue;
|
|
1442
|
-
if ([...seen].some((s) => connectionKeyMatchesProfile(s, profile.name)))
|
|
1555
|
+
for (const profile of await listProfiles()) {
|
|
1556
|
+
if (seenProfiles.has(profile.name))
|
|
1443
1557
|
continue;
|
|
1444
1558
|
const reconciled = await this.reconcileFromDisk(profile.name);
|
|
1445
1559
|
if (reconciled)
|
|
@@ -1453,31 +1567,28 @@ export class BrowserService {
|
|
|
1453
1567
|
}
|
|
1454
1568
|
return statuses;
|
|
1455
1569
|
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Rebuild a profile's status from its runtime dirs when no live connection is
|
|
1572
|
+
* registered — a daemon that never connected this profile, or a browser
|
|
1573
|
+
* started by an earlier daemon.
|
|
1574
|
+
*
|
|
1575
|
+
* Takes a BARE profile name and walks every runtime key that belongs to it
|
|
1576
|
+
* ({@link listProfileCacheDirs}), including the legacy pre-composite dir named
|
|
1577
|
+
* exactly the profile.
|
|
1578
|
+
*/
|
|
1456
1579
|
async reconcileFromDisk(profileName) {
|
|
1457
|
-
// Runtime dirs live under the composite (`comet-local@endpoint-0`), not the
|
|
1458
|
-
// bare YAML name. Walk every matching cache dir so bare `status --profile`
|
|
1459
|
-
// sees the same tasks as unscoped `status`.
|
|
1460
1580
|
const dirs = listProfileCacheDirs(profileName);
|
|
1461
|
-
if (dirs.length === 0) {
|
|
1462
|
-
// Legacy: non-composite dir named exactly the bare profile.
|
|
1463
|
-
const info = getRunningChromeInfo(profileName);
|
|
1464
|
-
if (!info)
|
|
1465
|
-
return null;
|
|
1466
|
-
return this.profileStatusFromDisk(profileName, profileName, info.port, info.pid);
|
|
1467
|
-
}
|
|
1468
|
-
// Prefer a dir that still has a live browser; fall back to the first.
|
|
1469
1581
|
for (const dir of dirs) {
|
|
1470
|
-
const
|
|
1471
|
-
const info = getRunningChromeInfo(
|
|
1582
|
+
const key = asConnectionKey(path.basename(dir));
|
|
1583
|
+
const info = getRunningChromeInfo(key);
|
|
1472
1584
|
if (!info)
|
|
1473
1585
|
continue;
|
|
1474
|
-
return this.profileStatusFromDisk(profileName,
|
|
1586
|
+
return this.profileStatusFromDisk(profileName, key, info.port, info.pid);
|
|
1475
1587
|
}
|
|
1476
1588
|
return null;
|
|
1477
1589
|
}
|
|
1478
|
-
async profileStatusFromDisk(
|
|
1479
|
-
const
|
|
1480
|
-
const profile = await getProfile(bare);
|
|
1590
|
+
async profileStatusFromDisk(profileName, runtimeKey, port, pid) {
|
|
1591
|
+
const profile = await getProfile(profileName);
|
|
1481
1592
|
const tasks = this.loadTaskState(runtimeKey);
|
|
1482
1593
|
const taskStatuses = [];
|
|
1483
1594
|
for (const [, task] of tasks) {
|
|
@@ -1492,7 +1603,10 @@ export class BrowserService {
|
|
|
1492
1603
|
}
|
|
1493
1604
|
const configuredPort = profile ? extractConfiguredPort(profile) : undefined;
|
|
1494
1605
|
return {
|
|
1495
|
-
name
|
|
1606
|
+
// Bare name for the user; the endpoint travels in its own field.
|
|
1607
|
+
name: profileName,
|
|
1608
|
+
endpoint: parseConnectionKey(runtimeKey).endpoint,
|
|
1609
|
+
key: runtimeKey,
|
|
1496
1610
|
running: true,
|
|
1497
1611
|
port,
|
|
1498
1612
|
pid,
|
|
@@ -1804,8 +1918,8 @@ export class BrowserService {
|
|
|
1804
1918
|
* effort: a remote CDP endpoint that doesn't expose the Browser domain must not
|
|
1805
1919
|
* fail the whole connect.
|
|
1806
1920
|
*/
|
|
1807
|
-
async applyDefaultDownloadBehavior(conn,
|
|
1808
|
-
const downloadPath = getProfileDownloadsDir(
|
|
1921
|
+
async applyDefaultDownloadBehavior(conn, key) {
|
|
1922
|
+
const downloadPath = getProfileDownloadsDir(key);
|
|
1809
1923
|
try {
|
|
1810
1924
|
await fs.promises.mkdir(downloadPath, { recursive: true });
|
|
1811
1925
|
await conn.cdp.send('Browser.setDownloadBehavior', {
|
|
@@ -1821,7 +1935,7 @@ export class BrowserService {
|
|
|
1821
1935
|
}
|
|
1822
1936
|
}
|
|
1823
1937
|
async setDownloadPath(taskId, downloadPath, tabHint) {
|
|
1824
|
-
const { conn, task,
|
|
1938
|
+
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
1825
1939
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1826
1940
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1827
1941
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1830,7 +1944,7 @@ export class BrowserService {
|
|
|
1830
1944
|
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
1831
1945
|
// No explicit --path: fall back to the profile's downloads dir, the same
|
|
1832
1946
|
// destination already set browser-global at connect (applyDefaultDownloadBehavior).
|
|
1833
|
-
const resolvedPath = downloadPath ?? getProfileDownloadsDir(
|
|
1947
|
+
const resolvedPath = downloadPath ?? getProfileDownloadsDir(runtimeKey);
|
|
1834
1948
|
await fs.promises.mkdir(resolvedPath, { recursive: true });
|
|
1835
1949
|
await conn.cdp.send('Browser.setDownloadBehavior', {
|
|
1836
1950
|
behavior: 'allow',
|
|
@@ -1908,20 +2022,20 @@ export class BrowserService {
|
|
|
1908
2022
|
}
|
|
1909
2023
|
this.connections.clear();
|
|
1910
2024
|
}
|
|
1911
|
-
findAvailableFork(
|
|
2025
|
+
findAvailableFork(baseKey) {
|
|
1912
2026
|
for (const [name, conn] of this.connections) {
|
|
1913
|
-
if (conn.forkedFrom ===
|
|
2027
|
+
if (conn.forkedFrom === baseKey && conn.tasks.size === 0) {
|
|
1914
2028
|
return { name, conn };
|
|
1915
2029
|
}
|
|
1916
2030
|
}
|
|
1917
2031
|
return null;
|
|
1918
2032
|
}
|
|
1919
|
-
async forkElectronProfile(profile) {
|
|
2033
|
+
async forkElectronProfile(profile, baseKey) {
|
|
1920
2034
|
let forkNum = 2;
|
|
1921
|
-
while (this.connections.has(`${
|
|
2035
|
+
while (this.connections.has(asConnectionKey(`${baseKey}.${forkNum}`))) {
|
|
1922
2036
|
forkNum++;
|
|
1923
2037
|
}
|
|
1924
|
-
const forkName = `${
|
|
2038
|
+
const forkName = asConnectionKey(`${baseKey}.${forkNum}`);
|
|
1925
2039
|
const port = allocatePort();
|
|
1926
2040
|
const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
|
|
1927
2041
|
const { pid, wsUrl } = await launchBrowser(forkName, profile.browser, port, chromeOpts, profile.secrets, profile.binary, profile.electron === true);
|
|
@@ -1935,8 +2049,9 @@ export class BrowserService {
|
|
|
1935
2049
|
electron: true,
|
|
1936
2050
|
browserType: profile.browser,
|
|
1937
2051
|
targetFilter: profile.targetFilter,
|
|
1938
|
-
|
|
1939
|
-
|
|
2052
|
+
key: forkName,
|
|
2053
|
+
profile: profile.name,
|
|
2054
|
+
forkedFrom: baseKey,
|
|
1940
2055
|
tasks: new Map(),
|
|
1941
2056
|
sessionCache: new Map(),
|
|
1942
2057
|
};
|
|
@@ -1949,12 +2064,13 @@ export class BrowserService {
|
|
|
1949
2064
|
* already resolved the endpoint and built the `effectiveProfile` with
|
|
1950
2065
|
* the per-endpoint binary/targetFilter overrides applied; we just use it.
|
|
1951
2066
|
*
|
|
1952
|
-
* `
|
|
1953
|
-
* so per-endpoint pid/port files don't collide when the same app
|
|
1954
|
-
* locally and remotely at the same time.
|
|
2067
|
+
* `key` is the runtime key (`<profile>@<endpoint>`) and is what every on-disk
|
|
2068
|
+
* lookup uses, so per-endpoint pid/port files don't collide when the same app
|
|
2069
|
+
* runs locally and remotely at the same time. It is a SEPARATE argument
|
|
2070
|
+
* because `effectiveProfile.name` stays the bare, user-facing name (RUSH-2709).
|
|
1955
2071
|
*/
|
|
1956
|
-
async connectProfile(effectiveProfile, target) {
|
|
1957
|
-
const existingInfo = getRunningChromeInfo(
|
|
2072
|
+
async connectProfile(effectiveProfile, target, key) {
|
|
2073
|
+
const existingInfo = getRunningChromeInfo(key);
|
|
1958
2074
|
if (existingInfo) {
|
|
1959
2075
|
try {
|
|
1960
2076
|
const { wsUrl, browser } = await discoverBrowserWsUrl(existingInfo.port, 'localhost', effectiveProfile.name);
|
|
@@ -1962,7 +2078,7 @@ export class BrowserService {
|
|
|
1962
2078
|
const cdp = new CDPClient();
|
|
1963
2079
|
await cdp.connect(wsUrl);
|
|
1964
2080
|
await this.enableDomains(cdp);
|
|
1965
|
-
const tasks = this.loadTaskState(
|
|
2081
|
+
const tasks = this.loadTaskState(key);
|
|
1966
2082
|
return {
|
|
1967
2083
|
cdp,
|
|
1968
2084
|
port: existingInfo.port,
|
|
@@ -1981,19 +2097,19 @@ export class BrowserService {
|
|
|
1981
2097
|
// or because the OS reused the pid for an unrelated process.
|
|
1982
2098
|
// Wipe the stale runtime files and fall through to a fresh
|
|
1983
2099
|
// connect against the profile's currently-configured endpoint.
|
|
1984
|
-
clearProfileRuntime(
|
|
2100
|
+
clearProfileRuntime(key);
|
|
1985
2101
|
}
|
|
1986
2102
|
}
|
|
1987
|
-
const conn = await this.connectEndpoint(effectiveProfile, target);
|
|
2103
|
+
const conn = await this.connectEndpoint(effectiveProfile, target, key);
|
|
1988
2104
|
if (!conn) {
|
|
1989
2105
|
throw new Error(`Could not connect to endpoint ${target} for profile "${effectiveProfile.name}"`);
|
|
1990
2106
|
}
|
|
1991
2107
|
return conn;
|
|
1992
2108
|
}
|
|
1993
|
-
async connectEndpoint(profile, endpoint) {
|
|
2109
|
+
async connectEndpoint(profile, endpoint, key) {
|
|
1994
2110
|
const url = new URL(endpoint);
|
|
1995
2111
|
if (url.protocol === 'cdp:') {
|
|
1996
|
-
const conn = await connectLocal(endpoint, profile);
|
|
2112
|
+
const conn = await connectLocal(endpoint, profile, key);
|
|
1997
2113
|
await this.enableDomains(conn.cdp);
|
|
1998
2114
|
return {
|
|
1999
2115
|
cdp: conn.cdp,
|
|
@@ -2002,12 +2118,12 @@ export class BrowserService {
|
|
|
2002
2118
|
electron: profile.electron,
|
|
2003
2119
|
browserType: profile.browser,
|
|
2004
2120
|
targetFilter: profile.targetFilter,
|
|
2005
|
-
tasks: conn.pid === 0 ? this.loadTaskState(
|
|
2121
|
+
tasks: conn.pid === 0 ? this.loadTaskState(key) : new Map(),
|
|
2006
2122
|
sessionCache: new Map(),
|
|
2007
2123
|
};
|
|
2008
2124
|
}
|
|
2009
2125
|
if (url.protocol === 'ssh:') {
|
|
2010
|
-
const conn = await connectSSH(endpoint, profile);
|
|
2126
|
+
const conn = await connectSSH(endpoint, profile, key);
|
|
2011
2127
|
await this.enableDomains(conn.cdp);
|
|
2012
2128
|
return {
|
|
2013
2129
|
cdp: conn.cdp,
|
|
@@ -2038,7 +2154,7 @@ export class BrowserService {
|
|
|
2038
2154
|
electron: profile.electron,
|
|
2039
2155
|
browserType: profile.browser,
|
|
2040
2156
|
targetFilter: profile.targetFilter,
|
|
2041
|
-
tasks: this.loadTaskState(
|
|
2157
|
+
tasks: this.loadTaskState(key),
|
|
2042
2158
|
sessionCache: new Map(),
|
|
2043
2159
|
};
|
|
2044
2160
|
}
|
|
@@ -2056,7 +2172,7 @@ export class BrowserService {
|
|
|
2056
2172
|
electron: profile.electron,
|
|
2057
2173
|
browserType: profile.browser,
|
|
2058
2174
|
targetFilter: profile.targetFilter,
|
|
2059
|
-
tasks: this.loadTaskState(
|
|
2175
|
+
tasks: this.loadTaskState(key),
|
|
2060
2176
|
sessionCache: new Map(),
|
|
2061
2177
|
};
|
|
2062
2178
|
}
|
|
@@ -2074,7 +2190,7 @@ export class BrowserService {
|
|
|
2074
2190
|
*/
|
|
2075
2191
|
async createPageTarget(conn, params) {
|
|
2076
2192
|
if (conn.browserType === 'arc') {
|
|
2077
|
-
throw arcNotDrivableError(conn.
|
|
2193
|
+
throw arcNotDrivableError(conn.profile);
|
|
2078
2194
|
}
|
|
2079
2195
|
return (await conn.cdp.send('Target.createTarget', params));
|
|
2080
2196
|
}
|
|
@@ -2177,7 +2293,7 @@ export class BrowserService {
|
|
|
2177
2293
|
if (matches.length === 1) {
|
|
2178
2294
|
const m = matches[0];
|
|
2179
2295
|
await this.touchTask(m.conn, m.task);
|
|
2180
|
-
return { conn: m.conn, task: m.task,
|
|
2296
|
+
return { conn: m.conn, task: m.task, key: m.key, created: false };
|
|
2181
2297
|
}
|
|
2182
2298
|
if (matches.length > 1) {
|
|
2183
2299
|
const lines = matches.map((m) => {
|
|
@@ -2217,7 +2333,7 @@ export class BrowserService {
|
|
|
2217
2333
|
for (const task of conn.tasks.values()) {
|
|
2218
2334
|
if (hasIdentity) {
|
|
2219
2335
|
if (taskMatchesCaller(task, caller)) {
|
|
2220
|
-
out.push({ conn, task,
|
|
2336
|
+
out.push({ conn, task, key: conn.key ?? key });
|
|
2221
2337
|
}
|
|
2222
2338
|
}
|
|
2223
2339
|
else {
|
|
@@ -2225,7 +2341,7 @@ export class BrowserService {
|
|
|
2225
2341
|
// (a human-driven shell). If there is exactly one live task total,
|
|
2226
2342
|
// the single-task path still needs that listed — fall through below.
|
|
2227
2343
|
if (!task.sessionId && !task.launchId) {
|
|
2228
|
-
out.push({ conn, task,
|
|
2344
|
+
out.push({ conn, task, key: conn.key ?? key });
|
|
2229
2345
|
}
|
|
2230
2346
|
}
|
|
2231
2347
|
}
|
|
@@ -2240,7 +2356,7 @@ export class BrowserService {
|
|
|
2240
2356
|
const conn = this.connections.get(only.profile) ??
|
|
2241
2357
|
[...this.connections.entries()].find(([, c]) => c.tasks.has(only.task.name) || [...c.tasks.values()].includes(only.task))?.[1];
|
|
2242
2358
|
if (conn) {
|
|
2243
|
-
out.push({ conn, task: only.task,
|
|
2359
|
+
out.push({ conn, task: only.task, key: only.profile });
|
|
2244
2360
|
}
|
|
2245
2361
|
}
|
|
2246
2362
|
}
|
|
@@ -2291,33 +2407,34 @@ export class BrowserService {
|
|
|
2291
2407
|
// `recordBrowserSession` call in `start` uses for the same reason.
|
|
2292
2408
|
}
|
|
2293
2409
|
}
|
|
2294
|
-
async findTask(taskId,
|
|
2295
|
-
if (
|
|
2410
|
+
async findTask(taskId, profileRef) {
|
|
2411
|
+
if (profileRef) {
|
|
2296
2412
|
// Accept bare profile names against composite connection keys.
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2413
|
+
// One rule: an exact runtime key, else every key belonging to that bare
|
|
2414
|
+
// profile. Callers pass whichever they hold — the composite is theirs to
|
|
2415
|
+
// ignore (RUSH-2709).
|
|
2416
|
+
const keys = [...this.connections.keys()].filter((k) => k === profileRef || keyBelongsToProfile(k, profileRef));
|
|
2300
2417
|
for (const key of keys) {
|
|
2301
2418
|
const conn = this.connections.get(key);
|
|
2302
2419
|
const task = this.lookupTaskOnConn(conn, taskId);
|
|
2303
2420
|
if (task) {
|
|
2304
2421
|
await this.touchTask(conn, task);
|
|
2305
|
-
return { conn, task,
|
|
2422
|
+
return { conn, task, key: conn.key ?? key };
|
|
2306
2423
|
}
|
|
2307
2424
|
}
|
|
2308
2425
|
// RAM miss → rehydrate from disk for this profile.
|
|
2309
|
-
const rehydrated = await this.rehydrateTaskFromDisk(taskId,
|
|
2426
|
+
const rehydrated = await this.rehydrateTaskFromDisk(taskId, profileRef);
|
|
2310
2427
|
if (rehydrated) {
|
|
2311
2428
|
await this.touchTask(rehydrated.conn, rehydrated.task);
|
|
2312
2429
|
return rehydrated;
|
|
2313
2430
|
}
|
|
2314
|
-
throw new Error(actionable(`Task "${taskId}" not found on profile "${
|
|
2431
|
+
throw new Error(actionable(`Task "${taskId}" not found on profile "${profileRef}".`, `Next: agents browser status --profile ${parseConnectionKey(profileRef).profile}`));
|
|
2315
2432
|
}
|
|
2316
2433
|
for (const [key, conn] of this.connections) {
|
|
2317
2434
|
const task = this.lookupTaskOnConn(conn, taskId);
|
|
2318
2435
|
if (task) {
|
|
2319
2436
|
await this.touchTask(conn, task);
|
|
2320
|
-
return { conn, task,
|
|
2437
|
+
return { conn, task, key: conn.key ?? key };
|
|
2321
2438
|
}
|
|
2322
2439
|
}
|
|
2323
2440
|
// RAM miss after a daemon restart: tasks.json still has the task and
|
|
@@ -2341,11 +2458,8 @@ export class BrowserService {
|
|
|
2341
2458
|
* Returns null when nothing is speaking CDP so callers can fall through
|
|
2342
2459
|
* to disk reconcile.
|
|
2343
2460
|
*/
|
|
2344
|
-
async attachRunningProfile(
|
|
2345
|
-
const bare
|
|
2346
|
-
const endpointFromKey = dirName.includes('@')
|
|
2347
|
-
? dirName.slice(dirName.indexOf('@') + 1).split('.')[0]
|
|
2348
|
-
: undefined;
|
|
2461
|
+
async attachRunningProfile(key, diskTasks) {
|
|
2462
|
+
const { profile: bare, endpoint: endpointFromKey } = parseConnectionKey(key);
|
|
2349
2463
|
const profile = await getProfile(bare);
|
|
2350
2464
|
if (!profile)
|
|
2351
2465
|
return null;
|
|
@@ -2356,7 +2470,7 @@ export class BrowserService {
|
|
|
2356
2470
|
catch {
|
|
2357
2471
|
return null;
|
|
2358
2472
|
}
|
|
2359
|
-
const existingInfo = getRunningChromeInfo(
|
|
2473
|
+
const existingInfo = getRunningChromeInfo(key);
|
|
2360
2474
|
const parsed = parseEndpointUrl(resolved.target);
|
|
2361
2475
|
const port = existingInfo?.port ?? parsed?.port;
|
|
2362
2476
|
if (port === undefined)
|
|
@@ -2366,12 +2480,12 @@ export class BrowserService {
|
|
|
2366
2480
|
if (resolved.target.startsWith('ssh:'))
|
|
2367
2481
|
return null;
|
|
2368
2482
|
try {
|
|
2369
|
-
const { wsUrl, browser } = await discoverBrowserWsUrl(port, host,
|
|
2483
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(port, host, bare);
|
|
2370
2484
|
verifyBrowserIdentity(browser, profile.browser, port, host);
|
|
2371
2485
|
const cdp = new CDPClient();
|
|
2372
2486
|
await cdp.connect(wsUrl);
|
|
2373
2487
|
await this.enableDomains(cdp);
|
|
2374
|
-
const tasks = this.loadTaskState(
|
|
2488
|
+
const tasks = this.loadTaskState(key);
|
|
2375
2489
|
for (const [k, t] of diskTasks) {
|
|
2376
2490
|
if (!tasks.has(k))
|
|
2377
2491
|
tasks.set(k, t);
|
|
@@ -2383,8 +2497,8 @@ export class BrowserService {
|
|
|
2383
2497
|
electron: profile.electron,
|
|
2384
2498
|
browserType: profile.browser,
|
|
2385
2499
|
targetFilter: resolved.targetFilter ?? profile.targetFilter,
|
|
2386
|
-
|
|
2387
|
-
|
|
2500
|
+
key,
|
|
2501
|
+
profile: bare,
|
|
2388
2502
|
tasks,
|
|
2389
2503
|
sessionCache: new Map(),
|
|
2390
2504
|
};
|
|
@@ -2416,21 +2530,23 @@ export class BrowserService {
|
|
|
2416
2530
|
return;
|
|
2417
2531
|
}
|
|
2418
2532
|
for (const dirName of dirNames) {
|
|
2419
|
-
|
|
2533
|
+
// A runtime dir name IS a connection key — that is what wrote it.
|
|
2534
|
+
const key = asConnectionKey(dirName);
|
|
2535
|
+
if (this.connections.has(key))
|
|
2420
2536
|
continue;
|
|
2421
2537
|
// Skip non-runtime dirs (e.g. profiles/)
|
|
2422
2538
|
if (dirName === 'profiles' || dirName === 'sessions' || dirName === 'exports')
|
|
2423
2539
|
continue;
|
|
2424
|
-
const tasks = this.loadTaskState(
|
|
2540
|
+
const tasks = this.loadTaskState(key);
|
|
2425
2541
|
if (tasks.size === 0)
|
|
2426
2542
|
continue;
|
|
2427
2543
|
// Soft attach only — never launch / never clear pid files.
|
|
2428
|
-
const conn = await this.attachRunningProfile(
|
|
2544
|
+
const conn = await this.attachRunningProfile(key, tasks);
|
|
2429
2545
|
if (!conn)
|
|
2430
2546
|
continue;
|
|
2431
|
-
this.connections.set(
|
|
2547
|
+
this.connections.set(key, conn);
|
|
2432
2548
|
try {
|
|
2433
|
-
await this.applyDefaultDownloadBehavior(conn,
|
|
2549
|
+
await this.applyDefaultDownloadBehavior(conn, key);
|
|
2434
2550
|
}
|
|
2435
2551
|
catch {
|
|
2436
2552
|
// Non-fatal for rehydrate.
|
|
@@ -2459,12 +2575,13 @@ export class BrowserService {
|
|
|
2459
2575
|
return null;
|
|
2460
2576
|
}
|
|
2461
2577
|
if (profileHint) {
|
|
2462
|
-
dirNames = dirNames.filter((d) => d === profileHint ||
|
|
2578
|
+
dirNames = dirNames.filter((d) => d === profileHint || keyBelongsToProfile(d, profileHint));
|
|
2463
2579
|
}
|
|
2464
2580
|
for (const dirName of dirNames) {
|
|
2465
2581
|
if (dirName === 'profiles' || dirName === 'sessions' || dirName === 'exports')
|
|
2466
2582
|
continue;
|
|
2467
|
-
const
|
|
2583
|
+
const key = asConnectionKey(dirName);
|
|
2584
|
+
const tasks = this.loadTaskState(key);
|
|
2468
2585
|
let found;
|
|
2469
2586
|
for (const task of tasks.values()) {
|
|
2470
2587
|
if (task.id === taskId || task.name === taskId) {
|
|
@@ -2478,17 +2595,17 @@ export class BrowserService {
|
|
|
2478
2595
|
if (!found)
|
|
2479
2596
|
continue;
|
|
2480
2597
|
// Already connected under this key?
|
|
2481
|
-
let conn = this.connections.get(
|
|
2598
|
+
let conn = this.connections.get(key);
|
|
2482
2599
|
if (!conn) {
|
|
2483
|
-
const attached = await this.attachRunningProfile(
|
|
2600
|
+
const attached = await this.attachRunningProfile(key, tasks);
|
|
2484
2601
|
if (!attached) {
|
|
2485
2602
|
// CDP down — cannot rehydrate a live connection; caller will error.
|
|
2486
2603
|
continue;
|
|
2487
2604
|
}
|
|
2488
2605
|
conn = attached;
|
|
2489
|
-
this.connections.set(
|
|
2606
|
+
this.connections.set(key, conn);
|
|
2490
2607
|
try {
|
|
2491
|
-
await this.applyDefaultDownloadBehavior(conn,
|
|
2608
|
+
await this.applyDefaultDownloadBehavior(conn, key);
|
|
2492
2609
|
}
|
|
2493
2610
|
catch {
|
|
2494
2611
|
// Non-fatal.
|
|
@@ -2499,7 +2616,7 @@ export class BrowserService {
|
|
|
2499
2616
|
if (!this.lookupTaskOnConn(conn, task.name)) {
|
|
2500
2617
|
conn.tasks.set(task.name, task);
|
|
2501
2618
|
}
|
|
2502
|
-
return { conn, task,
|
|
2619
|
+
return { conn, task, key: conn.key ?? key };
|
|
2503
2620
|
}
|
|
2504
2621
|
return null;
|
|
2505
2622
|
}
|
|
@@ -2519,20 +2636,9 @@ export class BrowserService {
|
|
|
2519
2636
|
}
|
|
2520
2637
|
return tabs;
|
|
2521
2638
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
let resolvedKey = profileName;
|
|
2526
|
-
if (!conn) {
|
|
2527
|
-
for (const [key, c] of this.connections) {
|
|
2528
|
-
if (connectionKeyMatchesProfile(key, profileName) ||
|
|
2529
|
-
c.bareName === profileName) {
|
|
2530
|
-
conn = c;
|
|
2531
|
-
resolvedKey = key;
|
|
2532
|
-
break;
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2639
|
+
/** Live status for ONE registered connection, addressed by its runtime key. */
|
|
2640
|
+
async getProfileStatus(key) {
|
|
2641
|
+
const conn = this.connections.get(key);
|
|
2536
2642
|
if (!conn)
|
|
2537
2643
|
return null;
|
|
2538
2644
|
// Fetch all targets once for efficiency
|
|
@@ -2578,11 +2684,14 @@ export class BrowserService {
|
|
|
2578
2684
|
domains: domainSet.size > 0 ? Array.from(domainSet) : undefined,
|
|
2579
2685
|
});
|
|
2580
2686
|
}
|
|
2581
|
-
const
|
|
2687
|
+
const parsed = parseConnectionKey(key);
|
|
2688
|
+
const bare = conn.profile ?? parsed.profile;
|
|
2582
2689
|
const profile = await getProfile(bare);
|
|
2583
2690
|
const configuredPort = profile ? extractConfiguredPort(profile) : undefined;
|
|
2584
2691
|
return {
|
|
2585
|
-
name:
|
|
2692
|
+
name: bare,
|
|
2693
|
+
endpoint: parsed.endpoint,
|
|
2694
|
+
key,
|
|
2586
2695
|
running: true,
|
|
2587
2696
|
port: conn.port,
|
|
2588
2697
|
pid: conn.pid,
|
|
@@ -2642,14 +2751,14 @@ export class BrowserService {
|
|
|
2642
2751
|
invalidateTargetCache(conn) {
|
|
2643
2752
|
conn.targetCache = undefined;
|
|
2644
2753
|
}
|
|
2645
|
-
async saveTaskState(
|
|
2646
|
-
const runtimeDir = getProfileRuntimeDir(
|
|
2754
|
+
async saveTaskState(key, tasks) {
|
|
2755
|
+
const runtimeDir = getProfileRuntimeDir(key);
|
|
2647
2756
|
await fs.promises.mkdir(runtimeDir, { recursive: true });
|
|
2648
2757
|
const state = Object.fromEntries(tasks);
|
|
2649
2758
|
await fs.promises.writeFile(path.join(runtimeDir, 'tasks.json'), JSON.stringify(state, null, 2));
|
|
2650
2759
|
}
|
|
2651
|
-
loadTaskState(
|
|
2652
|
-
const runtimeDir = getProfileRuntimeDir(
|
|
2760
|
+
loadTaskState(key) {
|
|
2761
|
+
const runtimeDir = getProfileRuntimeDir(key);
|
|
2653
2762
|
const tasksFile = path.join(runtimeDir, 'tasks.json');
|
|
2654
2763
|
if (!fs.existsSync(tasksFile)) {
|
|
2655
2764
|
return new Map();
|
|
@@ -2671,7 +2780,7 @@ export class BrowserService {
|
|
|
2671
2780
|
tasks.set(key, {
|
|
2672
2781
|
id: task.id,
|
|
2673
2782
|
name: task.name || key,
|
|
2674
|
-
profile: task.profile,
|
|
2783
|
+
profile: asConnectionKey(task.profile),
|
|
2675
2784
|
tabs,
|
|
2676
2785
|
currentTabId: tabIds.length > 0 ? tabIds[tabIds.length - 1] : undefined,
|
|
2677
2786
|
createdAt: task.createdAt,
|
|
@@ -2713,7 +2822,9 @@ export class BrowserService {
|
|
|
2713
2822
|
history.unshift({
|
|
2714
2823
|
id: task.id,
|
|
2715
2824
|
name: task.name,
|
|
2716
|
-
|
|
2825
|
+
// `task.profile` is the runtime key; history is a user-facing table, so
|
|
2826
|
+
// it carries the bare name (RUSH-2709).
|
|
2827
|
+
profile: parseConnectionKey(task.profile).profile,
|
|
2717
2828
|
createdAt: task.createdAt,
|
|
2718
2829
|
endedAt: Date.now(),
|
|
2719
2830
|
domains,
|