@phnx-labs/agents-cli 1.22.114 → 1.22.116
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 +245 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +210 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +214 -148
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/feed.js +65 -14
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +158 -11
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-term.d.ts +24 -0
- package/dist/commands/setup-term.js +70 -0
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +29 -11
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +93 -302
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +3 -9
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +6 -5
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +229 -37
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +141 -0
- package/dist/lib/feed/hub-server.js +586 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +72 -1
- package/dist/lib/hosts/remote-cmd.js +151 -10
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +27 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +220 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- package/dist/lib/storage/visibility.js +0 -99
|
@@ -258,6 +258,39 @@ function readTaskCount(dir) {
|
|
|
258
258
|
* alone — we'd rather leak than wrongly kill a user-owned process that
|
|
259
259
|
* happens to share metadata.
|
|
260
260
|
*/
|
|
261
|
+
/**
|
|
262
|
+
* Clean up what a DEAD daemon genuinely left behind — which is not the browser.
|
|
263
|
+
*
|
|
264
|
+
* A live local browser outliving its daemon is the NORMAL case, not an orphan.
|
|
265
|
+
* `BrowserService.shutdown` deliberately closes only CDP and leaves the browser
|
|
266
|
+
* running, and on macOS the spawn is detached so the daemon's own positive-pid
|
|
267
|
+
* `killTree` reaches the daemon alone. This reaper used to read "the recorded
|
|
268
|
+
* `daemonPid` has exited" as "everything it recorded is garbage" and SIGTERM the
|
|
269
|
+
* browser — so replacing the daemon (a menubar activation, an upgrade, a crash
|
|
270
|
+
* restart) killed a browser the user was looking at, and `clearProfileRuntime`
|
|
271
|
+
* then removed the `port`/meta that {@link readProfileRuntime} needs, so even a
|
|
272
|
+
* survivor could not be re-attached and a second browser would launch beside it.
|
|
273
|
+
*
|
|
274
|
+
* The three cases are now decided independently:
|
|
275
|
+
*
|
|
276
|
+
* - **A live local browser/electron is preserved untouched** — not signalled, not
|
|
277
|
+
* cleared, and its `daemonPid` deliberately NOT rewritten. Ownership is adopted
|
|
278
|
+
* only after an identity-checked attach succeeds (`adoptProfileRuntimeOwner`,
|
|
279
|
+
* called from the reuse branch in `BrowserService.connectProfile`), so nothing
|
|
280
|
+
* claims a browser it has not proven it can talk to. Re-running this is a no-op.
|
|
281
|
+
* - **A dead browser record is cleared**, with no signal sent: there is nothing
|
|
282
|
+
* alive to signal, and the stale pid/port would otherwise mislead the next
|
|
283
|
+
* attach.
|
|
284
|
+
* - **A stale tunnel IS reaped.** A local `ssh -L` cannot outlive its purpose, so
|
|
285
|
+
* a dead owner does make it an orphan. This is the one thing the old code was
|
|
286
|
+
* right about; it just shared a code path with the browser.
|
|
287
|
+
*
|
|
288
|
+
* The accepted trade: a genuinely leaked browser from a long-dead daemon is no
|
|
289
|
+
* longer force-killed here. Idle/abandoned cleanup has its own owner in
|
|
290
|
+
* `browser/hygiene.ts` (session-dead + idle), which the daemon's periodic tick and
|
|
291
|
+
* `agents browser prune` both call — that is the right place for it, because it can
|
|
292
|
+
* tell "idle and abandoned" from "in use right now" and this function cannot.
|
|
293
|
+
*/
|
|
261
294
|
export function reapOrphanedProcesses() {
|
|
262
295
|
const root = getBrowserRuntimeDir();
|
|
263
296
|
if (!fs.existsSync(root))
|
|
@@ -275,28 +308,76 @@ export function reapOrphanedProcesses() {
|
|
|
275
308
|
// Owning daemon still alive — leave its kids alone.
|
|
276
309
|
if (isProcessAlive(meta.daemonPid))
|
|
277
310
|
continue;
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
311
|
+
// A local `ssh -L` whose owner is gone is genuinely orphaned: nothing is
|
|
312
|
+
// driving it and it cannot be re-adopted, so it is the one process to signal.
|
|
313
|
+
// Matched against the recorded command (or `ssh`) so a recycled pid is not
|
|
314
|
+
// mistaken for our tunnel.
|
|
315
|
+
if (meta.tunnelPid && meta.tunnelPid !== 0) {
|
|
316
|
+
const cmdOk = !meta.command
|
|
317
|
+
|| matchesCommand(meta.tunnelPid, meta.command)
|
|
318
|
+
|| matchesCommand(meta.tunnelPid, 'ssh');
|
|
319
|
+
if (cmdOk) {
|
|
320
|
+
try {
|
|
321
|
+
process.kill(meta.tunnelPid, 'SIGTERM');
|
|
322
|
+
reaped++;
|
|
323
|
+
details.push(`reaped tunnel ${meta.tunnelPid} (profile ${profileName})`);
|
|
324
|
+
}
|
|
325
|
+
catch { /* already gone */ }
|
|
291
326
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
}
|
|
328
|
+
// A tunnel-kind record has no local browser of its own, so clearing it is the
|
|
329
|
+
// whole cleanup. `pid` on such a record is the REMOTE browser (normally 0) and
|
|
330
|
+
// is never ours to signal.
|
|
331
|
+
if (meta.kind === 'tunnel') {
|
|
332
|
+
clearProfileRuntime(profileName);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// `isProcessAlive` re-checks the recorded command, so a recycled pid does not
|
|
336
|
+
// read as our browser. A record with no pid at all is stale by definition.
|
|
337
|
+
const liveBrowser = !!meta.pid && meta.pid !== 0 && isProcessAlive(meta.pid, meta.command);
|
|
338
|
+
if (liveBrowser) {
|
|
339
|
+
details.push(`preserved live browser ${meta.pid} (profile ${profileName})`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
// Dead browser: clear the stale record. No signal — there is nothing there.
|
|
296
343
|
clearProfileRuntime(profileName);
|
|
344
|
+
if (meta.pid)
|
|
345
|
+
details.push(`cleared dead browser record ${meta.pid} (profile ${profileName})`);
|
|
297
346
|
}
|
|
298
347
|
return { reaped, details };
|
|
299
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Take ownership of a runtime record after an identity-checked attach succeeded.
|
|
351
|
+
*
|
|
352
|
+
* Called from the reuse branch in `BrowserService.connectProfile`, which has just
|
|
353
|
+
* probed `/json/version` and run `verifyBrowserIdentity` against the profile's
|
|
354
|
+
* expected browser family — so by this point the process on the recorded port is
|
|
355
|
+
* proven to be ours. Only then may a new service claim it.
|
|
356
|
+
*
|
|
357
|
+
* Rewrites `daemonPid` ALONE. Every other field — `pid`, `port`, `command`,
|
|
358
|
+
* `userDataDir`, `spawnedAt`, `kind`, `tunnelPid` — is the original launch's
|
|
359
|
+
* metadata and stays exactly as recorded: this is an ownership handover, not a
|
|
360
|
+
* re-registration, and rewriting the rest would lose the provenance the reaper and
|
|
361
|
+
* the hygiene pass both read. A record that has since disappeared is left alone
|
|
362
|
+
* rather than recreated from a partial view.
|
|
363
|
+
*/
|
|
364
|
+
export function adoptProfileRuntimeOwner(profileName, daemonPid = process.pid) {
|
|
365
|
+
const meta = readProfileRuntimeMeta(profileName);
|
|
366
|
+
if (!meta)
|
|
367
|
+
return false;
|
|
368
|
+
if (meta.daemonPid === daemonPid)
|
|
369
|
+
return true;
|
|
370
|
+
const dir = getProfileRuntimeDir(profileName);
|
|
371
|
+
try {
|
|
372
|
+
fs.writeFileSync(path.join(dir, META_FILE), JSON.stringify({ ...meta, daemonPid }));
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Losing the handover is not worth failing an otherwise good attach; the next
|
|
377
|
+
// startup simply preserves the browser again and re-attaches.
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
300
381
|
/**
|
|
301
382
|
* True when ANY cache dir belonging to this profile still has a live browser or
|
|
302
383
|
* SSH tunnel, or an open task. The composite naming (`<name>@<endpoint>`) means
|
|
@@ -478,7 +559,7 @@ function matchesCommand(pid, expectedCommand) {
|
|
|
478
559
|
* determined. The process-listing API differs per OS: Windows has no `ps`, so
|
|
479
560
|
* we query `tasklist` (CSV image name in column 1); POSIX uses `ps -o comm=`.
|
|
480
561
|
*/
|
|
481
|
-
function liveProcessCommand(pid) {
|
|
562
|
+
export function liveProcessCommand(pid) {
|
|
482
563
|
try {
|
|
483
564
|
if (process.platform === 'win32') {
|
|
484
565
|
const out = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], {
|
|
@@ -731,6 +731,16 @@ export declare class BrowserService {
|
|
|
731
731
|
* async/promise evaluation fails honestly with an AppleScript error.
|
|
732
732
|
*/
|
|
733
733
|
private evaluateArcNative;
|
|
734
|
+
/**
|
|
735
|
+
* Resolve a task's owned Arc tabs against one shared snapshot of every
|
|
736
|
+
* window/space/tab, rather than asking Arc per tab as `listArcTaskTabs` does.
|
|
737
|
+
*
|
|
738
|
+
* Same ownership rule as the live path — `requireArcTask`, then the recorded
|
|
739
|
+
* ref must be present in its original window and Space. The snapshot changes
|
|
740
|
+
* how often Arc is asked, never what counts as owned. Actions keep the live
|
|
741
|
+
* path, because a snapshot can go stale before it is acted on.
|
|
742
|
+
*/
|
|
743
|
+
private listArcTaskTabsFromSnapshot;
|
|
734
744
|
private listArcTaskTabs;
|
|
735
745
|
/**
|
|
736
746
|
* Close native Arc tabs owned by a task. Only closes tabs the task created
|
|
@@ -8,7 +8,7 @@ import { assertRemoteControlAllowedForRequest } from './remote-control.js';
|
|
|
8
8
|
import { connectLocal } from './drivers/local.js';
|
|
9
9
|
import { connectSSH, shellQuote } from './drivers/ssh.js';
|
|
10
10
|
import { adoptLegacyRuntimeIfLocal, isLegacyEndpointKey, resolveBrowserTarget, shouldForkProfile, } from './resolve-target.js';
|
|
11
|
-
import { clearProfileRuntime, listProfileCacheDirs, readProfileRuntimeMeta, isProcessAlive } from './runtime-state.js';
|
|
11
|
+
import { adoptProfileRuntimeOwner, clearProfileRuntime, listProfileCacheDirs, readProfileRuntimeMeta, isProcessAlive } from './runtime-state.js';
|
|
12
12
|
import { resolveDomainSkill } from './domain-skills.js';
|
|
13
13
|
import { generateTaskId, generateShortId, asConnectionKey, parseConnectionKey, keyBelongsToProfile, } from './types.js';
|
|
14
14
|
import { reapAbandonedTasks, resolveLiveIdentities, taskOwnerIsGone, } from './hygiene.js';
|
|
@@ -333,6 +333,11 @@ export function arcCreateMarker() {
|
|
|
333
333
|
export function isArcCreateMarker(url) {
|
|
334
334
|
return url.startsWith(ARC_CREATE_MARKER_PREFIX);
|
|
335
335
|
}
|
|
336
|
+
/** Run `fn` once per pass and share its promise, rejections included. */
|
|
337
|
+
function onceAsync(fn) {
|
|
338
|
+
let pending;
|
|
339
|
+
return () => (pending ??= fn());
|
|
340
|
+
}
|
|
336
341
|
export class BrowserService {
|
|
337
342
|
static SOURCE_PREFIX = {
|
|
338
343
|
'rush-app': 'rush-app-',
|
|
@@ -1212,6 +1217,8 @@ export class BrowserService {
|
|
|
1212
1217
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
1213
1218
|
return { tabId: resolvedTabId };
|
|
1214
1219
|
}
|
|
1220
|
+
requireCdp(conn, 'enumerate');
|
|
1221
|
+
await conn.cdp.send('Target.activateTarget', { targetId: task.tabs[resolvedTabId] });
|
|
1215
1222
|
task.currentTabId = resolvedTabId;
|
|
1216
1223
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
1217
1224
|
return { tabId: resolvedTabId };
|
|
@@ -2196,9 +2203,20 @@ export class BrowserService {
|
|
|
2196
2203
|
* it used to return (RUSH-2709).
|
|
2197
2204
|
*/
|
|
2198
2205
|
async status(profileRef) {
|
|
2206
|
+
// Built before rehydration: a cold pass rehydrates every saved runtime dir
|
|
2207
|
+
// first, and each Arc one probed Arc on its own, spending the budget before
|
|
2208
|
+
// the snapshot existed. One enumeration and one liveness read cover the pass.
|
|
2209
|
+
const arc = {
|
|
2210
|
+
spaces: onceAsync(() => enumerateArcSpaces()),
|
|
2211
|
+
running: onceAsync(() => isArcRunning()),
|
|
2212
|
+
};
|
|
2199
2213
|
// Reconnect live browsers after a daemon restart so status is not empty
|
|
2200
|
-
// while tasks.json still holds open work.
|
|
2201
|
-
|
|
2214
|
+
// while tasks.json still holds open work. Rehydration is per runtime dir:
|
|
2215
|
+
// one unreadable saved profile is reported below, never fatal to the pass.
|
|
2216
|
+
const rehydrateErrors = new Map();
|
|
2217
|
+
await this.rehydrateAllFromDisk(arc.running, (key, err) => {
|
|
2218
|
+
rehydrateErrors.set(key, err instanceof Error ? err.message : String(err));
|
|
2219
|
+
});
|
|
2202
2220
|
// A caller who pasted a runtime key out of an older listing still gets the
|
|
2203
2221
|
// profile it belongs to, rather than an empty result.
|
|
2204
2222
|
const profileName = profileRef ? parseConnectionKey(profileRef).profile : undefined;
|
|
@@ -2208,25 +2226,99 @@ export class BrowserService {
|
|
|
2208
2226
|
? [...this.connections.keys()].filter((k) => keyBelongsToProfile(k, profileName))
|
|
2209
2227
|
: Array.from(this.connections.keys());
|
|
2210
2228
|
for (const key of candidates) {
|
|
2211
|
-
|
|
2229
|
+
// Read-only: a profile that cannot be read is reported as unreadable, not
|
|
2230
|
+
// allowed to abort the listing. Before this, a single stale Arc task made
|
|
2231
|
+
// `status` throw and EVERY profile vanished, including healthy ones.
|
|
2232
|
+
let status;
|
|
2233
|
+
try {
|
|
2234
|
+
status = await this.getProfileStatus(key, arc);
|
|
2235
|
+
}
|
|
2236
|
+
catch (err) {
|
|
2237
|
+
const parsed = parseConnectionKey(key);
|
|
2238
|
+
statuses.push({
|
|
2239
|
+
name: this.connections.get(key)?.profile ?? parsed.profile,
|
|
2240
|
+
endpoint: parsed.endpoint,
|
|
2241
|
+
key,
|
|
2242
|
+
running: false,
|
|
2243
|
+
tasks: [],
|
|
2244
|
+
unavailable: err instanceof Error ? err.message : String(err),
|
|
2245
|
+
});
|
|
2246
|
+
seenProfiles.add(parsed.profile);
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2212
2249
|
if (status) {
|
|
2213
2250
|
statuses.push(status);
|
|
2214
2251
|
seenProfiles.add(status.name);
|
|
2215
2252
|
}
|
|
2216
2253
|
}
|
|
2254
|
+
// Reported BEFORE the disk reconcile, and keyed by the runtime dir that
|
|
2255
|
+
// actually failed. Reconcile reads the same `tasks.json`, so without this
|
|
2256
|
+
// ordering one corrupt runtime produced two rows for the same profile: one
|
|
2257
|
+
// keyed here and one unkeyed from reconcile, which the dedupe then missed.
|
|
2258
|
+
for (const [key, message] of rehydrateErrors) {
|
|
2259
|
+
const parsed = parseConnectionKey(key);
|
|
2260
|
+
if (profileName && parsed.profile !== profileName)
|
|
2261
|
+
continue;
|
|
2262
|
+
if (statuses.some((status) => status.key === key))
|
|
2263
|
+
continue;
|
|
2264
|
+
statuses.push({
|
|
2265
|
+
name: parsed.profile,
|
|
2266
|
+
endpoint: parsed.endpoint,
|
|
2267
|
+
key,
|
|
2268
|
+
running: false,
|
|
2269
|
+
tasks: [],
|
|
2270
|
+
unavailable: message,
|
|
2271
|
+
});
|
|
2272
|
+
seenProfiles.add(parsed.profile);
|
|
2273
|
+
}
|
|
2274
|
+
// Disk reconcile reads `tasks.json` too, so it fails on exactly the same
|
|
2275
|
+
// corrupt file — report that profile rather than let it empty the table.
|
|
2276
|
+
const reconcile = async (name) => {
|
|
2277
|
+
const failures = new Map();
|
|
2278
|
+
let reconciled = null;
|
|
2279
|
+
try {
|
|
2280
|
+
reconciled = await this.reconcileFromDisk(name, (key, err) => {
|
|
2281
|
+
failures.set(key, err instanceof Error ? err.message : String(err));
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
catch (err) {
|
|
2285
|
+
// Listing the profile's runtime dirs failed, so no single runtime is at
|
|
2286
|
+
// fault and none is invented: this is reported at profile level.
|
|
2287
|
+
statuses.push({
|
|
2288
|
+
name,
|
|
2289
|
+
running: false,
|
|
2290
|
+
tasks: [],
|
|
2291
|
+
unavailable: err instanceof Error ? err.message : String(err),
|
|
2292
|
+
});
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (reconciled)
|
|
2296
|
+
statuses.push(reconciled);
|
|
2297
|
+
// Keyed by the runtime dir that actually failed, so a profile with several
|
|
2298
|
+
// runtimes names the unreadable one rather than guessing at the first.
|
|
2299
|
+
for (const [key, message] of failures) {
|
|
2300
|
+
if (statuses.some((status) => status.key === key))
|
|
2301
|
+
continue;
|
|
2302
|
+
const parsed = parseConnectionKey(key);
|
|
2303
|
+
statuses.push({
|
|
2304
|
+
name,
|
|
2305
|
+
endpoint: parsed.endpoint,
|
|
2306
|
+
key,
|
|
2307
|
+
running: false,
|
|
2308
|
+
tasks: [],
|
|
2309
|
+
unavailable: message,
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2217
2313
|
if (!profileName) {
|
|
2218
2314
|
for (const profile of await listProfiles()) {
|
|
2219
2315
|
if (seenProfiles.has(profile.name))
|
|
2220
2316
|
continue;
|
|
2221
|
-
|
|
2222
|
-
if (reconciled)
|
|
2223
|
-
statuses.push(reconciled);
|
|
2317
|
+
await reconcile(profile.name);
|
|
2224
2318
|
}
|
|
2225
2319
|
}
|
|
2226
2320
|
else if (statuses.length === 0) {
|
|
2227
|
-
|
|
2228
|
-
if (reconciled)
|
|
2229
|
-
statuses.push(reconciled);
|
|
2321
|
+
await reconcile(profileName);
|
|
2230
2322
|
}
|
|
2231
2323
|
return statuses;
|
|
2232
2324
|
}
|
|
@@ -2239,14 +2331,27 @@ export class BrowserService {
|
|
|
2239
2331
|
* ({@link listProfileCacheDirs}), including the legacy pre-composite dir named
|
|
2240
2332
|
* exactly the profile.
|
|
2241
2333
|
*/
|
|
2242
|
-
async reconcileFromDisk(profileName
|
|
2334
|
+
async reconcileFromDisk(profileName,
|
|
2335
|
+
/**
|
|
2336
|
+
* Read-only callers pass a sink so an unreadable runtime dir is attributed
|
|
2337
|
+
* to the dir that actually failed, and the remaining dirs are still tried.
|
|
2338
|
+
* Callers without a sink keep the strict behaviour.
|
|
2339
|
+
*/
|
|
2340
|
+
onError) {
|
|
2243
2341
|
const dirs = listProfileCacheDirs(profileName);
|
|
2244
2342
|
for (const dir of dirs) {
|
|
2245
2343
|
const key = asConnectionKey(path.basename(dir));
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2344
|
+
try {
|
|
2345
|
+
const info = getRunningChromeInfo(key);
|
|
2346
|
+
if (!info)
|
|
2347
|
+
continue;
|
|
2348
|
+
return await this.profileStatusFromDisk(profileName, key, info.port, info.pid);
|
|
2349
|
+
}
|
|
2350
|
+
catch (err) {
|
|
2351
|
+
if (!onError)
|
|
2352
|
+
throw err;
|
|
2353
|
+
onError(key, err);
|
|
2354
|
+
}
|
|
2250
2355
|
}
|
|
2251
2356
|
return null;
|
|
2252
2357
|
}
|
|
@@ -2771,6 +2876,12 @@ export class BrowserService {
|
|
|
2771
2876
|
await cdp.connect(wsUrl);
|
|
2772
2877
|
await this.enableDomains(cdp);
|
|
2773
2878
|
const tasks = this.loadTaskState(key);
|
|
2879
|
+
// The attach is now PROVEN: the stored port answered /json/version and
|
|
2880
|
+
// `verifyBrowserIdentity` matched the profile's browser family. Only here
|
|
2881
|
+
// may this service claim a browser the startup reaper deliberately left
|
|
2882
|
+
// alone — it rewrites `daemonPid` only, keeping the original launch's
|
|
2883
|
+
// metadata. An ambiguous identity throws above and adopts nothing.
|
|
2884
|
+
adoptProfileRuntimeOwner(key);
|
|
2774
2885
|
return {
|
|
2775
2886
|
backend: 'cdp',
|
|
2776
2887
|
cdp,
|
|
@@ -3339,7 +3450,13 @@ export class BrowserService {
|
|
|
3339
3450
|
* Returns null when nothing is speaking CDP so callers can fall through
|
|
3340
3451
|
* to disk reconcile.
|
|
3341
3452
|
*/
|
|
3342
|
-
async attachRunningProfile(key, diskTasks
|
|
3453
|
+
async attachRunningProfile(key, diskTasks,
|
|
3454
|
+
/**
|
|
3455
|
+
* Shared Arc liveness for one status pass; each saved Arc runtime used to
|
|
3456
|
+
* probe on its own. Callers with no pass to share pass nothing and probe
|
|
3457
|
+
* directly, as before.
|
|
3458
|
+
*/
|
|
3459
|
+
arcRunning) {
|
|
3343
3460
|
const { profile: bare, endpoint: endpointFromKey } = parseConnectionKey(key);
|
|
3344
3461
|
const profile = await getProfile(bare);
|
|
3345
3462
|
if (!profile)
|
|
@@ -3363,7 +3480,7 @@ export class BrowserService {
|
|
|
3363
3480
|
// is still running. No port, no CDP — just verify Arc is alive and restore
|
|
3364
3481
|
// the task state.
|
|
3365
3482
|
if (resolved.target.startsWith('arc-native:')) {
|
|
3366
|
-
if (process.platform !== 'darwin' || !(await isArcRunning()))
|
|
3483
|
+
if (process.platform !== 'darwin' || !(await (arcRunning ?? isArcRunning)()))
|
|
3367
3484
|
return null;
|
|
3368
3485
|
const tasks = this.loadTaskState(key);
|
|
3369
3486
|
for (const [k, t] of diskTasks) {
|
|
@@ -3398,6 +3515,10 @@ export class BrowserService {
|
|
|
3398
3515
|
const port = parseEndpointUrl(resolved.target)?.port ?? 9600;
|
|
3399
3516
|
try {
|
|
3400
3517
|
const ff = await connectFirefox(profile, key, port, { profileDir: profile.userDataDir });
|
|
3518
|
+
// A served port that accepted a fresh BiDi session is this profile's own
|
|
3519
|
+
// Firefox, so claim the record for this daemon (see the local-CDP claim
|
|
3520
|
+
// below for why adoption waits for a proven attach).
|
|
3521
|
+
adoptProfileRuntimeOwner(key);
|
|
3401
3522
|
const tasks = this.loadTaskState(key);
|
|
3402
3523
|
for (const [k, t] of diskTasks) {
|
|
3403
3524
|
if (!tasks.has(k))
|
|
@@ -3476,6 +3597,14 @@ export class BrowserService {
|
|
|
3476
3597
|
const cdp = new CDPClient();
|
|
3477
3598
|
await cdp.connect(wsUrl);
|
|
3478
3599
|
await this.enableDomains(cdp);
|
|
3600
|
+
// This is the path a replacement daemon takes to recover a browser its
|
|
3601
|
+
// predecessor launched, so it is where ownership actually transfers. Only
|
|
3602
|
+
// now — after `verifyBrowserIdentity` confirmed the browser family on the
|
|
3603
|
+
// recorded port and CDP is attached — is it proven that this record
|
|
3604
|
+
// describes a browser we are driving. `adoptProfileRuntimeOwner` rewrites
|
|
3605
|
+
// `daemonPid` alone; an ambiguous identity throws above and adopts
|
|
3606
|
+
// nothing, leaving the record (and the browser) untouched.
|
|
3607
|
+
adoptProfileRuntimeOwner(key);
|
|
3479
3608
|
const tasks = this.loadTaskState(key);
|
|
3480
3609
|
for (const [k, t] of diskTasks) {
|
|
3481
3610
|
if (!tasks.has(k))
|
|
@@ -3555,7 +3684,14 @@ export class BrowserService {
|
|
|
3555
3684
|
}
|
|
3556
3685
|
return existing;
|
|
3557
3686
|
}
|
|
3558
|
-
async rehydrateAllFromDisk(
|
|
3687
|
+
async rehydrateAllFromDisk(arcRunning,
|
|
3688
|
+
/**
|
|
3689
|
+
* Read-only callers pass a sink to make rehydration per-runtime: one saved
|
|
3690
|
+
* dir that cannot be read (corrupt `tasks.json`, an Arc Apple Event that
|
|
3691
|
+
* times out) is reported through this instead of aborting the pass. Callers
|
|
3692
|
+
* without a sink keep the strict behaviour and the error propagates.
|
|
3693
|
+
*/
|
|
3694
|
+
onError) {
|
|
3559
3695
|
const runtimeRoot = getBrowserRuntimeDir();
|
|
3560
3696
|
let dirNames = [];
|
|
3561
3697
|
try {
|
|
@@ -3579,24 +3715,31 @@ export class BrowserService {
|
|
|
3579
3715
|
// Skip non-runtime dirs (e.g. profiles/)
|
|
3580
3716
|
if (dirName === 'profiles' || dirName === 'sessions' || dirName === 'exports')
|
|
3581
3717
|
continue;
|
|
3582
|
-
const tasks = this.loadTaskState(key);
|
|
3583
|
-
if (tasks.size === 0)
|
|
3584
|
-
continue;
|
|
3585
|
-
// Soft attach only — never launch / never clear pid files.
|
|
3586
|
-
const conn = await this.attachRunningProfile(key, tasks);
|
|
3587
|
-
if (!conn)
|
|
3588
|
-
continue;
|
|
3589
|
-
// A concurrent rehydrate may have registered one for this key while we
|
|
3590
|
-
// awaited the attach; keep the winner and tear our loser's tunnel down.
|
|
3591
|
-
const registered = this.registerRehydratedConnection(key, conn);
|
|
3592
|
-
if (registered !== conn)
|
|
3593
|
-
continue;
|
|
3594
3718
|
try {
|
|
3595
|
-
|
|
3596
|
-
|
|
3719
|
+
const tasks = this.loadTaskState(key);
|
|
3720
|
+
if (tasks.size === 0)
|
|
3721
|
+
continue;
|
|
3722
|
+
// Soft attach only — never launch / never clear pid files.
|
|
3723
|
+
const conn = await this.attachRunningProfile(key, tasks, arcRunning);
|
|
3724
|
+
if (!conn)
|
|
3725
|
+
continue;
|
|
3726
|
+
// A concurrent rehydrate may have registered one for this key while we
|
|
3727
|
+
// awaited the attach; keep the winner and tear our loser's tunnel down.
|
|
3728
|
+
const registered = this.registerRehydratedConnection(key, conn);
|
|
3729
|
+
if (registered !== conn)
|
|
3730
|
+
continue;
|
|
3731
|
+
try {
|
|
3732
|
+
if (conn.backend !== 'arc-native' && conn.backend !== 'bidi')
|
|
3733
|
+
await this.applyDefaultDownloadBehavior(conn, key);
|
|
3734
|
+
}
|
|
3735
|
+
catch {
|
|
3736
|
+
// Non-fatal for rehydrate.
|
|
3737
|
+
}
|
|
3597
3738
|
}
|
|
3598
|
-
catch {
|
|
3599
|
-
|
|
3739
|
+
catch (err) {
|
|
3740
|
+
if (!onError)
|
|
3741
|
+
throw err;
|
|
3742
|
+
onError(key, err);
|
|
3600
3743
|
}
|
|
3601
3744
|
}
|
|
3602
3745
|
}
|
|
@@ -3688,14 +3831,29 @@ export class BrowserService {
|
|
|
3688
3831
|
return tabs;
|
|
3689
3832
|
}
|
|
3690
3833
|
/** Live status for ONE registered connection, addressed by its runtime key. */
|
|
3691
|
-
async getProfileStatus(key) {
|
|
3834
|
+
async getProfileStatus(key, arc) {
|
|
3692
3835
|
const conn = this.connections.get(key);
|
|
3693
3836
|
if (!conn)
|
|
3694
3837
|
return null;
|
|
3695
3838
|
if (conn.backend === 'arc-native') {
|
|
3839
|
+
// One snapshot and one liveness read for the whole pass, shared across
|
|
3840
|
+
// every task and every Arc profile (built in `status`).
|
|
3841
|
+
const spaces = arc?.spaces ?? onceAsync(() => enumerateArcSpaces());
|
|
3842
|
+
const running = arc?.running ?? onceAsync(() => isArcRunning());
|
|
3696
3843
|
const tasks = [];
|
|
3697
3844
|
for (const task of conn.tasks.values()) {
|
|
3698
|
-
|
|
3845
|
+
// The moved-tab refusal is the ownership guarantee actions rely on and
|
|
3846
|
+
// stays strict. `status` only reads, so one stale task must not take down
|
|
3847
|
+
// its neighbours: report what disk knows plus the reason, and leave
|
|
3848
|
+
// `tabs` ABSENT rather than empty so nothing claims a moved tab is live.
|
|
3849
|
+
let tabs = [];
|
|
3850
|
+
let unavailable;
|
|
3851
|
+
try {
|
|
3852
|
+
tabs = await this.listArcTaskTabsFromSnapshot(task, spaces);
|
|
3853
|
+
}
|
|
3854
|
+
catch (err) {
|
|
3855
|
+
unavailable = err instanceof Error ? err.message : String(err);
|
|
3856
|
+
}
|
|
3699
3857
|
const domains = tabs.flatMap((tab) => {
|
|
3700
3858
|
try {
|
|
3701
3859
|
const domain = new URL(tab.url).hostname.replace(/^www\./, '');
|
|
@@ -3714,6 +3872,7 @@ export class BrowserService {
|
|
|
3714
3872
|
createdAt: task.createdAt,
|
|
3715
3873
|
tabs: tabs.length ? tabs : undefined,
|
|
3716
3874
|
domains: [...new Set(domains)],
|
|
3875
|
+
unavailable,
|
|
3717
3876
|
});
|
|
3718
3877
|
}
|
|
3719
3878
|
const parsed = parseConnectionKey(key);
|
|
@@ -3721,7 +3880,7 @@ export class BrowserService {
|
|
|
3721
3880
|
name: conn.profile ?? parsed.profile,
|
|
3722
3881
|
endpoint: parsed.endpoint,
|
|
3723
3882
|
key,
|
|
3724
|
-
running: await
|
|
3883
|
+
running: await running(),
|
|
3725
3884
|
port: 0,
|
|
3726
3885
|
pid: 0,
|
|
3727
3886
|
tasks,
|
|
@@ -4197,6 +4356,39 @@ export class BrowserService {
|
|
|
4197
4356
|
}
|
|
4198
4357
|
return executeJavaScript(ref, expression);
|
|
4199
4358
|
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Resolve a task's owned Arc tabs against one shared snapshot of every
|
|
4361
|
+
* window/space/tab, rather than asking Arc per tab as `listArcTaskTabs` does.
|
|
4362
|
+
*
|
|
4363
|
+
* Same ownership rule as the live path — `requireArcTask`, then the recorded
|
|
4364
|
+
* ref must be present in its original window and Space. The snapshot changes
|
|
4365
|
+
* how often Arc is asked, never what counts as owned. Actions keep the live
|
|
4366
|
+
* path, because a snapshot can go stale before it is acted on.
|
|
4367
|
+
*/
|
|
4368
|
+
async listArcTaskTabsFromSnapshot(task, spaces) {
|
|
4369
|
+
// Before the snapshot on purpose: a ref naming a window its task does not
|
|
4370
|
+
// own is a moved tab on the record alone, so it needs no round trip and
|
|
4371
|
+
// reports the right reason even when Arc is unreachable.
|
|
4372
|
+
const native = this.requireArcTask(task);
|
|
4373
|
+
const snapshot = await spaces();
|
|
4374
|
+
const tabs = [];
|
|
4375
|
+
for (const [shortId, ref] of Object.entries(native.tabs)) {
|
|
4376
|
+
const live = snapshot
|
|
4377
|
+
.find((space) => space.windowId === ref.windowId && space.spaceId === ref.spaceId)
|
|
4378
|
+
?.tabs.find((tab) => tab.tabId === ref.tabId);
|
|
4379
|
+
if (!live) {
|
|
4380
|
+
throw new Error(`Owned Arc tab ${shortId} is missing from its original window/Space; refusing to adopt a moved tab.`);
|
|
4381
|
+
}
|
|
4382
|
+
tabs.push({
|
|
4383
|
+
id: shortId,
|
|
4384
|
+
url: live.url,
|
|
4385
|
+
title: live.title,
|
|
4386
|
+
task: task.name,
|
|
4387
|
+
current: shortId === task.currentTabId,
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
return tabs;
|
|
4391
|
+
}
|
|
4200
4392
|
async listArcTaskTabs(task) {
|
|
4201
4393
|
const native = this.requireArcTask(task);
|
|
4202
4394
|
const tabs = [];
|
|
@@ -358,6 +358,15 @@ export interface ProfileStatus {
|
|
|
358
358
|
/** The port declared in the profile's first endpoint, when it differs from the running port. */
|
|
359
359
|
configuredPort?: number;
|
|
360
360
|
tasks: TaskStatus[];
|
|
361
|
+
/**
|
|
362
|
+
* Why this profile's live state could not be read, when it could not be.
|
|
363
|
+
*
|
|
364
|
+
* `status` is read-only, so one unreadable profile must not hide the others:
|
|
365
|
+
* the profile is still listed, with what is known from disk, and this says
|
|
366
|
+
* what is missing. Absent means the read succeeded — it is never set to paper
|
|
367
|
+
* over a failure that did not happen.
|
|
368
|
+
*/
|
|
369
|
+
unavailable?: string;
|
|
361
370
|
}
|
|
362
371
|
export interface TaskStatus {
|
|
363
372
|
id: string;
|
|
@@ -376,6 +385,15 @@ export interface TaskStatus {
|
|
|
376
385
|
current?: boolean;
|
|
377
386
|
task?: string;
|
|
378
387
|
}>;
|
|
388
|
+
/**
|
|
389
|
+
* Why this task's live tabs could not be enumerated, when they could not be.
|
|
390
|
+
*
|
|
391
|
+
* Set together with an ABSENT {@link tabs} — the recorded {@link tabCount}
|
|
392
|
+
* still reports what the task owns on disk, but nothing claims those tabs are
|
|
393
|
+
* live, because that is exactly what could not be verified. An Arc tab moved
|
|
394
|
+
* to another window or Space is the case this exists for.
|
|
395
|
+
*/
|
|
396
|
+
unavailable?: string;
|
|
379
397
|
}
|
|
380
398
|
export interface HistoricalTask {
|
|
381
399
|
id: string;
|
|
@@ -23,6 +23,7 @@ import * as yaml from 'yaml';
|
|
|
23
23
|
import { listResources, resolveResource } from './resources.js';
|
|
24
24
|
import { probeCapture } from './probe.js';
|
|
25
25
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
26
|
+
import { execFileShellSpec } from './platform/exec.js';
|
|
26
27
|
import { localBinDir } from './platform/posixpath.js';
|
|
27
28
|
// ─── Validation primitives ───────────────────────────────────────────────────
|
|
28
29
|
/** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
|
|
@@ -457,7 +458,8 @@ export function buildInstallCommand(method) {
|
|
|
457
458
|
function runInstallMethod(method) {
|
|
458
459
|
if ('npm' in method) {
|
|
459
460
|
assertNpmPackage(method.npm);
|
|
460
|
-
const
|
|
461
|
+
const invocation = execFileShellSpec('npm', ['install', '-g', method.npm]);
|
|
462
|
+
const r = spawnSync(invocation.command, invocation.args, { stdio: 'inherit', shell: invocation.shell });
|
|
461
463
|
if (r.status !== 0) {
|
|
462
464
|
throw new Error(`npm install -g ${method.npm} exited with status ${r.status ?? 'unknown'}`);
|
|
463
465
|
}
|
|
@@ -16,7 +16,7 @@ import { insertTask, updateTaskStatus } from './store.js';
|
|
|
16
16
|
import { renderStream } from './stream.js';
|
|
17
17
|
import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from './types.js';
|
|
18
18
|
import { emit } from '../feed/events.js';
|
|
19
|
-
import { shareRuntimeEnv } from '../share
|
|
19
|
+
import { shareRuntimeEnv } from '../share-runtime.js';
|
|
20
20
|
/** Map a supported image file extension to its wire mimeType. Rejects anything else. */
|
|
21
21
|
function imageMimeFromPath(file) {
|
|
22
22
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DEFAULT_CF_BUNDLE = "cloudflare";
|
|
2
|
+
/** Cloudflare API credentials for provisioning, read from `cloudflare` (or a
|
|
3
|
+
* user-named bundle). Fuzzy-matches key names so it works across bundle layouts. */
|
|
4
|
+
export declare function readCloudflareCreds(bundle?: string, override?: {
|
|
5
|
+
apiToken?: string;
|
|
6
|
+
accountId?: string;
|
|
7
|
+
}): {
|
|
8
|
+
apiToken: string;
|
|
9
|
+
accountId: string;
|
|
10
|
+
};
|