@phnx-labs/agents-cli 1.22.50 → 1.22.52
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 +219 -0
- package/dist/bootstrap.js +1 -1
- package/dist/commands/attach.js +7 -0
- package/dist/commands/browser.d.ts +10 -0
- package/dist/commands/browser.js +191 -54
- package/dist/commands/config.js +20 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +8 -4
- package/dist/commands/detach.js +1 -1
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/focus.d.ts +1 -10
- package/dist/commands/focus.js +14 -79
- package/dist/commands/go.d.ts +26 -0
- package/dist/commands/go.js +63 -5
- package/dist/commands/menubar.js +6 -4
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/repo.js +31 -3
- package/dist/commands/sessions-resume.d.ts +1 -0
- package/dist/commands/sessions-resume.js +13 -2
- package/dist/commands/sessions-stop.js +1 -1
- package/dist/commands/sessions.d.ts +23 -13
- package/dist/commands/sessions.js +40 -20
- package/dist/commands/setup-browser.d.ts +5 -2
- package/dist/commands/setup-browser.js +14 -29
- package/dist/commands/setup-preferences.d.ts +22 -3
- package/dist/commands/setup-preferences.js +25 -8
- package/dist/commands/share.js +12 -8
- package/dist/commands/status.js +5 -0
- package/dist/commands/sync.js +58 -2
- package/dist/commands/tmux.d.ts +8 -1
- package/dist/commands/tmux.js +167 -17
- package/dist/commands/traces.js +32 -4
- package/dist/lib/browser/ipc.d.ts +44 -0
- package/dist/lib/browser/ipc.js +120 -8
- package/dist/lib/browser/profiles.d.ts +39 -17
- package/dist/lib/browser/profiles.js +51 -52
- package/dist/lib/browser/runtime-state.d.ts +4 -2
- package/dist/lib/browser/runtime-state.js +4 -2
- package/dist/lib/browser/service.js +4 -3
- package/dist/lib/channels/owner-forward.d.ts +88 -0
- package/dist/lib/channels/owner-forward.js +116 -0
- package/dist/lib/channels/owner-sink.js +7 -0
- package/dist/lib/claude-statusline.d.ts +9 -0
- package/dist/lib/claude-statusline.js +45 -4
- package/dist/lib/computer/ssh-tunnel.d.ts +7 -6
- package/dist/lib/computer/ssh-tunnel.js +13 -8
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +9 -2
- package/dist/lib/device-config.js +23 -0
- package/dist/lib/exec.d.ts +8 -0
- package/dist/lib/exec.js +7 -0
- package/dist/lib/factory/snapshot.d.ts +1 -1
- package/dist/lib/factory/snapshot.js +1 -1
- package/dist/lib/feed-broadcast.js +15 -1
- package/dist/lib/git.d.ts +93 -0
- package/dist/lib/git.js +232 -0
- package/dist/lib/helper-download.d.ts +12 -2
- package/dist/lib/helper-download.js +12 -2
- package/dist/lib/installations/migrate.js +4 -4
- package/dist/lib/menubar/install-menubar.d.ts +71 -8
- package/dist/lib/menubar/install-menubar.js +183 -24
- package/dist/lib/monitors/remote.d.ts +18 -1
- package/dist/lib/monitors/remote.js +15 -2
- package/dist/lib/notify.d.ts +7 -0
- package/dist/lib/notify.js +15 -1
- package/dist/lib/session/local-tmux-attach.d.ts +69 -0
- package/dist/lib/session/local-tmux-attach.js +164 -0
- package/dist/lib/session/remote-active.d.ts +8 -0
- package/dist/lib/session/remote-active.js +1 -0
- package/dist/lib/share/publish.d.ts +8 -11
- package/dist/lib/share/publish.js +16 -20
- package/dist/lib/share/worker-template.js +99 -12
- package/dist/lib/star-nudge.d.ts +2 -2
- package/dist/lib/star-nudge.js +2 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +2 -2
- package/dist/lib/sync-status.d.ts +17 -0
- package/dist/lib/sync-status.js +21 -2
- package/dist/lib/tmux/index.d.ts +1 -1
- package/dist/lib/tmux/index.js +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/traces/sync.d.ts +30 -0
- package/dist/lib/traces/sync.js +91 -10
- package/package.json +3 -3
package/dist/commands/browser.js
CHANGED
|
@@ -14,7 +14,7 @@ import { DEFAULT_VIEWPORT, parseWindowSize, parseWindowPosition } from '../lib/b
|
|
|
14
14
|
import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
|
|
15
15
|
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
16
16
|
import { parseTargetFilter } from '../lib/browser/service.js';
|
|
17
|
-
import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest as sendRawIPCRequest, } from '../lib/browser/ipc.js';
|
|
17
|
+
import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest as sendRawIPCRequest, resetBrowserDaemon, } from '../lib/browser/ipc.js';
|
|
18
18
|
import { bindTask, getTaskBinding, honorScreenshotOutput, REJECT_DEVICE_MESSAGE, resolveTaskRoute, unbindTask, unbindTasksForProfile, updateTaskBinding, } from '../lib/browser/task-index.js';
|
|
19
19
|
import { isSelfHost } from '../lib/devices/self-host.js';
|
|
20
20
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
@@ -248,6 +248,23 @@ function assertDeviceDeclaresProfile(device, profileName) {
|
|
|
248
248
|
throw new Error(`Device "${device}" does not declare browser profile "${profileName}". ${where}\n` +
|
|
249
249
|
`Next: run \`agents browser profiles add ${profileName} --browser <b>\` on ${device}.`);
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The fleet browser hub this box drives by default (`browser.device`), or
|
|
253
|
+
* undefined to drive locally. It is one fleet-synced value in the central
|
|
254
|
+
* agents.yaml, so every box reads the same hub name — but the hub itself resolves
|
|
255
|
+
* to a self-host, and a fleet-dispatched invocation is already ON its target, so
|
|
256
|
+
* both return undefined. That self short-circuit is what lets a single synced
|
|
257
|
+
* value be safe: only the boxes that are NOT the hub forward to it, and the hub
|
|
258
|
+
* never forwards to itself.
|
|
259
|
+
*/
|
|
260
|
+
export function defaultBrowserHub() {
|
|
261
|
+
if (isFleetRemoteInvocation())
|
|
262
|
+
return undefined;
|
|
263
|
+
const hub = getConfigValue('browser.device').value;
|
|
264
|
+
if (!hub || isSelfHost(hub))
|
|
265
|
+
return undefined;
|
|
266
|
+
return hub;
|
|
267
|
+
}
|
|
251
268
|
// Help groups — surfaces the actual mental model an agent follows
|
|
252
269
|
// ("open a session / drive the page / capture evidence / rare extras")
|
|
253
270
|
// instead of an alphabetical dump. Everything not listed falls into a
|
|
@@ -277,7 +294,7 @@ export function registerBrowserCommand(program) {
|
|
|
277
294
|
# Create a Chrome profile pointed at a CDP endpoint
|
|
278
295
|
agents browser profiles create work --browser chrome --endpoint http://localhost:9222
|
|
279
296
|
|
|
280
|
-
# Start a session
|
|
297
|
+
# Start a session on this machine's default browser (set it once with: agents browser use)
|
|
281
298
|
agents browser start
|
|
282
299
|
|
|
283
300
|
# Or pin to a specific profile
|
|
@@ -306,6 +323,11 @@ export function registerBrowserCommand(program) {
|
|
|
306
323
|
|
|
307
324
|
# Close tabs the daemon's own reaper would have caught on its next 5-min tick
|
|
308
325
|
agents browser prune --dry-run
|
|
326
|
+
|
|
327
|
+
# Recover a wedged daemon ("Timeout waiting for browser daemon socket"):
|
|
328
|
+
# stop it and clear a stale socket, then start clean
|
|
329
|
+
agents browser stop --daemon
|
|
330
|
+
agents browser start
|
|
309
331
|
`,
|
|
310
332
|
notes: `
|
|
311
333
|
Most agent workflows should use the 'browser' skill instead of raw subcommands.
|
|
@@ -346,7 +368,7 @@ export function buildBrowserUseChoices(profiles, installed, current) {
|
|
|
346
368
|
export async function runBrowserUse(name, opts, interactive = isInteractiveTerminal()) {
|
|
347
369
|
if (opts.unset || name === 'auto') {
|
|
348
370
|
unsetConfigValue('browser.profile');
|
|
349
|
-
console.log('Default browser profile cleared. `agents browser start`
|
|
371
|
+
console.log('Default browser profile cleared. Pick one with `agents browser use <name>` (or `agents setup`); until then `agents browser start` has no default to launch here.');
|
|
350
372
|
return true;
|
|
351
373
|
}
|
|
352
374
|
let selectedName = name;
|
|
@@ -355,8 +377,15 @@ export async function runBrowserUse(name, opts, interactive = isInteractiveTermi
|
|
|
355
377
|
if (!interactive) {
|
|
356
378
|
console.log(current
|
|
357
379
|
? `Default browser profile (this machine): ${current}`
|
|
358
|
-
: 'Default browser profile (this machine):
|
|
380
|
+
: 'Default browser profile (this machine): none — pick one with `agents browser use <name>`');
|
|
381
|
+
const hub = getConfigValue('browser.device').value;
|
|
382
|
+
if (hub) {
|
|
383
|
+
console.log(isSelfHost(hub)
|
|
384
|
+
? `Browser hub (browser.device): ${hub} — this machine, so drives locally`
|
|
385
|
+
: `Browser hub (browser.device): ${hub} — bare \`agents browser start\` drives ${hub}'s browser`);
|
|
386
|
+
}
|
|
359
387
|
console.log('Usage: agents browser use <name> (or --unset)');
|
|
388
|
+
console.log('Fleet hub: agents config set browser.device <device> (drive that box\'s browser from every machine)');
|
|
360
389
|
return true;
|
|
361
390
|
}
|
|
362
391
|
const choices = buildBrowserUseChoices(await listProfiles(), listInstalledBrowsers(), current);
|
|
@@ -397,7 +426,7 @@ export async function runBrowserUse(name, opts, interactive = isInteractiveTermi
|
|
|
397
426
|
function configureBrowserUseCommand(command, deprecated = false) {
|
|
398
427
|
command
|
|
399
428
|
.description('Pick the profile `agents browser start` uses when no --profile is passed. No name opens a picker on a TTY or prints the current default headlessly.')
|
|
400
|
-
.option('--unset', 'Clear the configured default (
|
|
429
|
+
.option('--unset', 'Clear the configured default (leaves this machine with no default until you pick one)')
|
|
401
430
|
.action(async (name, opts) => {
|
|
402
431
|
if (deprecated) {
|
|
403
432
|
console.warn(chalk.yellow('Deprecation: `agents browser profiles set-default` is replaced by `agents browser use`.'));
|
|
@@ -918,7 +947,7 @@ function registerProfilesCommands(browser) {
|
|
|
918
947
|
(removed > 0 ? ` (and ${removed} cache dir${removed === 1 ? '' : 's'})` : ''));
|
|
919
948
|
if (getConfiguredDefaultProfileName() === name) {
|
|
920
949
|
console.error(`warning: "${name}" was this machine's default browser profile; ` +
|
|
921
|
-
`\`agents browser start\`
|
|
950
|
+
`\`agents browser start\` has no default here until you run: agents browser use <name>`);
|
|
922
951
|
}
|
|
923
952
|
});
|
|
924
953
|
profiles
|
|
@@ -1001,13 +1030,13 @@ function registerProfilesCommands(browser) {
|
|
|
1001
1030
|
with \`agents browser profiles claim\` on the machine that hosts the browser.
|
|
1002
1031
|
|
|
1003
1032
|
In \`list\`, the \`*\` marker means "this machine's configured default"
|
|
1004
|
-
(\`agents browser start\` with no --profile).
|
|
1005
|
-
named \`auto-chrome\`; \`default\` is only an ALIAS for whichever profile
|
|
1033
|
+
(\`agents browser start\` with no --profile). A profile a setup wizard pins
|
|
1034
|
+
is named \`auto-chrome\`; \`default\` is only an ALIAS for whichever profile
|
|
1006
1035
|
this machine is configured to use, resolved the same way by every command.
|
|
1007
1036
|
|
|
1008
1037
|
\`prune\` only considers profiles this device declares. It never removes a
|
|
1009
|
-
profile that is in use, the configured default, or the auto-
|
|
1010
|
-
|
|
1038
|
+
profile that is in use, the configured default, or the \`auto-chrome\`
|
|
1039
|
+
profile a setup wizard created.
|
|
1011
1040
|
`,
|
|
1012
1041
|
});
|
|
1013
1042
|
profiles
|
|
@@ -1347,13 +1376,13 @@ function registerTaskCommands(browser) {
|
|
|
1347
1376
|
});
|
|
1348
1377
|
browser
|
|
1349
1378
|
.command('start')
|
|
1350
|
-
.description('Start a browser task. Pass --profile <name>; omit to use your configured default (`agents browser use <name>`
|
|
1351
|
-
.option('-p, --profile <name>', 'Browser profile to use (omit to use
|
|
1379
|
+
.description('Start a browser task. Pass --profile <name>; omit to use your configured default (set it with `agents browser use <name>` or `agents setup`). Page verbs (navigate/screenshot/…) create a task implicitly when none exists — start is for --profile/--url/--record/--title.')
|
|
1380
|
+
.option('-p, --profile <name>', 'Browser profile to use (omit to use this machine\'s configured default; set it with `agents browser use <name>`)')
|
|
1352
1381
|
.option(TASK_OPTION_FLAG, 'Task name (auto-generated short id if omitted)')
|
|
1353
1382
|
.option('--title <label>', 'Human label shown in `browser status` (defaults to first navigated host)')
|
|
1354
1383
|
.option('-e, --endpoint <name>', 'Endpoint preset (defaults to the profile\'s default)')
|
|
1355
1384
|
.option('-u, --url <url>', 'Open URL in first tab')
|
|
1356
|
-
.option('--device <name>', 'Device that hosts this task;
|
|
1385
|
+
.option('--device <name>', 'Device that hosts this task (defaults to the browser.device hub when set; use `local` to force this machine). Later verbs resolve it from --task; not valid on page verbs')
|
|
1357
1386
|
.option('--fresh', 'Always open a new tab, skipping the reclaim of a tab an abandoned task is holding on that URL')
|
|
1358
1387
|
.option('--no-skills', 'Skip auto-discovery of site-specific SKILL.md from ~/.agents/skills/browser/domain-skills/')
|
|
1359
1388
|
.option('--record', 'Start recording right after the tab opens (shorthand for `agents browser record start` as a follow-up)')
|
|
@@ -1373,13 +1402,133 @@ function registerTaskCommands(browser) {
|
|
|
1373
1402
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1374
1403
|
process.exit(1);
|
|
1375
1404
|
}
|
|
1405
|
+
// Thin-client hub (PHNX-2010): when this box has a fleet browser hub
|
|
1406
|
+
// (`browser.device`) and the caller gave no explicit --device, forward the
|
|
1407
|
+
// WHOLE start there — bare OR --profile — and skip the local profile/browser
|
|
1408
|
+
// resolution below. The hub owns its profiles, browser, and consent, so this
|
|
1409
|
+
// box needs no local browser and need not carry the named profile; forwarding
|
|
1410
|
+
// the local pre-checks here would reject a profile that lives only on the hub.
|
|
1411
|
+
// Later page verbs follow the task to the hub via the task→device index bound
|
|
1412
|
+
// here. `--device local`/`--device self` (below) force a local run instead;
|
|
1413
|
+
// `defaultBrowserHub()` is undefined on the hub itself and on a fleet-remote
|
|
1414
|
+
// re-exec, so neither forwards to itself.
|
|
1415
|
+
if (!opts.device) {
|
|
1416
|
+
const hub = defaultBrowserHub();
|
|
1417
|
+
if (hub) {
|
|
1418
|
+
// No local profile validation: the hub is the authority on its own
|
|
1419
|
+
// profiles (including fleet-scoped ones this box can't see in
|
|
1420
|
+
// `declaringDevices`), so a bad `--profile` is caught by the hub's own
|
|
1421
|
+
// start and streamed back with its exit code, rather than second-guessed
|
|
1422
|
+
// here against a registry that doesn't carry the hub's profiles.
|
|
1423
|
+
try {
|
|
1424
|
+
const result = await dispatchBrowserToDevice(hub, browserForwardedArgv(), 'capture');
|
|
1425
|
+
process.stdout.write(result.stdout);
|
|
1426
|
+
process.stderr.write(result.stderr);
|
|
1427
|
+
if (result.code !== 0)
|
|
1428
|
+
process.exit(result.code);
|
|
1429
|
+
const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
|
|
1430
|
+
if (!taskName) {
|
|
1431
|
+
console.error(`Remote start on ${hub} produced no task name.`);
|
|
1432
|
+
process.exit(1);
|
|
1433
|
+
}
|
|
1434
|
+
bindTask(taskName, {
|
|
1435
|
+
device: hub,
|
|
1436
|
+
// Only what the caller named; the hub's own default (bare start) is
|
|
1437
|
+
// opaque here. Name-based `done`/`stop --task` and the post-stop
|
|
1438
|
+
// unbind GC these regardless.
|
|
1439
|
+
profile: opts.profile,
|
|
1440
|
+
url: opts.url,
|
|
1441
|
+
sessionId: callerSessionId(),
|
|
1442
|
+
launchId: callerLaunchId(),
|
|
1443
|
+
createdAt: Date.now(),
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
catch (err) {
|
|
1447
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1448
|
+
process.exit(1);
|
|
1449
|
+
}
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
// Resolve an EXPLICIT --device BEFORE any local profile/browser
|
|
1454
|
+
// resolution (PHNX-3289). The task lives on the TARGET device, so its
|
|
1455
|
+
// browser binary and profile must resolve THERE. Resolving locally first
|
|
1456
|
+
// made a browserless box fail with a misleading "No supported browser
|
|
1457
|
+
// found" (bare start) or "Profile <x> not found" (a profile that lives
|
|
1458
|
+
// only on the target) before the start ever routed to --device. This
|
|
1459
|
+
// mirrors the hub path above: the target is the authority on its own
|
|
1460
|
+
// profiles, so forward the whole start and let it validate. `local`/`self`
|
|
1461
|
+
// force a local run; a fleet-remote re-exec is already ON the target and
|
|
1462
|
+
// falls through to the local launch below.
|
|
1463
|
+
const deviceName = opts.device === 'local' || opts.device === 'self' ? undefined : opts.device;
|
|
1464
|
+
if (deviceName) {
|
|
1465
|
+
const lowered = deviceName.toLowerCase();
|
|
1466
|
+
if (lowered === 'all' || lowered === 'auto') {
|
|
1467
|
+
console.error(`--device ${deviceName} is not valid on \`agents browser start\`: a task lives on one device.\n` +
|
|
1468
|
+
`Next: agents browser start --task <name> --device <device>`);
|
|
1469
|
+
process.exit(1);
|
|
1470
|
+
}
|
|
1471
|
+
if (!isSelfHost(deviceName) && !isFleetRemoteInvocation()) {
|
|
1472
|
+
// Resolve the profile NAME (never a local browser auto-pick) so a
|
|
1473
|
+
// named profile can still be validated against the target's
|
|
1474
|
+
// declarations before we round-trip. A bare start carries no name to
|
|
1475
|
+
// validate — the target picks its own default — so it forwards
|
|
1476
|
+
// straight through, which is exactly what unblocks a browserless box.
|
|
1477
|
+
// Only resolve when the caller actually named a profile: passing an
|
|
1478
|
+
// undefined ref would let `resolveProfileRef` fall back to THIS box's
|
|
1479
|
+
// configured/auto-detected default (matching the ~4 other call sites
|
|
1480
|
+
// in this file), and validating that local name against the target
|
|
1481
|
+
// re-introduces the exact false failure this reorder fixes.
|
|
1482
|
+
const forwardProfile = opts.profile ? await resolveProfileRef(opts.profile) : undefined;
|
|
1483
|
+
if (forwardProfile) {
|
|
1484
|
+
try {
|
|
1485
|
+
assertDeviceDeclaresProfile(deviceName, forwardProfile);
|
|
1486
|
+
}
|
|
1487
|
+
catch (err) {
|
|
1488
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const result = await dispatchBrowserToDevice(deviceName, browserForwardedArgv(), 'capture');
|
|
1494
|
+
process.stdout.write(result.stdout);
|
|
1495
|
+
process.stderr.write(result.stderr);
|
|
1496
|
+
if (result.code !== 0)
|
|
1497
|
+
process.exit(result.code);
|
|
1498
|
+
const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
|
|
1499
|
+
if (!taskName) {
|
|
1500
|
+
console.error(`Remote start on ${deviceName} produced no task name.`);
|
|
1501
|
+
process.exit(1);
|
|
1502
|
+
}
|
|
1503
|
+
bindTask(taskName, {
|
|
1504
|
+
device: deviceName,
|
|
1505
|
+
// Only the caller-named profile; the target's own default (bare
|
|
1506
|
+
// start) is opaque here, exactly as in the hub path.
|
|
1507
|
+
profile: forwardProfile ?? opts.profile,
|
|
1508
|
+
url: opts.url,
|
|
1509
|
+
sessionId: callerSessionId(),
|
|
1510
|
+
launchId: callerLaunchId(),
|
|
1511
|
+
createdAt: Date.now(),
|
|
1512
|
+
});
|
|
1513
|
+
if (!result.stderr.includes(`started on ${deviceName}`)) {
|
|
1514
|
+
console.error(`Task "${taskName}" started on ${deviceName}.`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
catch (err) {
|
|
1518
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1519
|
+
process.exit(1);
|
|
1520
|
+
}
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1376
1524
|
// One resolution order for every command (RUSH-2709): `--profile default`
|
|
1377
1525
|
// means the same profile here as it does in stop / status / navigate.
|
|
1378
1526
|
// `start` is the one command that LAUNCHES, so its implicit path goes
|
|
1379
1527
|
// through ensureDefaultBrowserProfile, which additionally verifies the
|
|
1380
|
-
// resolved default can launch on THIS machine and
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1528
|
+
// resolved default can launch on THIS machine and, when nothing launchable
|
|
1529
|
+
// resolves, throws an actionable "pick a browser in `agents setup`" error
|
|
1530
|
+
// (PHNX-3296 — it no longer silently mints an auto-chrome). A filter-only
|
|
1531
|
+
// command must not warn about the config, so that check lives only here.
|
|
1383
1532
|
let profileName;
|
|
1384
1533
|
try {
|
|
1385
1534
|
profileName = await resolveProfileRefForStart(opts.profile);
|
|
@@ -1409,14 +1558,12 @@ function registerTaskCommands(browser) {
|
|
|
1409
1558
|
process.exit(1);
|
|
1410
1559
|
}
|
|
1411
1560
|
}
|
|
1412
|
-
|
|
1561
|
+
// Only the LOCAL launch reaches here — an explicit remote --device already
|
|
1562
|
+
// forwarded and returned above. A residual `deviceName` means an explicit
|
|
1563
|
+
// self target (or a fleet-remote re-exec on the target); its profile
|
|
1564
|
+
// resolved locally, so verify this box actually declares it (the
|
|
1565
|
+
// `profiles claim` hint lives in `assertDeviceDeclaresProfile`).
|
|
1413
1566
|
if (deviceName) {
|
|
1414
|
-
const lowered = deviceName.toLowerCase();
|
|
1415
|
-
if (lowered === 'all' || lowered === 'auto') {
|
|
1416
|
-
console.error(`--device ${deviceName} is not valid on \`agents browser start\`: a task lives on one device.\n` +
|
|
1417
|
-
`Next: agents browser start --task <name> --device <device>`);
|
|
1418
|
-
process.exit(1);
|
|
1419
|
-
}
|
|
1420
1567
|
try {
|
|
1421
1568
|
assertDeviceDeclaresProfile(deviceName, profileName);
|
|
1422
1569
|
}
|
|
@@ -1424,36 +1571,6 @@ function registerTaskCommands(browser) {
|
|
|
1424
1571
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1425
1572
|
process.exit(1);
|
|
1426
1573
|
}
|
|
1427
|
-
if (!isSelfHost(deviceName) && !isFleetRemoteInvocation()) {
|
|
1428
|
-
try {
|
|
1429
|
-
const result = await dispatchBrowserToDevice(deviceName, browserForwardedArgv(), 'capture');
|
|
1430
|
-
process.stdout.write(result.stdout);
|
|
1431
|
-
process.stderr.write(result.stderr);
|
|
1432
|
-
if (result.code !== 0)
|
|
1433
|
-
process.exit(result.code);
|
|
1434
|
-
const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
|
|
1435
|
-
if (!taskName) {
|
|
1436
|
-
console.error(`Remote start on ${deviceName} produced no task name.`);
|
|
1437
|
-
process.exit(1);
|
|
1438
|
-
}
|
|
1439
|
-
bindTask(taskName, {
|
|
1440
|
-
device: deviceName,
|
|
1441
|
-
profile: profileName,
|
|
1442
|
-
url: opts.url,
|
|
1443
|
-
sessionId: callerSessionId(),
|
|
1444
|
-
launchId: callerLaunchId(),
|
|
1445
|
-
createdAt: Date.now(),
|
|
1446
|
-
});
|
|
1447
|
-
if (!result.stderr.includes(`started on ${deviceName}`)) {
|
|
1448
|
-
console.error(`Task "${taskName}" started on ${deviceName} (profile: ${profileName}).`);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
catch (err) {
|
|
1452
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
1453
|
-
process.exit(1);
|
|
1454
|
-
}
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
1574
|
}
|
|
1458
1575
|
// Grounded login guardrail: if the opening URL is a known login-gated
|
|
1459
1576
|
// service and the resolved profile has no session for it, say so on stderr
|
|
@@ -1566,11 +1683,31 @@ function registerTaskCommands(browser) {
|
|
|
1566
1683
|
});
|
|
1567
1684
|
browser
|
|
1568
1685
|
.command('stop')
|
|
1569
|
-
.description('Stop a browser task and close its tabs; with --profile, detach the whole profile (close browser + drop cached connection)')
|
|
1686
|
+
.description('Stop a browser task and close its tabs; with --profile, detach the whole profile (close browser + drop cached connection); with --daemon, stop the browser daemon and clear a wedged socket')
|
|
1570
1687
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1571
1688
|
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1572
1689
|
.option('-p, --profile <name>', 'Detach the whole profile (incl. composite "name@endpoint") instead of stopping a single task')
|
|
1690
|
+
.option('--daemon', 'Stop the browser daemon entirely and clear a stale/wedged socket so the next start comes up clean (recovery for "Timeout waiting for browser daemon socket")')
|
|
1573
1691
|
.action(async (opts) => {
|
|
1692
|
+
if (opts.daemon) {
|
|
1693
|
+
if (opts.profile || opts.task) {
|
|
1694
|
+
console.error('--daemon stops the whole browser daemon; do not combine it with --profile or --task.');
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
try {
|
|
1698
|
+
const result = await resetBrowserDaemon();
|
|
1699
|
+
const parts = [];
|
|
1700
|
+
parts.push(result.wasRunning ? 'Stopped browser daemon' : 'Browser daemon was not running');
|
|
1701
|
+
if (result.socketCleared)
|
|
1702
|
+
parts.push('cleared stale socket');
|
|
1703
|
+
console.log(`${parts.join('; ')}. It restarts on the next browser command.`);
|
|
1704
|
+
}
|
|
1705
|
+
catch (err) {
|
|
1706
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1707
|
+
process.exit(1);
|
|
1708
|
+
}
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1574
1711
|
if (opts.profile) {
|
|
1575
1712
|
const profile = (await resolveProfileRef(opts.profile)) ?? opts.profile;
|
|
1576
1713
|
const response = await sendIPCRequest({
|
package/dist/commands/config.js
CHANGED
|
@@ -111,6 +111,11 @@ function setConfig(parsed, value) {
|
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
case 'browser': {
|
|
114
|
+
if (parsed.property === 'device') {
|
|
115
|
+
// Fleet hub: user scope, one central value, never peer-targeted.
|
|
116
|
+
setConfigValue('browser.device', value);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
114
119
|
// Device-local default lives in the per-device doc's config: block
|
|
115
120
|
// (same store `agents devices config` / getConfigValue use). Bare
|
|
116
121
|
// browser.profile targets this machine; devices.<name>.browser.profile
|
|
@@ -155,6 +160,11 @@ function unsetConfig(parsed) {
|
|
|
155
160
|
return had;
|
|
156
161
|
}
|
|
157
162
|
case 'browser': {
|
|
163
|
+
if (parsed.property === 'device') {
|
|
164
|
+
const had = getConfigValue('browser.device').value !== undefined;
|
|
165
|
+
unsetConfigValue('browser.device');
|
|
166
|
+
return had;
|
|
167
|
+
}
|
|
158
168
|
const target = parsed.device ? { device: parsed.device } : undefined;
|
|
159
169
|
// Must follow parsed.property. Hardcoding 'browser.profile' here meant
|
|
160
170
|
// `config unset browser.viewer` deleted the user's browser.profile while
|
|
@@ -195,6 +205,9 @@ function getConfig(parsed) {
|
|
|
195
205
|
case 'auto':
|
|
196
206
|
return getConfigValue('auto.pool').value;
|
|
197
207
|
case 'browser': {
|
|
208
|
+
if (parsed.property === 'device') {
|
|
209
|
+
return getConfigValue('browser.device').value;
|
|
210
|
+
}
|
|
198
211
|
return getConfigValue(parsed.property === 'viewer' ? 'browser.viewer' : 'browser.profile', parsed.device ? { device: parsed.device } : undefined).value;
|
|
199
212
|
}
|
|
200
213
|
case 'project':
|
|
@@ -267,6 +280,13 @@ function* listCentralConfigEntries() {
|
|
|
267
280
|
const key = 'browser.viewer';
|
|
268
281
|
yield { key, value: browserViewer, hint: configKeyStorageHint(parseConfigKey(key)) };
|
|
269
282
|
}
|
|
283
|
+
// Fleet browser hub — user scope, so it belongs in the central listing next to
|
|
284
|
+
// its siblings. Omitting it repeats the browser.viewer invisibility bug.
|
|
285
|
+
const browserDevice = getConfigValue('browser.device').value;
|
|
286
|
+
if (browserDevice !== undefined) {
|
|
287
|
+
const key = 'browser.device';
|
|
288
|
+
yield { key, value: browserDevice, hint: configKeyStorageHint(parseConfigKey(key)) };
|
|
289
|
+
}
|
|
270
290
|
}
|
|
271
291
|
/** Collect device-scope config entries. */
|
|
272
292
|
function* listDeviceConfigEntries(device) {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { Command } from 'commander';
|
|
18
18
|
import { type DaemonServiceId } from '../lib/daemon-services.js';
|
|
19
|
+
/** Elapsed wall-clock seconds since `pid` started, or null if unavailable (best-effort, POSIX only). */
|
|
20
|
+
export declare function uptimeSeconds(pid: number): number | null;
|
|
19
21
|
/** One row of the roster every registered daemon service — supervisor-managed or legacy — renders in `agents daemon services`. */
|
|
20
22
|
export interface DaemonServiceRow {
|
|
21
23
|
id: DaemonServiceId;
|
package/dist/commands/daemon.js
CHANGED
|
@@ -30,6 +30,7 @@ import { parseDuration } from '../lib/hooks/cache.js';
|
|
|
30
30
|
import { registerFunnelCommand } from './funnel.js';
|
|
31
31
|
import { DEFAULT_WEBHOOK_PORT, DEFAULT_WEBHOOK_RATE_LIMIT, addHostedReceiver, getDaemonWebhooksConfigPath, hostedReceiverPort, readDaemonWebhooksConfig, removeHostedReceiver, } from '../lib/daemon-webhooks.js';
|
|
32
32
|
import { parseFunnelPort } from '../lib/funnel.js';
|
|
33
|
+
import { parseEtimeToSeconds } from '../lib/secrets/reaper.js';
|
|
33
34
|
/**
|
|
34
35
|
* Provably absent, or null when unknowable.
|
|
35
36
|
*
|
|
@@ -256,13 +257,16 @@ function registryScopedDuplicates(processes, ownerPid) {
|
|
|
256
257
|
return processes.filter((p) => registered.has(p.pid));
|
|
257
258
|
}
|
|
258
259
|
/** Elapsed wall-clock seconds since `pid` started, or null if unavailable (best-effort, POSIX only). */
|
|
259
|
-
function uptimeSeconds(pid) {
|
|
260
|
+
export function uptimeSeconds(pid) {
|
|
260
261
|
if (process.platform === 'win32')
|
|
261
262
|
return null;
|
|
262
263
|
try {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
// `-o etimes=` is a GNU/procps keyword macOS/BSD `ps` rejects with a
|
|
265
|
+
// non-zero exit (`ps: etimes: keyword not found`), so `agents daemon status`
|
|
266
|
+
// errored out entirely on macOS. `etime` (`[[dd-]hh:]mm:ss`) is the portable
|
|
267
|
+
// POSIX field; `parseEtimeToSeconds` (shared with the keychain reaper) parses it.
|
|
268
|
+
const out = execFileSync('ps', ['-o', 'etime=', '-p', String(pid)], { encoding: 'utf-8' }).trim();
|
|
269
|
+
return parseEtimeToSeconds(out);
|
|
266
270
|
}
|
|
267
271
|
catch {
|
|
268
272
|
return null;
|
package/dist/commands/detach.js
CHANGED
|
@@ -123,7 +123,7 @@ function spawnHeadless(command, args, logFile) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
export async function detachAction(id, opts = {}) {
|
|
126
|
-
const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true });
|
|
126
|
+
const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true, selector: id });
|
|
127
127
|
const resolved = resolveOne(activeById, id);
|
|
128
128
|
if ('error' in resolved) {
|
|
129
129
|
console.error(chalk.red(resolved.error));
|
|
@@ -7,7 +7,7 @@ import { arch, platform, release } from 'node:os';
|
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { showUrl } from '../lib/open-url.js';
|
|
10
|
-
const REPO = 'phnx-labs/
|
|
10
|
+
const REPO = 'phnx-labs/agi-cli';
|
|
11
11
|
const DISCUSSION_BASE = `https://github.com/${REPO}/discussions/new`;
|
|
12
12
|
const ISSUE_BASE = `https://github.com/${REPO}/issues/new`;
|
|
13
13
|
function readCliVersion() {
|
package/dist/commands/focus.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { Command } from 'commander';
|
|
|
16
16
|
import { probeAttachRail, type AttachRailLiveness, type UnreachableFallback } from './go.js';
|
|
17
17
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
18
18
|
import { type SessionMeta } from '../lib/session/types.js';
|
|
19
|
+
export { looksLikeTmuxAlias, resolveTmuxAliasState, shouldAttachLocalTmuxAliasBeforeFleet, type TmuxAliasState } from '../lib/session/local-tmux-attach.js';
|
|
19
20
|
import { type LiveStatusFilter } from './sessions.js';
|
|
20
21
|
import { openSurfaces, type Backend } from '../lib/terminal/index.js';
|
|
21
22
|
/** Options for `sessions focus` — device scope + the `--active` live-state filters. */
|
|
@@ -99,16 +100,6 @@ export declare function isAttachableLiveSession(session: ActiveSession): boolean
|
|
|
99
100
|
* ("Session transcript not available (file no longer exists). Path: ").
|
|
100
101
|
*/
|
|
101
102
|
export declare function dedupeSessionsByLogicalId(rows: SessionMeta[], self?: string): SessionMeta[];
|
|
102
|
-
export type TmuxAliasState = 'not-an-alias' | 'no-server' | 'absent' | 'dead' | 'live';
|
|
103
|
-
/** Shape of the tmux alias the CLI mints for an agent session: `ag-<agent>-<shortid>`.
|
|
104
|
-
* Delegates to the one canonical matcher beside the name parsers in active.ts. */
|
|
105
|
-
export declare function looksLikeTmuxAlias(selector: string): boolean;
|
|
106
|
-
/**
|
|
107
|
-
* Classify a selector against the live tmux server. Split from the attach so the
|
|
108
|
-
* decision is testable against a real tmux server without replacing the caller's
|
|
109
|
-
* shell (attaching is not something a test can undo).
|
|
110
|
-
*/
|
|
111
|
-
export declare function resolveTmuxAliasState(selector: string, socket?: string): Promise<TmuxAliasState>;
|
|
112
103
|
/**
|
|
113
104
|
* The session to focus once an id/identity selector has resolved across the
|
|
114
105
|
* fleet. Prefer the already-gathered candidate-pool row (it carries the live
|
package/dist/commands/focus.js
CHANGED
|
@@ -17,8 +17,9 @@ import chalk from 'chalk';
|
|
|
17
17
|
import { confirm } from '@inquirer/prompts';
|
|
18
18
|
import { gatherLiveTargets, pickLiveTarget, pickLiveTargets, jumpTo, probeAttachRail, refuseFallback } from './go.js';
|
|
19
19
|
import { sessionProcessIsLocal, sessionProcessHost, shortIdFromName } from '../lib/session/active.js';
|
|
20
|
-
import { attachTmux, ensureSessionHookRepaired, getDefaultSocketPath, hasSession, runTmux } from '../lib/tmux/index.js';
|
|
21
20
|
import { isAgentTmuxAlias } from '../lib/session/types.js';
|
|
21
|
+
import { attachLocalLiveSelector } from '../lib/session/local-tmux-attach.js';
|
|
22
|
+
export { looksLikeTmuxAlias, resolveTmuxAliasState, shouldAttachLocalTmuxAliasBeforeFleet } from '../lib/session/local-tmux-attach.js';
|
|
22
23
|
import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, isRunningLiveSession, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
|
|
23
24
|
import { resolveBackend, CONFIRM_THRESHOLD } from './sessions-resume.js';
|
|
24
25
|
import { runOnPeer } from '../lib/session/remote-list.js';
|
|
@@ -221,6 +222,18 @@ export async function focusAction(id, opts) {
|
|
|
221
222
|
process.exitCode = 1;
|
|
222
223
|
return;
|
|
223
224
|
}
|
|
225
|
+
// A live local tmux alias — or a bare 8-hex short id naming exactly one
|
|
226
|
+
// live local pane — attaches immediately, before any fleet SSH.
|
|
227
|
+
// `collectSessionCandidates` fans out twice (transcript pool + live
|
|
228
|
+
// roster), so a selector that is already a pane on THIS box used to print
|
|
229
|
+
// two unreachable-device lists and stall on offline peers (~2 min measured
|
|
230
|
+
// on yosemite-s0 for `sessions resume ag-claude-0145ab8f --attach-only`)
|
|
231
|
+
// and only then attach. SES-41 / PHNX-3292: an alive `ag-<agent>-<8hex>`
|
|
232
|
+
// pane is attached by name; the pane is sufficient, no fleet SSH needed. A
|
|
233
|
+
// dead/absent pane falls through to id resolution.
|
|
234
|
+
if (await attachLocalLiveSelector(textSelector, hosts)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
224
237
|
// RUSH-2477: an id selector for a LOCAL indexed session resolves against the
|
|
225
238
|
// WAL index alone — a plain read, no write-heavy discovery scan (none of
|
|
226
239
|
// `tryClaimScan`/`releaseScan`'s `BEGIN IMMEDIATE` writer lock) and no
|
|
@@ -268,12 +281,6 @@ export async function focusAction(id, opts) {
|
|
|
268
281
|
let exact = idSelector
|
|
269
282
|
? dedupeSessionsByLogicalId(sessions.filter((session) => session.id.toLowerCase().startsWith(idSelector)), self)
|
|
270
283
|
: [];
|
|
271
|
-
// A tmux alias naming a LIVE local session attaches directly. This must run
|
|
272
|
-
// before the metadata resolver: that resolver treats an unmatched alias as a
|
|
273
|
-
// keyword query, so `ag-kimi-632c1fbc` came back as 13 unrelated text hits
|
|
274
|
-
// while the pane was alive and attachable the whole time.
|
|
275
|
-
if (exact.length === 0 && textSelector && !hosts.length && await attachLiveTmuxAlias(textSelector))
|
|
276
|
-
return;
|
|
277
284
|
// Not live (or remote-scoped): an alias still carries the session's shortid,
|
|
278
285
|
// so resolve by THAT rather than handing the whole `ag-<agent>-<shortid>`
|
|
279
286
|
// string to the resolver — which treats an unmatched alias as a keyword
|
|
@@ -396,25 +403,6 @@ function looksLikeIdentitySelector(selector) {
|
|
|
396
403
|
return !!selector && (/^[0-9a-f][0-9a-f-]{5,}$/i.test(selector) ||
|
|
397
404
|
isAgentTmuxAlias(selector));
|
|
398
405
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Attach a live tmux session named exactly as the selector, without needing the
|
|
401
|
-
* session index to know anything about it.
|
|
402
|
-
*
|
|
403
|
-
* SES-41 requires a `ag-<agent>-<8hex>` tmux alias to resolve, but the alias's
|
|
404
|
-
* hex is the LAUNCH id, not the harness session id, and for a harness that
|
|
405
|
-
* writes no `state/sessions/<pid>.json` record there is no mapping back to a
|
|
406
|
-
* SessionMeta at all. Measured on yosemite-s0: `--resolve ag-kimi-632c1fbc`
|
|
407
|
-
* fell through to a keyword search and returned 13 unrelated rows, while the
|
|
408
|
-
* pane itself was alive (`%124`, `kimi-code`). The pane is the thing the user
|
|
409
|
-
* asked for, and its NAME is sufficient to attach it — so an unattributable
|
|
410
|
-
* session is still reachable instead of being a dead end that forces raw
|
|
411
|
-
* `tmux -S … attach`.
|
|
412
|
-
*
|
|
413
|
-
* Returns false when this is not an alias, the server/session is absent, or
|
|
414
|
-
* every pane is dead — the caller then continues to normal id resolution.
|
|
415
|
-
* SES-39's "re-read `pane_dead` immediately before attach" is honoured here:
|
|
416
|
-
* liveness is queried at attach time, not read from the roster.
|
|
417
|
-
*/
|
|
418
406
|
/**
|
|
419
407
|
* Rank two rows that are the same logical session, best first.
|
|
420
408
|
*
|
|
@@ -447,59 +435,6 @@ export function dedupeSessionsByLogicalId(rows, self) {
|
|
|
447
435
|
}
|
|
448
436
|
return [...byId.values()];
|
|
449
437
|
}
|
|
450
|
-
/** Shape of the tmux alias the CLI mints for an agent session: `ag-<agent>-<shortid>`.
|
|
451
|
-
* Delegates to the one canonical matcher beside the name parsers in active.ts. */
|
|
452
|
-
export function looksLikeTmuxAlias(selector) {
|
|
453
|
-
return isAgentTmuxAlias(selector);
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Classify a selector against the live tmux server. Split from the attach so the
|
|
457
|
-
* decision is testable against a real tmux server without replacing the caller's
|
|
458
|
-
* shell (attaching is not something a test can undo).
|
|
459
|
-
*/
|
|
460
|
-
export async function resolveTmuxAliasState(selector, socket) {
|
|
461
|
-
if (!looksLikeTmuxAlias(selector))
|
|
462
|
-
return 'not-an-alias';
|
|
463
|
-
const sock = socket ?? getDefaultSocketPath();
|
|
464
|
-
if (!fs.existsSync(sock))
|
|
465
|
-
return 'no-server';
|
|
466
|
-
if (!(await hasSession(selector, sock).catch(() => false)))
|
|
467
|
-
return 'absent';
|
|
468
|
-
const panes = await runTmux({
|
|
469
|
-
socket: sock,
|
|
470
|
-
args: ['list-panes', '-t', `=${selector}`, '-F', '#{pane_dead}'],
|
|
471
|
-
}).catch(() => undefined);
|
|
472
|
-
const states = (panes?.stdout ?? '').split('\n').map(s => s.trim()).filter(Boolean);
|
|
473
|
-
return states.some(d => d === '0') ? 'live' : 'dead';
|
|
474
|
-
}
|
|
475
|
-
async function attachLiveTmuxAlias(selector) {
|
|
476
|
-
const socket = getDefaultSocketPath();
|
|
477
|
-
if (await resolveTmuxAliasState(selector, socket) !== 'live')
|
|
478
|
-
return false;
|
|
479
|
-
if (!process.stdout.isTTY) {
|
|
480
|
-
console.error(chalk.red(`"${selector}" is a live tmux session, but attaching needs a TTY.`));
|
|
481
|
-
console.error(chalk.gray(` Run it from a terminal, or: agents tmux attach ${selector}`));
|
|
482
|
-
process.exitCode = 1;
|
|
483
|
-
return true;
|
|
484
|
-
}
|
|
485
|
-
// Already inside a tmux client on this socket — which is the normal state for
|
|
486
|
-
// an interactive agent session here — MOVE that client instead of nesting a
|
|
487
|
-
// second one. jumpTo (go.ts) does the same; attaching from inside tmux
|
|
488
|
-
// otherwise stacks clients rather than taking you to the session.
|
|
489
|
-
if (process.env.TMUX) {
|
|
490
|
-
await runTmux({ socket, args: ['switch-client', '-t', `=${selector}`], throwOnError: false }).catch(() => { });
|
|
491
|
-
console.log(chalk.gray(`Switched this tmux client to ${selector}.`));
|
|
492
|
-
return true;
|
|
493
|
-
}
|
|
494
|
-
console.log(chalk.gray(`Attaching ${selector} — Ctrl-b d to detach.`));
|
|
495
|
-
// Repair a legacy/stale pane-died hook before handing the session to an
|
|
496
|
-
// attach client — the 5-min daemon reconcile that used to cover this was
|
|
497
|
-
// deleted; attach-time repair is what closes the gap now (RUSH-2435).
|
|
498
|
-
await ensureSessionHookRepaired(selector, socket);
|
|
499
|
-
const code = await attachTmux({ socket, args: ['attach-session', '-t', `=${selector}`] });
|
|
500
|
-
process.exitCode = code;
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
438
|
function hasFocusFilters(opts, statuses) {
|
|
504
439
|
return statuses.length > 0 || !!(opts.active || opts.local || opts.device?.length || focusOptionAgent(opts) ||
|
|
505
440
|
opts.all || opts.teams || opts.inTeam || opts.routine || opts.project || opts.skill || opts.plugin ||
|