@phnx-labs/agents-cli 1.22.25 → 1.22.27
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 +427 -0
- package/README.md +35 -3
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +32 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +146 -38
- package/dist/commands/output.js +100 -25
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +349 -37
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -3
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +17 -21
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +10 -1
- package/dist/lib/devices/doctor-findings.js +47 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +191 -12
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +45 -0
- package/dist/lib/hosts/passthrough.js +59 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +6 -2
- package/dist/lib/hosts/ready.js +33 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +56 -21
- package/dist/lib/hosts/reconnect.js +128 -71
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- 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.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +9 -4
- package/dist/lib/runner.js +182 -30
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- 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 +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +38 -3
- package/dist/lib/session/db.js +273 -16
- package/dist/lib/session/discover.d.ts +22 -1
- package/dist/lib/session/discover.js +239 -51
- package/dist/lib/session/insights.d.ts +37 -0
- package/dist/lib/session/insights.js +219 -9
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +42 -3
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { randomUUID } from 'crypto';
|
|
12
12
|
import { sshExec, sshStream, shellQuote } from '../ssh-exec.js';
|
|
13
|
-
import { sshTargetFor } from './types.js';
|
|
13
|
+
import { hostIdentityArgs, sshTargetFor } from './types.js';
|
|
14
14
|
import { ensureHostReady } from './ready.js';
|
|
15
15
|
import { remoteShellFor, posixEnvExports } from './remote-cmd.js';
|
|
16
16
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
@@ -156,7 +156,7 @@ function terminateRemoteLaunch(task) {
|
|
|
156
156
|
`sleep 1; kill -KILL -- -${pid} 2>/dev/null || true; ` +
|
|
157
157
|
`elif kill -0 -- -${pid} 2>/dev/null; then exit 1; fi; ` +
|
|
158
158
|
`rm -f ${task.remoteLog} ${task.remoteExit}`;
|
|
159
|
-
const result = sshExec(task.target, command, { timeoutMs: 10000, multiplex: true });
|
|
159
|
+
const result = sshExec(task.target, command, { timeoutMs: 10000, multiplex: true, extraSshArgs: task.identityFile ? ['-i', task.identityFile, '-o', 'IdentitiesOnly=yes'] : undefined });
|
|
160
160
|
if (result.code !== 0) {
|
|
161
161
|
throw new Error(`Failed to terminate remote task ${task.id} on ${task.host}: ` +
|
|
162
162
|
`${(result.stderr || result.stdout).trim() || 'ssh error'}`);
|
|
@@ -210,7 +210,7 @@ export function stopDispatchedTask(task) {
|
|
|
210
210
|
throw new Error(`Cannot stop remote task ${task.id}: launch returned no PID.`);
|
|
211
211
|
}
|
|
212
212
|
const command = buildStopRemoteCommand(task.pid, task.remoteExit);
|
|
213
|
-
const result = sshExec(task.target, command, { timeoutMs: 10000, multiplex: true });
|
|
213
|
+
const result = sshExec(task.target, command, { timeoutMs: 10000, multiplex: true, extraSshArgs: task.identityFile ? ['-i', task.identityFile, '-o', 'IdentitiesOnly=yes'] : undefined });
|
|
214
214
|
if (result.code !== 0) {
|
|
215
215
|
throw new Error(`Failed to stop remote task ${task.id} on ${task.host}: ` +
|
|
216
216
|
`${(result.stderr || result.stdout).trim() || 'ssh error'}`);
|
|
@@ -271,6 +271,7 @@ async function launchDetached(host, target, opts) {
|
|
|
271
271
|
timeoutMs: 30000,
|
|
272
272
|
multiplex: !opts.copyCreds,
|
|
273
273
|
hostKeyOpts: credHostKeyOpts,
|
|
274
|
+
extraSshArgs: hostIdentityArgs(host),
|
|
274
275
|
});
|
|
275
276
|
if (res.code !== 0) {
|
|
276
277
|
throw new Error(`Failed to launch on "${host.name}": ${(res.stderr || res.stdout).trim() || 'ssh error'}`);
|
|
@@ -280,6 +281,7 @@ async function launchDetached(host, target, opts) {
|
|
|
280
281
|
id,
|
|
281
282
|
host: host.name,
|
|
282
283
|
target,
|
|
284
|
+
identityFile: host.identityFile,
|
|
283
285
|
agent: opts.agentLabel,
|
|
284
286
|
prompt: opts.promptLabel,
|
|
285
287
|
pid: Number.isFinite(pid) ? pid : undefined,
|
|
@@ -311,6 +313,7 @@ async function launchDetached(host, target, opts) {
|
|
|
311
313
|
taskId: id,
|
|
312
314
|
echo: true,
|
|
313
315
|
timeoutMs: opts.timeoutMs,
|
|
316
|
+
extraSshArgs: hostIdentityArgs(host),
|
|
314
317
|
});
|
|
315
318
|
// -1 = the follow window closed while the run continues on the host. Leave the
|
|
316
319
|
// record 'running' (do NOT freeze it terminal) so a later `hosts ps`/`logs`
|
|
@@ -464,6 +467,7 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
464
467
|
tty: process.stdin.isTTY,
|
|
465
468
|
multiplex: !opts.copyCreds,
|
|
466
469
|
hostKeyOpts: credHostKeyOpts,
|
|
470
|
+
extraSshArgs: hostIdentityArgs(host),
|
|
467
471
|
});
|
|
468
472
|
}
|
|
469
473
|
/** Dispatch an `agents run <agent> "<prompt>"` onto a host (the `run --host` path). */
|
|
@@ -24,6 +24,28 @@ interface RemoteSpec {
|
|
|
24
24
|
/** Flags appended when running non-interactively (no local TTY / `--no-tty`). */
|
|
25
25
|
nonInteractive?: string[];
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* First-class groups that run transparently on a remote via SSH when
|
|
29
|
+
* `--host`/`--device` is present. Keep both canonical names and aliases
|
|
30
|
+
* (`repo`/`repos`, `exec`/`run`) so either argv form routes the same way.
|
|
31
|
+
*
|
|
32
|
+
* Prefer adding here over per-command SSH code — this is the single choke point.
|
|
33
|
+
*
|
|
34
|
+
* Every key MUST be a real top-level command (a `KNOWN_TOP_LEVEL_COMMANDS`
|
|
35
|
+
* member) — `passthrough.test.ts` asserts it. A key that is not one is dead:
|
|
36
|
+
* the gate in {@link maybeRunOnHost} rejects the name as unknown before this
|
|
37
|
+
* table is consulted, and before that gate existed it SSH'd a command the peer
|
|
38
|
+
* would also reject. `cli`/`packages`/`versions`/`daemon` were exactly that
|
|
39
|
+
* (the commands are `clis`, `registry`/`search`/`install`/`publish`,
|
|
40
|
+
* `add`/`use`/`list`, and none) and were removed.
|
|
41
|
+
*/
|
|
42
|
+
export declare const REMOTE_PASSTHROUGH: Record<string, RemoteSpec>;
|
|
43
|
+
/**
|
|
44
|
+
* Commands that register and interpret `--host`/`--device` themselves — must
|
|
45
|
+
* fall through to local commander even when the flag is present. Do not add
|
|
46
|
+
* these to {@link REMOTE_PASSTHROUGH}.
|
|
47
|
+
*/
|
|
48
|
+
export declare const OWN_HOST_COMMANDS: Set<string>;
|
|
27
49
|
/** Pull the value of `--host`/`-H`/`--remote-cwd` (any form) out of an argv. */
|
|
28
50
|
export declare function flagValue(args: string[], long: string, short?: string): string | undefined;
|
|
29
51
|
/** Injectable dependencies for {@link runFleetPassthrough} — used by tests. */
|
|
@@ -61,6 +83,29 @@ export declare function runFleetPassthrough(command: string, allArgs: string[],
|
|
|
61
83
|
* @param allArgs `process.argv.slice(2)` — the command name followed by its args.
|
|
62
84
|
*/
|
|
63
85
|
export declare function maybeRunOnHost(command: string, allArgs: string[], opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* `--host` passthrough for a **standalone binary** whose command name is fixed by
|
|
88
|
+
* the binary itself (the `browser`/`computer` bins, `dist/browser.js` etc.) rather
|
|
89
|
+
* than being the first argv token.
|
|
90
|
+
*
|
|
91
|
+
* `agents browser … --host <box>` routes through {@link maybeRunOnHost} in
|
|
92
|
+
* index.ts, but the standalone `browser` binary never enters index.ts — so without
|
|
93
|
+
* this it dropped `--host` entirely (commander errored with `unknown option
|
|
94
|
+
* '--host'`). This wires the identical routing: it synthesizes the implicit command
|
|
95
|
+
* token (`browser`) at the front of the argv so the remote invocation builds as
|
|
96
|
+
* `agents browser …`, delegates to {@link maybeRunOnHost}, and returns `true` when
|
|
97
|
+
* it dispatched the run to a remote (the local program must then NOT parse).
|
|
98
|
+
*
|
|
99
|
+
* On a local / self-host fall-through it rewrites `process.argv` to the ORIGINAL
|
|
100
|
+
* args minus the routing flags — never the synthetic command token — so the
|
|
101
|
+
* standalone commander program parses cleanly and never sees an unknown `--host`.
|
|
102
|
+
* When no routing flag is present it leaves `process.argv` untouched (the common
|
|
103
|
+
* case), so an unrelated flag like `--no-tty` is never stripped from a purely local
|
|
104
|
+
* run.
|
|
105
|
+
*
|
|
106
|
+
* @param command the fixed command name the binary stands for (`'browser'`).
|
|
107
|
+
*/
|
|
108
|
+
export declare function maybeRunStandaloneOnHost(command: string, opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
64
109
|
/**
|
|
65
110
|
* Run `agents <forwardedArgs>` on `host` over SSH, streaming its output, and
|
|
66
111
|
* return the exit code. The single place the SSH hop is built, so every remote
|
|
@@ -21,7 +21,7 @@ import { assertValidSshTarget, sshStream } from '../ssh-exec.js';
|
|
|
21
21
|
import { resolveHost, resolveHostByCap } from './registry.js';
|
|
22
22
|
import { sshTargetFor } from './types.js';
|
|
23
23
|
import { dispatchAgentsCommand, withActorEnv } from './dispatch.js';
|
|
24
|
-
import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
|
|
24
|
+
import { stripRoutingFlags, buildRemoteAgentsInvocation, stripClixml, HOST_ROUTING_SPECS, } from './remote-cmd.js';
|
|
25
25
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
26
26
|
import { machineId } from '../session/sync/config.js';
|
|
27
27
|
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
@@ -36,8 +36,16 @@ import { isKnownTopLevelCommand } from '../startup/command-registry.js';
|
|
|
36
36
|
* (`repo`/`repos`, `exec`/`run`) so either argv form routes the same way.
|
|
37
37
|
*
|
|
38
38
|
* Prefer adding here over per-command SSH code — this is the single choke point.
|
|
39
|
+
*
|
|
40
|
+
* Every key MUST be a real top-level command (a `KNOWN_TOP_LEVEL_COMMANDS`
|
|
41
|
+
* member) — `passthrough.test.ts` asserts it. A key that is not one is dead:
|
|
42
|
+
* the gate in {@link maybeRunOnHost} rejects the name as unknown before this
|
|
43
|
+
* table is consulted, and before that gate existed it SSH'd a command the peer
|
|
44
|
+
* would also reject. `cli`/`packages`/`versions`/`daemon` were exactly that
|
|
45
|
+
* (the commands are `clis`, `registry`/`search`/`install`/`publish`,
|
|
46
|
+
* `add`/`use`/`list`, and none) and were removed.
|
|
39
47
|
*/
|
|
40
|
-
const REMOTE_PASSTHROUGH = {
|
|
48
|
+
export const REMOTE_PASSTHROUGH = {
|
|
41
49
|
// status / inspect
|
|
42
50
|
view: {},
|
|
43
51
|
inspect: {},
|
|
@@ -65,10 +73,8 @@ const REMOTE_PASSTHROUGH = {
|
|
|
65
73
|
permissions: {},
|
|
66
74
|
perms: {},
|
|
67
75
|
mcp: {},
|
|
68
|
-
cli: {},
|
|
69
76
|
subagents: {},
|
|
70
77
|
workflows: {},
|
|
71
|
-
packages: {},
|
|
72
78
|
models: {},
|
|
73
79
|
profiles: {},
|
|
74
80
|
defaults: {},
|
|
@@ -89,20 +95,18 @@ const REMOTE_PASSTHROUGH = {
|
|
|
89
95
|
lock: {},
|
|
90
96
|
feedback: {},
|
|
91
97
|
wallet: {},
|
|
92
|
-
daemon: {},
|
|
93
98
|
pty: {},
|
|
94
99
|
tmux: {},
|
|
95
100
|
watchdog: {},
|
|
96
101
|
factory: {},
|
|
97
102
|
browser: {},
|
|
98
|
-
versions: {},
|
|
99
103
|
};
|
|
100
104
|
/**
|
|
101
105
|
* Commands that register and interpret `--host`/`--device` themselves — must
|
|
102
106
|
* fall through to local commander even when the flag is present. Do not add
|
|
103
107
|
* these to {@link REMOTE_PASSTHROUGH}.
|
|
104
108
|
*/
|
|
105
|
-
const OWN_HOST_COMMANDS = new Set([
|
|
109
|
+
export const OWN_HOST_COMMANDS = new Set([
|
|
106
110
|
'run',
|
|
107
111
|
'exec', // deprecated alias of run
|
|
108
112
|
'harness', // `--host <agent>` names the host CLI to run under, not a remote device
|
|
@@ -186,7 +190,9 @@ function buildFleetForwardedArgs(allArgs) {
|
|
|
186
190
|
/** Parse stdout as JSON; on failure return an object describing the error. */
|
|
187
191
|
function safeJsonParse(stdout) {
|
|
188
192
|
try {
|
|
189
|
-
|
|
193
|
+
// A Windows device relays its `--json` through PowerShell, which can prefix a
|
|
194
|
+
// CLIXML banner ahead of the payload — strip it before parsing (RUSH-2286).
|
|
195
|
+
return JSON.parse(stripClixml(stdout));
|
|
190
196
|
}
|
|
191
197
|
catch {
|
|
192
198
|
return { parseError: 'invalid JSON', snippet: stdout.trim().slice(0, 200) };
|
|
@@ -551,6 +557,51 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
551
557
|
});
|
|
552
558
|
return true;
|
|
553
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* `--host` passthrough for a **standalone binary** whose command name is fixed by
|
|
562
|
+
* the binary itself (the `browser`/`computer` bins, `dist/browser.js` etc.) rather
|
|
563
|
+
* than being the first argv token.
|
|
564
|
+
*
|
|
565
|
+
* `agents browser … --host <box>` routes through {@link maybeRunOnHost} in
|
|
566
|
+
* index.ts, but the standalone `browser` binary never enters index.ts — so without
|
|
567
|
+
* this it dropped `--host` entirely (commander errored with `unknown option
|
|
568
|
+
* '--host'`). This wires the identical routing: it synthesizes the implicit command
|
|
569
|
+
* token (`browser`) at the front of the argv so the remote invocation builds as
|
|
570
|
+
* `agents browser …`, delegates to {@link maybeRunOnHost}, and returns `true` when
|
|
571
|
+
* it dispatched the run to a remote (the local program must then NOT parse).
|
|
572
|
+
*
|
|
573
|
+
* On a local / self-host fall-through it rewrites `process.argv` to the ORIGINAL
|
|
574
|
+
* args minus the routing flags — never the synthetic command token — so the
|
|
575
|
+
* standalone commander program parses cleanly and never sees an unknown `--host`.
|
|
576
|
+
* When no routing flag is present it leaves `process.argv` untouched (the common
|
|
577
|
+
* case), so an unrelated flag like `--no-tty` is never stripped from a purely local
|
|
578
|
+
* run.
|
|
579
|
+
*
|
|
580
|
+
* @param command the fixed command name the binary stands for (`'browser'`).
|
|
581
|
+
*/
|
|
582
|
+
export async function maybeRunStandaloneOnHost(command, opts) {
|
|
583
|
+
const rawArgs = process.argv.slice(2);
|
|
584
|
+
const hasRoutingFlag = flagValue(rawArgs, 'host', 'H') !== undefined ||
|
|
585
|
+
flagValue(rawArgs, 'device') !== undefined ||
|
|
586
|
+
flagValue(rawArgs, 'hosts') !== undefined ||
|
|
587
|
+
flagValue(rawArgs, 'devices') !== undefined;
|
|
588
|
+
// No routing flag → nothing to route or strip. Leave argv alone so a purely
|
|
589
|
+
// local run keeps every flag it passed.
|
|
590
|
+
if (!hasRoutingFlag)
|
|
591
|
+
return false;
|
|
592
|
+
// Keep --help/--version local (docs must work without a reachable host), mirroring
|
|
593
|
+
// index.ts's `helpOrVersionRequested` guard, but still strip the routing flags
|
|
594
|
+
// below so commander doesn't choke on them.
|
|
595
|
+
const helpOrVersion = rawArgs.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V');
|
|
596
|
+
if (!helpOrVersion && (await maybeRunOnHost(command, [command, ...rawArgs], opts))) {
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
// Local / self-host fall-through (maybeRunOnHost may have rewritten process.argv
|
|
600
|
+
// with the synthetic command token). Rebuild argv from the original args minus
|
|
601
|
+
// the routing flags so the standalone program parses cleanly.
|
|
602
|
+
process.argv = [process.argv[0], process.argv[1], ...stripRoutingFlags(rawArgs, STRIP_SPECS)];
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
554
605
|
/**
|
|
555
606
|
* Run `agents <forwardedArgs>` on `host` over SSH, streaming its output, and
|
|
556
607
|
* return the exit code. The single place the SSH hop is built, so every remote
|
|
@@ -55,6 +55,7 @@ export interface FollowOptions {
|
|
|
55
55
|
pollMs?: number;
|
|
56
56
|
/** Idle-backoff ceiling (default 4× the fast interval, min 4000ms). */
|
|
57
57
|
maxPollMs?: number;
|
|
58
|
+
extraSshArgs?: string[];
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
60
61
|
* Build the per-task sentinel that separates the log tail from the exit-file
|
|
@@ -124,7 +125,7 @@ export declare function parseStreamingExitFrame(stderr: Buffer, taskId: string):
|
|
|
124
125
|
* File identity (`dev:ino`) of a path on the remote host, or null if it can't be
|
|
125
126
|
* stat'd. GNU (`-c`) then BSD (`-f`) format, so it works on Linux and macOS hosts.
|
|
126
127
|
*/
|
|
127
|
-
export declare function readRemoteFileId(target: string, remotePath: string): string | null;
|
|
128
|
+
export declare function readRemoteFileId(target: string, remotePath: string, extraSshArgs?: string[]): string | null;
|
|
128
129
|
/**
|
|
129
130
|
* True when the local mirror file IS the very file we're tailing — the
|
|
130
131
|
* localhost-as-host case, where `remoteLog` ($HOME-expanded) and `localLogPath`
|
|
@@ -160,8 +160,8 @@ export function parseStreamingExitFrame(stderr, taskId) {
|
|
|
160
160
|
* File identity (`dev:ino`) of a path on the remote host, or null if it can't be
|
|
161
161
|
* stat'd. GNU (`-c`) then BSD (`-f`) format, so it works on Linux and macOS hosts.
|
|
162
162
|
*/
|
|
163
|
-
export function readRemoteFileId(target, remotePath) {
|
|
164
|
-
const res = sshExec(target, `stat -c '%d:%i' ${remotePath} 2>/dev/null || stat -f '%d:%i' ${remotePath} 2>/dev/null`, { timeoutMs: 8000 });
|
|
163
|
+
export function readRemoteFileId(target, remotePath, extraSshArgs) {
|
|
164
|
+
const res = sshExec(target, `stat -c '%d:%i' ${remotePath} 2>/dev/null || stat -f '%d:%i' ${remotePath} 2>/dev/null`, { timeoutMs: 8000, extraSshArgs });
|
|
165
165
|
const id = res.stdout.trim();
|
|
166
166
|
return id || null;
|
|
167
167
|
}
|
|
@@ -189,7 +189,7 @@ export async function followHostTask(target, opts) {
|
|
|
189
189
|
let mirror = true;
|
|
190
190
|
try {
|
|
191
191
|
const s = fs.statSync(local);
|
|
192
|
-
if (mirrorAliasesSource(`${s.dev}:${s.ino}`, readRemoteFileId(target, opts.remoteLog))) {
|
|
192
|
+
if (mirrorAliasesSource(`${s.dev}:${s.ino}`, readRemoteFileId(target, opts.remoteLog, opts.extraSshArgs))) {
|
|
193
193
|
mirror = false;
|
|
194
194
|
}
|
|
195
195
|
}
|
|
@@ -223,6 +223,7 @@ export async function followHostTask(target, opts) {
|
|
|
223
223
|
timeoutMs: remaining,
|
|
224
224
|
signal: abort.signal,
|
|
225
225
|
multiplex: true,
|
|
226
|
+
extraSshArgs: opts.extraSshArgs,
|
|
226
227
|
onStdout: (chunk) => { gotOutput = flush(chunk) || gotOutput; },
|
|
227
228
|
});
|
|
228
229
|
exitFrame = parseStreamingExitFrame(stream.stderr, opts.taskId);
|
|
@@ -31,7 +31,7 @@ export declare function remoteAgentsVersion(target: string, os?: string): string
|
|
|
31
31
|
* equivalents (`Select-Object -Last`, `Test-Path`). Pure/exported. */
|
|
32
32
|
export declare function buildBootstrapCommand(spec: string, os?: string): string;
|
|
33
33
|
/** Install (or upgrade to) a specific agents-cli version on the remote, then `agents setup`. */
|
|
34
|
-
export declare function bootstrapAgentsCli(target: string, version: string | null, os?: string): {
|
|
34
|
+
export declare function bootstrapAgentsCli(target: string, version: string | null, os?: string, extraSshArgs?: string[]): {
|
|
35
35
|
ok: boolean;
|
|
36
36
|
output: string;
|
|
37
37
|
};
|
|
@@ -42,6 +42,8 @@ export interface ReadyProbe {
|
|
|
42
42
|
version: string | null;
|
|
43
43
|
/** Raw `agents view`/`list` output, for installed-agent checks. */
|
|
44
44
|
view: string;
|
|
45
|
+
/** True when the ssh probe timed out before the sentinel arrived. */
|
|
46
|
+
timedOut?: boolean;
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
49
|
* Answer every readiness question in ONE ssh round-trip: reachable? (the login
|
|
@@ -59,11 +61,13 @@ export interface ReadyProbe {
|
|
|
59
61
|
* absorbed by its `.trim()`. Pure/exported so both branches are unit-testable.
|
|
60
62
|
*/
|
|
61
63
|
export declare function buildReadyProbeCommand(os?: string): string;
|
|
62
|
-
export declare function readyProbe(target: string, os?: string): ReadyProbe;
|
|
64
|
+
export declare function readyProbe(target: string, os?: string, extraSshArgs?: string[]): ReadyProbe;
|
|
63
65
|
/** Pure parser for `readyProbe` output (unit-tested without ssh). */
|
|
64
66
|
export declare function parseReadyProbe(stdout: string): ReadyProbe;
|
|
65
67
|
/** True if `view` output lists the named agent (word-boundary, case-insensitive). */
|
|
66
68
|
export declare function viewHasAgent(view: string, agent: string): boolean;
|
|
69
|
+
/** Read the requested harness's sign-in verdict from `agents view --json`. */
|
|
70
|
+
export declare function viewAgentSignedIn(view: string, agent: string): boolean | undefined;
|
|
67
71
|
export interface EnsureReadyOptions {
|
|
68
72
|
agent: string;
|
|
69
73
|
/** Throw instead of warn when the agent isn't installed remotely. */
|
package/dist/lib/hosts/ready.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { sshExec, shellQuote } from '../ssh-exec.js';
|
|
12
|
-
import { sshTargetFor } from './types.js';
|
|
12
|
+
import { hostIdentityArgs, sshTargetFor } from './types.js';
|
|
13
13
|
import { remoteShellFor, buildWindowsAgentsCommand, encodePowershell, powershellQuote, POWERSHELL_PROGRESS_SILENCE } from './remote-cmd.js';
|
|
14
14
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
15
15
|
/** Resolve this CLI's own version by walking up to the nearest package.json. */
|
|
@@ -87,9 +87,9 @@ export function buildBootstrapCommand(spec, os) {
|
|
|
87
87
|
return `bash -lc ${shellQuote(script)}`;
|
|
88
88
|
}
|
|
89
89
|
/** Install (or upgrade to) a specific agents-cli version on the remote, then `agents setup`. */
|
|
90
|
-
export function bootstrapAgentsCli(target, version, os) {
|
|
90
|
+
export function bootstrapAgentsCli(target, version, os, extraSshArgs) {
|
|
91
91
|
const spec = version ? `@phnx-labs/agents-cli@${version}` : '@phnx-labs/agents-cli';
|
|
92
|
-
const r = sshExec(target, buildBootstrapCommand(spec, os), { timeoutMs: 300000 });
|
|
92
|
+
const r = sshExec(target, buildBootstrapCommand(spec, os), { timeoutMs: 300000, extraSshArgs });
|
|
93
93
|
return { ok: r.code === 0, output: (r.stdout + r.stderr).trim() };
|
|
94
94
|
}
|
|
95
95
|
/** Sentinel splitting the version output from the agent listing in one probe. */
|
|
@@ -113,15 +113,19 @@ export function buildReadyProbeCommand(os) {
|
|
|
113
113
|
if (remoteShellFor(os) === 'powershell') {
|
|
114
114
|
const script = `${POWERSHELL_PROGRESS_SILENCE}; ` +
|
|
115
115
|
`agents --version 2>$null; Write-Output "${READY_MARKER}"; ` +
|
|
116
|
-
`agents view 2>$null; if ($LASTEXITCODE -ne 0) { agents list 2>$null }`;
|
|
116
|
+
`agents view --json 2>$null; if ($LASTEXITCODE -ne 0) { agents list 2>$null }`;
|
|
117
117
|
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
118
118
|
}
|
|
119
119
|
const script = `agents --version 2>/dev/null; printf '\\n${READY_MARKER}\\n'; ` +
|
|
120
|
-
`agents view 2>/dev/null || agents list 2>/dev/null`;
|
|
120
|
+
`agents view --json 2>/dev/null || agents list 2>/dev/null`;
|
|
121
121
|
return `bash -lc ${shellQuote(script)}`;
|
|
122
122
|
}
|
|
123
|
-
export function readyProbe(target, os) {
|
|
124
|
-
|
|
123
|
+
export function readyProbe(target, os, extraSshArgs) {
|
|
124
|
+
// Disable multiplexing: a stale control socket can hang the local ssh client
|
|
125
|
+
// until the timeout fires, just like sshExecAsync does for the same reason.
|
|
126
|
+
const r = sshExec(target, buildReadyProbeCommand(os), { timeoutMs: 20000, multiplex: false, extraSshArgs });
|
|
127
|
+
if (r.timedOut)
|
|
128
|
+
return { reachable: false, version: null, view: '', timedOut: true };
|
|
125
129
|
return parseReadyProbe(r.stdout);
|
|
126
130
|
}
|
|
127
131
|
/** Pure parser for `readyProbe` output (unit-tested without ssh). */
|
|
@@ -136,6 +140,22 @@ export function parseReadyProbe(stdout) {
|
|
|
136
140
|
export function viewHasAgent(view, agent) {
|
|
137
141
|
return new RegExp(`\\b${agent}\\b`, 'i').test(view);
|
|
138
142
|
}
|
|
143
|
+
/** Read the requested harness's sign-in verdict from `agents view --json`. */
|
|
144
|
+
export function viewAgentSignedIn(view, agent) {
|
|
145
|
+
try {
|
|
146
|
+
const rows = JSON.parse(view);
|
|
147
|
+
const row = rows.find((candidate) => candidate.agent?.toLowerCase() === agent.toLowerCase());
|
|
148
|
+
if (!row)
|
|
149
|
+
return undefined;
|
|
150
|
+
const verdicts = (row.versions ?? [])
|
|
151
|
+
.map((version) => version.signedIn)
|
|
152
|
+
.filter((value) => typeof value === 'boolean');
|
|
153
|
+
return verdicts.length === 0 ? undefined : verdicts.some(Boolean);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
139
159
|
/**
|
|
140
160
|
* Verify a host can run the agent: reachable + agents-cli present. Throws with an
|
|
141
161
|
* actionable message otherwise. Agent-not-installed is a warning by default (the
|
|
@@ -145,7 +165,12 @@ export function viewHasAgent(view, agent) {
|
|
|
145
165
|
*/
|
|
146
166
|
export function ensureHostReady(host, opts) {
|
|
147
167
|
const target = sshTargetFor(host);
|
|
148
|
-
const probe = readyProbe(target, host.os ?? resolveRemoteOsSync(host.name));
|
|
168
|
+
const probe = readyProbe(target, host.os ?? resolveRemoteOsSync(host.name), hostIdentityArgs(host));
|
|
169
|
+
if (probe.timedOut) {
|
|
170
|
+
throw new Error(`Host "${host.name}" (${target}) did not respond in time — the SSH probe timed out after 20 seconds. ` +
|
|
171
|
+
`The host may be slow to start a login shell (nvm/sdkman init, cold node startup). ` +
|
|
172
|
+
`Retry, or run \`agents ssh ${host.name} agents view\` to confirm manually.`);
|
|
173
|
+
}
|
|
149
174
|
if (!probe.reachable) {
|
|
150
175
|
throw new Error(`Host "${host.name}" (${target}) is not reachable over SSH. Check it's online and key auth works.`);
|
|
151
176
|
}
|
|
@@ -35,7 +35,7 @@ export declare function classifyExit(res: Pick<SshExecResult, 'code' | 'stdout'
|
|
|
35
35
|
* $HOME-prefixed path with a safe (hex) basename — intentionally unquoted so the
|
|
36
36
|
* remote shell expands $HOME (same contract as progress.ts's fetch).
|
|
37
37
|
*/
|
|
38
|
-
export declare function readRemoteExit(target: string, remoteExit: string, timeoutMs?: number): RemoteExitState;
|
|
38
|
+
export declare function readRemoteExit(target: string, remoteExit: string, timeoutMs?: number, identityFile?: string): RemoteExitState;
|
|
39
39
|
/**
|
|
40
40
|
* Heal one record. Terminal records are immutable (and never re-probed); a
|
|
41
41
|
* `running` record is resolved to completed/failed only when the remote `.exit`
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* record. We only ever CONFIRM completion; an unreachable host or an absent
|
|
11
11
|
* `.exit` leaves the record `running` (we never guess failure).
|
|
12
12
|
*/
|
|
13
|
-
import { sshExec
|
|
13
|
+
import { sshExec } from '../ssh-exec.js';
|
|
14
14
|
import { updateTask, terminalPatch } from './tasks.js';
|
|
15
15
|
/**
|
|
16
16
|
* Classify a `cat <remoteExit>` result into a remote run state. Pure: all the
|
|
@@ -35,8 +35,12 @@ export function classifyExit(res) {
|
|
|
35
35
|
* $HOME-prefixed path with a safe (hex) basename — intentionally unquoted so the
|
|
36
36
|
* remote shell expands $HOME (same contract as progress.ts's fetch).
|
|
37
37
|
*/
|
|
38
|
-
export function readRemoteExit(target, remoteExit, timeoutMs = 6000) {
|
|
39
|
-
return classifyExit(sshExec(target, `cat ${remoteExit} 2>/dev/null`, {
|
|
38
|
+
export function readRemoteExit(target, remoteExit, timeoutMs = 6000, identityFile) {
|
|
39
|
+
return classifyExit(sshExec(target, `cat ${remoteExit} 2>/dev/null`, {
|
|
40
|
+
timeoutMs,
|
|
41
|
+
multiplex: true,
|
|
42
|
+
extraSshArgs: identityFile ? ['-i', identityFile, '-o', 'IdentitiesOnly=yes'] : undefined,
|
|
43
|
+
}));
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
42
46
|
* Heal one record. Terminal records are immutable (and never re-probed); a
|
|
@@ -46,7 +50,7 @@ export function readRemoteExit(target, remoteExit, timeoutMs = 6000) {
|
|
|
46
50
|
export function reconcileTask(task) {
|
|
47
51
|
if (task.status !== 'running')
|
|
48
52
|
return task;
|
|
49
|
-
const st = readRemoteExit(task.target, task.remoteExit);
|
|
53
|
+
const st = readRemoteExit(task.target, task.remoteExit, 6000, task.identityFile);
|
|
50
54
|
if (st.state !== 'done')
|
|
51
55
|
return task;
|
|
52
56
|
return updateTask(task.id, terminalPatch(st.code)) ?? task;
|
|
@@ -66,11 +70,16 @@ export function reconcileRunningTasks(tasks) {
|
|
|
66
70
|
const reachable = new Map();
|
|
67
71
|
const patched = new Map();
|
|
68
72
|
for (const t of running) {
|
|
69
|
-
if (!reachable.has(t.target))
|
|
70
|
-
|
|
73
|
+
if (!reachable.has(t.target)) {
|
|
74
|
+
const probe = sshExec(t.target, 'true', {
|
|
75
|
+
timeoutMs: 6000,
|
|
76
|
+
extraSshArgs: t.identityFile ? ['-i', t.identityFile, '-o', 'IdentitiesOnly=yes'] : undefined,
|
|
77
|
+
});
|
|
78
|
+
reachable.set(t.target, probe.code === 0);
|
|
79
|
+
}
|
|
71
80
|
if (!reachable.get(t.target))
|
|
72
81
|
continue; // host down → leave running
|
|
73
|
-
const st = readRemoteExit(t.target, t.remoteExit);
|
|
82
|
+
const st = readRemoteExit(t.target, t.remoteExit, 6000, t.identityFile);
|
|
74
83
|
if (st.state === 'done') {
|
|
75
84
|
const updated = updateTask(t.id, terminalPatch(st.code));
|
|
76
85
|
if (updated)
|
|
@@ -6,13 +6,23 @@ export declare const SSH_CONN_FAILURE = 255;
|
|
|
6
6
|
* {@link wrapRemoteExitCode} — see the file header. Never produced by the ssh
|
|
7
7
|
* transport itself, so it can never be confused with {@link SSH_CONN_FAILURE}. */
|
|
8
8
|
export declare const REMOTE_EXIT_255_REMAPPED = 254;
|
|
9
|
-
/** Consecutive
|
|
10
|
-
* {@link MAX_BACKOFF_MS}. A reattach that
|
|
11
|
-
* refills the budget, so a long session that blinks all day reconnects every time
|
|
12
|
-
*
|
|
9
|
+
/** Consecutive unproductive reattaches before giving up. Backoff is capped at
|
|
10
|
+
* {@link MAX_BACKOFF_MS}. A reattach that reconnected and HELD (then dropped again)
|
|
11
|
+
* refills the budget, so a long session that blinks all day reconnects every time —
|
|
12
|
+
* an attempt that never reached the host, or reached it and died back inside
|
|
13
|
+
* {@link MIN_HOLD_MS}, counts against it. */
|
|
13
14
|
export declare const MAX_ATTEMPTS = 6;
|
|
15
|
+
/** How long a reattach must hold the remote pane before it counts as a genuine
|
|
16
|
+
* reconnection that refills the budget. Measured on the interactive attach ALONE
|
|
17
|
+
* — the preflight probe has already returned by then, so this is not the connect
|
|
18
|
+
* timing the file header rules out. 10s is comfortably longer than any attach that
|
|
19
|
+
* dies during TTY negotiation and far shorter than a session the user is working
|
|
20
|
+
* in; a link that drops the user out inside 10s on every attempt is one this loop
|
|
21
|
+
* should stop retrying, not one it should keep re-entering. */
|
|
22
|
+
export declare const MIN_HOLD_MS = 10000;
|
|
14
23
|
export interface ReconnectState {
|
|
15
|
-
/** Consecutive
|
|
24
|
+
/** Consecutive unproductive reattaches since the last genuine reconnection — one
|
|
25
|
+
* that reached the host and held for {@link MIN_HOLD_MS}. */
|
|
16
26
|
attempt: number;
|
|
17
27
|
}
|
|
18
28
|
export interface ReconnectOutcome {
|
|
@@ -20,8 +30,13 @@ export interface ReconnectOutcome {
|
|
|
20
30
|
code: number;
|
|
21
31
|
/** Whether the ssh handshake for this attempt actually completed. The initial run
|
|
22
32
|
* and any reattach whose preflight probe succeeded are `connected`; a reattach
|
|
23
|
-
* that couldn't reach the host is not.
|
|
33
|
+
* that couldn't reach the host is not. Half of the budget refill (see file head). */
|
|
24
34
|
connected: boolean;
|
|
35
|
+
/** Wall-clock ms the interactive attach ran for, timed from after the preflight
|
|
36
|
+
* probe returned. `0` when the attempt never reached the host. The other half of
|
|
37
|
+
* the refill: it must be at least {@link MIN_HOLD_MS} to count as a genuine
|
|
38
|
+
* reconnection rather than a link that drops the user straight back out. */
|
|
39
|
+
heldMs: number;
|
|
25
40
|
}
|
|
26
41
|
export type ReconnectDecision = {
|
|
27
42
|
action: 'stop';
|
|
@@ -34,6 +49,14 @@ export type ReconnectDecision = {
|
|
|
34
49
|
export declare function initialReconnectState(): ReconnectState;
|
|
35
50
|
/** Exponential backoff capped at {@link MAX_BACKOFF_MS}: 2s, 4s, 8s, 16s, 30s… */
|
|
36
51
|
export declare function backoffMs(attempt: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* Did this attempt genuinely put the user back into the agent? Only such an attempt
|
|
54
|
+
* refills the retry budget — it must have reached the host AND held the pane for at
|
|
55
|
+
* least {@link MIN_HOLD_MS}. An attach that reached the host and died right back is
|
|
56
|
+
* a flapping link, not a reconnection, and counts against the budget like an
|
|
57
|
+
* unreachable host (agents-cli#1884; see the file header).
|
|
58
|
+
*/
|
|
59
|
+
export declare function refillsBudget(outcome: ReconnectOutcome): boolean;
|
|
37
60
|
/**
|
|
38
61
|
* Decide what to do after a run/re-attach returned `outcome`. Pure — the only
|
|
39
62
|
* input is the prior state and the outcome, the only output is the next action.
|
|
@@ -41,15 +64,25 @@ export declare function backoffMs(attempt: number): number;
|
|
|
41
64
|
* - a non-255 code means the remote command spoke for itself (clean detach = 0,
|
|
42
65
|
* agent exit / no live session = non-zero) → stop and surface that code.
|
|
43
66
|
* - a 255 means the link dropped → retry, unless the budget is spent.
|
|
44
|
-
* - a 255 from an attempt that
|
|
45
|
-
*
|
|
46
|
-
*
|
|
67
|
+
* - a 255 from an attempt that reconnected AND held ({@link refillsBudget})
|
|
68
|
+
* refills the budget first; every other 255 counts against it, so a host that
|
|
69
|
+
* stays unreachable — and a link that keeps dropping the attach immediately —
|
|
70
|
+
* both give up after MAX_ATTEMPTS.
|
|
47
71
|
*/
|
|
48
72
|
export declare function reconnectStep(state: ReconnectState, outcome: ReconnectOutcome): ReconnectDecision;
|
|
49
73
|
/** Human-readable notice shown before each reconnect wait. "13 seconds", not "12.8s". */
|
|
50
74
|
export declare function reconnectNotice(sessionId: string, host: string, attempt: number, waitMs: number): string;
|
|
51
|
-
/** Notice shown once the retry budget is spent.
|
|
75
|
+
/** Notice shown once the retry budget is spent on a host that stayed UNREACHABLE.
|
|
76
|
+
* Hands back the one verb that re-enters the terminal — attach the live pane if it
|
|
77
|
+
* survived, else resume. */
|
|
52
78
|
export declare function exhaustedNotice(sessionId: string, host: string): string;
|
|
79
|
+
/** Notice shown when the budget is spent the OTHER way: the last reattach reached
|
|
80
|
+
* the host and the connection dropped again within {@link MIN_HOLD_MS}. Saying
|
|
81
|
+
* "couldn't reconnect" there would be false — it did reconnect and could not stay
|
|
82
|
+
* — and the user needs to know the link, not the host, is the problem. It claims
|
|
83
|
+
* no count of successful reconnections: the budget can also be spent by a run of
|
|
84
|
+
* unreachable attempts followed by one that reconnected and dropped straight out. */
|
|
85
|
+
export declare function unstableNotice(sessionId: string, host: string): string;
|
|
53
86
|
/** Notice shown when a reattach stops on a remapped remote-side exit
|
|
54
87
|
* ({@link REMOTE_EXIT_255_REMAPPED} — a would-be-255 the remote command decided
|
|
55
88
|
* on for its own reasons, not the ssh transport dropping; see
|
|
@@ -75,13 +108,14 @@ export declare function remoteExitNotice(sessionId: string, host: string): strin
|
|
|
75
108
|
*/
|
|
76
109
|
export declare function wrapRemoteExitCode(cmd: string): string;
|
|
77
110
|
/**
|
|
78
|
-
* The remote command a reattach runs — the peer's own
|
|
79
|
-
* (`agents sessions focus <id> --local
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
111
|
+
* The remote command a reattach runs — the peer's own recovery verb
|
|
112
|
+
* (`agents sessions focus <id> --local`), wrapped by {@link wrapRemoteExitCode}
|
|
113
|
+
* so a stray remote-origin 255 (from this command, whatever produces it — see the
|
|
114
|
+
* file header) can never masquerade as a network drop. No `--attach-only`: focus
|
|
115
|
+
* joins the live pane when it survived, else RESUMES the session in place, so a
|
|
116
|
+
* reattach landing after the pane died recovers the agent instead of dead-ending
|
|
117
|
+
* (RUSH-2085). Split out from {@link reattachRemoteSession} so it is unit-tested
|
|
118
|
+
* without SSH — mirrors `remoteAgentsJsonCommand` in lib/remote-agents-json.ts.
|
|
85
119
|
*/
|
|
86
120
|
export declare function reattachRemoteCommand(sessionId: string): string;
|
|
87
121
|
/**
|
|
@@ -89,10 +123,11 @@ export declare function reattachRemoteCommand(sessionId: string): string;
|
|
|
89
123
|
* `agents sessions focus`. A fast, un-multiplexed preflight probe (`ssh … true`)
|
|
90
124
|
* first establishes whether the host is actually reachable this attempt — that
|
|
91
125
|
* `connected` bit, not the call duration, is what the retry policy keys on. Only on
|
|
92
|
-
* a reachable host do we run the interactive attach (which carries no
|
|
93
|
-
* the agent already runs on the peer — so it rides the normal
|
|
94
|
-
* the ssh exit code (255 = dropped again / unreachable; 0 =
|
|
95
|
-
* session ended)
|
|
126
|
+
* a reachable host do we run the interactive attach-or-resume (which carries no
|
|
127
|
+
* credentials — the agent already runs on the peer — so it rides the normal
|
|
128
|
+
* transport). Returns the ssh exit code (255 = dropped again / unreachable; 0 =
|
|
129
|
+
* clean detach; other = session ended), whether this attempt connected, and how
|
|
130
|
+
* long the attach held — the two inputs {@link refillsBudget} decides on.
|
|
96
131
|
*/
|
|
97
132
|
export declare function reattachRemoteSession(host: Host, sessionId: string): ReconnectOutcome;
|
|
98
133
|
export interface ReconnectLoopOpts {
|