@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- 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/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- 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 +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -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/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- 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/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -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 +3 -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/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- 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/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- 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/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- 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/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- 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.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- 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/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 +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- 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/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import * as http from 'http';
|
|
30
30
|
import { isControlDevice } from '../devices/registry.js';
|
|
31
31
|
import { loadDevices } from '../devices/registry.js';
|
|
32
|
-
import { fleetDialTarget } from '../devices/connect.js';
|
|
32
|
+
import { deviceIdentityArgs, fleetDialTarget } from '../devices/connect.js';
|
|
33
33
|
import { planFleetTargets } from '../devices/fleet.js';
|
|
34
34
|
import { assertValidSshTarget, shellQuote, sshExecAsync } from '../ssh-exec.js';
|
|
35
35
|
import { machineId } from '../session/sync/config.js';
|
|
@@ -134,6 +134,7 @@ export function selectLoginTargets(devices, agents, cache, opts = {}) {
|
|
|
134
134
|
agent,
|
|
135
135
|
platform: device.platform,
|
|
136
136
|
target: fleetDialTarget(device),
|
|
137
|
+
extraSshArgs: deviceIdentityArgs(device),
|
|
137
138
|
flow,
|
|
138
139
|
remotable,
|
|
139
140
|
reason,
|
|
@@ -152,7 +153,7 @@ export function selectLoginTargets(devices, agents, cache, opts = {}) {
|
|
|
152
153
|
* devices need the askpass shim env, which the PTY env allowlist strips — those
|
|
153
154
|
* are a follow-up (TODO), surfaced as an error rather than silently mis-driven.
|
|
154
155
|
*/
|
|
155
|
-
export function buildRemoteLoginSshCommand(target, flow) {
|
|
156
|
+
export function buildRemoteLoginSshCommand(target, flow, extraSshArgs = []) {
|
|
156
157
|
assertValidSshTarget(target);
|
|
157
158
|
// The agent CLIs (kimi/droid/codex/…) are agents-cli shims that live in
|
|
158
159
|
// ~/.agents/.cache/shims and are only on PATH in an interactive/login shell.
|
|
@@ -165,6 +166,7 @@ export function buildRemoteLoginSshCommand(target, flow) {
|
|
|
165
166
|
'ssh', '-tt',
|
|
166
167
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
167
168
|
'-o', 'ConnectTimeout=10',
|
|
169
|
+
...extraSshArgs.map(shellQuote),
|
|
168
170
|
shellQuote(target),
|
|
169
171
|
shellQuote(remoteCmd),
|
|
170
172
|
].join(' ');
|
|
@@ -367,7 +369,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
367
369
|
* in the browser; the caller polls the credential file for completion. Testable
|
|
368
370
|
* against a fake {@link PtyDriver}.
|
|
369
371
|
*/
|
|
370
|
-
export async function driveRemoteLogin(target, flow, driver, opts = {}) {
|
|
372
|
+
export async function driveRemoteLogin(target, flow, driver, opts = {}, extraSshArgs = []) {
|
|
371
373
|
const initialDelayMs = opts.initialDelayMs ?? 4000;
|
|
372
374
|
const pollMs = opts.pollMs ?? 1000;
|
|
373
375
|
const timeoutMs = opts.timeoutMs ?? 90000;
|
|
@@ -377,7 +379,7 @@ export async function driveRemoteLogin(target, flow, driver, opts = {}) {
|
|
|
377
379
|
// `ssh -tt` process it drives) leaks until the sidecar's idle reaper. On the
|
|
378
380
|
// normal return paths the caller owns stop() via the returned sessionId.
|
|
379
381
|
try {
|
|
380
|
-
await driver.exec(id, buildRemoteLoginSshCommand(target, flow));
|
|
382
|
+
await driver.exec(id, buildRemoteLoginSshCommand(target, flow, extraSshArgs));
|
|
381
383
|
await sleep(initialDelayMs);
|
|
382
384
|
if (flow.deviceCodeSelect) {
|
|
383
385
|
await driver.write(id, flow.deviceCodeSelect);
|
|
@@ -431,9 +433,9 @@ export async function detectPending(opts = {}) {
|
|
|
431
433
|
* bumping after the human completes the browser step. POSIX `stat` only (the
|
|
432
434
|
* device-code agents are all POSIX-fleet).
|
|
433
435
|
*/
|
|
434
|
-
export async function remoteFileMtime(target, homeRel) {
|
|
436
|
+
export async function remoteFileMtime(target, homeRel, extraSshArgs = []) {
|
|
435
437
|
const cmd = `stat -c %Y "$HOME/${homeRel}" 2>/dev/null || stat -f %m "$HOME/${homeRel}" 2>/dev/null || echo 0`;
|
|
436
|
-
const res = await sshExecAsync(target, cmd, { timeoutMs: 15000, multiplex: true }).catch(() => null);
|
|
438
|
+
const res = await sshExecAsync(target, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs }).catch(() => null);
|
|
437
439
|
if (!res || res.code !== 0)
|
|
438
440
|
return 0;
|
|
439
441
|
const n = parseInt(res.stdout.trim(), 10);
|
|
@@ -507,10 +509,10 @@ export async function runFleetLogin(opts = {}) {
|
|
|
507
509
|
const k = key(p.device, p.agent);
|
|
508
510
|
const st = statuses.get(k);
|
|
509
511
|
st.state = 'driving';
|
|
510
|
-
const baseMtime = await remoteFileMtime(p.target, p.flow.successFile).catch(() => 0);
|
|
512
|
+
const baseMtime = await remoteFileMtime(p.target, p.flow.successFile, p.extraSshArgs).catch(() => 0);
|
|
511
513
|
let sessionId;
|
|
512
514
|
try {
|
|
513
|
-
const r = await driveRemoteLogin(p.target, p.flow, driver, opts.drive);
|
|
515
|
+
const r = await driveRemoteLogin(p.target, p.flow, driver, opts.drive, p.extraSshArgs);
|
|
514
516
|
sessionId = r.sessionId;
|
|
515
517
|
if (r.url && r.code) {
|
|
516
518
|
st.url = r.url;
|
|
@@ -539,7 +541,7 @@ export async function runFleetLogin(opts = {}) {
|
|
|
539
541
|
st.detail = 'device code expired before authorization';
|
|
540
542
|
break;
|
|
541
543
|
}
|
|
542
|
-
const mtime = await remoteFileMtime(p.target, p.flow.successFile).catch(() => 0);
|
|
544
|
+
const mtime = await remoteFileMtime(p.target, p.flow.successFile, p.extraSshArgs).catch(() => 0);
|
|
543
545
|
if (mtime > baseMtime) {
|
|
544
546
|
st.state = 'authorized';
|
|
545
547
|
break;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export declare function readGeminiSettings(settingsPath: string): Record<string, unknown>;
|
|
2
2
|
export declare function writeGeminiSettings(settingsPath: string, settings: Record<string, unknown>): void;
|
|
3
3
|
export declare function updateGeminiSettings(settingsPath: string, mutate: (settings: Record<string, unknown>) => void): Record<string, unknown>;
|
|
4
|
-
export declare function setGeminiAutoUpdateDisabled(settings: Record<string, unknown>): void;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic reader/writer for a `.gemini/…/settings.json`-shaped JSON config.
|
|
3
|
+
*
|
|
4
|
+
* Despite the name, this module is no longer gemini-specific: gemini itself
|
|
5
|
+
* is hard-deprecated and has no live caller left (its own settings writer,
|
|
6
|
+
* `generateGeminiConfig`, was removed with the RUSH-2202 runner.ts fix — a
|
|
7
|
+
* gemini routine is rejected before it ever reaches sandbox prep). Antigravity
|
|
8
|
+
* nests its own `settings.json` under the same `.gemini/antigravity-cli/` tree
|
|
9
|
+
* and shares this exact shape, so `permissions.ts` reuses `updateGeminiSettings`
|
|
10
|
+
* for antigravity's live permission writes. Keep the generic helpers; do not
|
|
11
|
+
* reintroduce gemini-only logic here.
|
|
12
|
+
*/
|
|
1
13
|
import * as fs from 'fs';
|
|
2
14
|
import * as path from 'path';
|
|
3
15
|
function isRecord(value) {
|
|
@@ -24,10 +36,3 @@ export function updateGeminiSettings(settingsPath, mutate) {
|
|
|
24
36
|
writeGeminiSettings(settingsPath, settings);
|
|
25
37
|
return settings;
|
|
26
38
|
}
|
|
27
|
-
export function setGeminiAutoUpdateDisabled(settings) {
|
|
28
|
-
const general = isRecord(settings.general) ? settings.general : {};
|
|
29
|
-
settings.general = {
|
|
30
|
-
...general,
|
|
31
|
-
enableAutoUpdate: false,
|
|
32
|
-
};
|
|
33
|
-
}
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -269,9 +269,15 @@ export declare function displayHomePath(dir: string): string;
|
|
|
269
269
|
* Pull changes in an existing repo.
|
|
270
270
|
* Refuses to pull if the working tree is dirty -- user must commit or discard changes first.
|
|
271
271
|
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
272
|
+
* Strategy (RUSH-2282):
|
|
273
|
+
* 1. Fetch, then compare HEAD to the resolved tracking ref.
|
|
274
|
+
* 2. Clean behind-only (HEAD is an ancestor of tracking) → `merge --ff-only`
|
|
275
|
+
* against the tracking ref. Never re-enter `git pull` after a bare fetch —
|
|
276
|
+
* multi-entry FETCH_HEAD (concurrent fetch, multi-branch remote) makes
|
|
277
|
+
* `git pull --rebase` die with "Cannot rebase onto multiple branches" even
|
|
278
|
+
* when the checkout is a pure fast-forward.
|
|
279
|
+
* 3. Genuinely diverged (local commits not on tracking) → `rebase` onto the
|
|
280
|
+
* tracking ref (same outcome as {@link syncRepoGit}, without a second pull).
|
|
275
281
|
*/
|
|
276
282
|
export declare function pullRepo(dir: string): Promise<{
|
|
277
283
|
success: boolean;
|
package/dist/lib/git.js
CHANGED
|
@@ -867,9 +867,15 @@ export function displayHomePath(dir) {
|
|
|
867
867
|
* Pull changes in an existing repo.
|
|
868
868
|
* Refuses to pull if the working tree is dirty -- user must commit or discard changes first.
|
|
869
869
|
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
870
|
+
* Strategy (RUSH-2282):
|
|
871
|
+
* 1. Fetch, then compare HEAD to the resolved tracking ref.
|
|
872
|
+
* 2. Clean behind-only (HEAD is an ancestor of tracking) → `merge --ff-only`
|
|
873
|
+
* against the tracking ref. Never re-enter `git pull` after a bare fetch —
|
|
874
|
+
* multi-entry FETCH_HEAD (concurrent fetch, multi-branch remote) makes
|
|
875
|
+
* `git pull --rebase` die with "Cannot rebase onto multiple branches" even
|
|
876
|
+
* when the checkout is a pure fast-forward.
|
|
877
|
+
* 3. Genuinely diverged (local commits not on tracking) → `rebase` onto the
|
|
878
|
+
* tracking ref (same outcome as {@link syncRepoGit}, without a second pull).
|
|
873
879
|
*/
|
|
874
880
|
export async function pullRepo(dir) {
|
|
875
881
|
try {
|
|
@@ -933,16 +939,16 @@ export async function pullRepo(dir) {
|
|
|
933
939
|
tracking = `origin/${branch}`;
|
|
934
940
|
}
|
|
935
941
|
}
|
|
936
|
-
// Split the remote-tracking ref (<remote>/<branch>)
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
// nothing" failure this change exists to remove. Branch names may contain
|
|
942
|
-
// slashes, so split on the FIRST separator only.
|
|
942
|
+
// Split the remote-tracking ref (<remote>/<branch>) so callers/logs can name
|
|
943
|
+
// the remote. Branch names may contain slashes, so split on the FIRST
|
|
944
|
+
// separator only. The integrate step below uses `tracking` directly (not a
|
|
945
|
+
// second `git pull <remote> <branch>`) so a multi-entry FETCH_HEAD cannot
|
|
946
|
+
// turn a clean fast-forward into "Cannot rebase onto multiple branches".
|
|
943
947
|
const sep = tracking.indexOf('/');
|
|
944
|
-
const remoteName = sep > 0 ? tracking.slice(0, sep) : 'origin';
|
|
945
948
|
const remoteBranch = sep > 0 ? tracking.slice(sep + 1) : branch;
|
|
949
|
+
// Keep branch-name validation on the ref we would have passed to pull —
|
|
950
|
+
// rejects traversal / flag-smuggling shapes before any integrate command.
|
|
951
|
+
assertValidBranchName(remoteBranch);
|
|
946
952
|
// Bare fetch: updates every remote, so the revparse below sees a fresh ref
|
|
947
953
|
// whichever one the branch tracks. Deliberately argument-less — simple-git's
|
|
948
954
|
// fetchTask only forwards a remote when BOTH remote and branch are passed,
|
|
@@ -962,17 +968,27 @@ export async function pullRepo(dir) {
|
|
|
962
968
|
branch,
|
|
963
969
|
};
|
|
964
970
|
}
|
|
971
|
+
// Behind-only when tracking has commits we lack AND we have none of our own
|
|
972
|
+
// on top. Use rev-list counts — simple-git does not reject on the exit-1
|
|
973
|
+
// that `merge-base --is-ancestor` returns for a non-ancestor, so a try/catch
|
|
974
|
+
// around that call would always report "can ff" (RUSH-2282).
|
|
975
|
+
const aheadCount = parseInt((await git.raw(['rev-list', '--count', `${tracking}..HEAD`])).trim(), 10);
|
|
976
|
+
const behindCount = parseInt((await git.raw(['rev-list', '--count', `HEAD..${tracking}`])).trim(), 10);
|
|
977
|
+
const canFastForward = Number.isFinite(aheadCount) &&
|
|
978
|
+
Number.isFinite(behindCount) &&
|
|
979
|
+
aheadCount === 0 &&
|
|
980
|
+
behindCount > 0;
|
|
965
981
|
try {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
982
|
+
if (canFastForward) {
|
|
983
|
+
// Integrate the already-fetched tracking ref. No network, no FETCH_HEAD.
|
|
984
|
+
await git.raw(['merge', '--ff-only', tracking]);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
// Diverged (or local-only commits). Rebase onto the tracking tip —
|
|
988
|
+
// same outcome as `git pull --rebase <remote> <branch>` without
|
|
989
|
+
// re-fetching or consulting FETCH_HEAD.
|
|
990
|
+
await git.raw(['rebase', tracking]);
|
|
991
|
+
}
|
|
976
992
|
}
|
|
977
993
|
catch (err) {
|
|
978
994
|
// Abort so the tree is restored, matching the atomicity --ff-only gave us.
|
|
@@ -982,10 +998,11 @@ export async function pullRepo(dir) {
|
|
|
982
998
|
// the cause. `agents sync` reaches this path unattended across the fleet,
|
|
983
999
|
// so a wedged checkout would be worse than the bug this fixes.
|
|
984
1000
|
await git.raw(['rebase', '--abort']).catch(() => { });
|
|
1001
|
+
const verb = canFastForward ? 'Fast-forward' : 'Rebase';
|
|
985
1002
|
return {
|
|
986
1003
|
success: false,
|
|
987
1004
|
commit: '',
|
|
988
|
-
error:
|
|
1005
|
+
error: `${verb} onto ${tracking} failed — the pull was rolled back, nothing changed.\n\nResolve the divergence, then pull again:\n\n cd ${displayHomePath(dir)} && git log --oneline HEAD...${tracking}\n\n${err.message}`,
|
|
989
1006
|
};
|
|
990
1007
|
}
|
|
991
1008
|
installGithooksSymlinks(dir);
|
|
@@ -10,8 +10,19 @@ export interface HookProfileRow {
|
|
|
10
10
|
cacheStalePct: number;
|
|
11
11
|
cacheMissPct: number;
|
|
12
12
|
errorCount: number;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Fraction (0-1) of fires with a real crash exit (exit 1 / other nonzero
|
|
15
|
+
* except intentional PreToolUse deny code 2). Exit 2 is blockRate.
|
|
16
|
+
*/
|
|
14
17
|
errorRate?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Count of intentional deny/block exits (PreToolUse exit 2). Deny-by-design
|
|
20
|
+
* guards (ask-user-question-guard, git-guard, plan-html-reminder) use this
|
|
21
|
+
* path — not a crash.
|
|
22
|
+
*/
|
|
23
|
+
blockCount: number;
|
|
24
|
+
/** Fraction (0-1) of fires with exit code 2 (intentional deny/block). */
|
|
25
|
+
blockRate?: number;
|
|
15
26
|
/** Fraction (0-1) of fires that hit their configured timeout. */
|
|
16
27
|
timeoutRate?: number;
|
|
17
28
|
/** Project key the row is scoped to (see project-key.ts) — set only when
|
|
@@ -72,7 +72,10 @@ export function aggregateHookProfile(events) {
|
|
|
72
72
|
const hits = evs.filter(e => e.cache === 'hit').length;
|
|
73
73
|
const stale = evs.filter(e => e.cache === 'stale-prefetch').length;
|
|
74
74
|
const misses = evs.filter(e => e.cache === 'miss').length;
|
|
75
|
-
|
|
75
|
+
// Exit classes (Claude/Codex PreToolUse convention): 0 allow, 2 deny/block,
|
|
76
|
+
// 1 / other nonzero = real error. Exit 2 is not a crash.
|
|
77
|
+
const blocks = evs.filter(e => e.exit === 2).length;
|
|
78
|
+
const errors = evs.filter(e => typeof e.exit === 'number' && e.exit !== 0 && e.exit !== 2).length;
|
|
76
79
|
rows.push({
|
|
77
80
|
hook,
|
|
78
81
|
n,
|
|
@@ -85,7 +88,9 @@ export function aggregateHookProfile(events) {
|
|
|
85
88
|
cacheStalePct: Math.round((stale / n) * 100),
|
|
86
89
|
cacheMissPct: Math.round((misses / n) * 100),
|
|
87
90
|
errorCount: errors,
|
|
91
|
+
blockCount: blocks,
|
|
88
92
|
...(errors > 0 ? { errorRate: Math.round((errors / n) * 1000) / 1000 } : {}),
|
|
93
|
+
...(blocks > 0 ? { blockRate: Math.round((blocks / n) * 1000) / 1000 } : {}),
|
|
89
94
|
// timeoutRate is not derivable here: the daily JSONL a shim writes only
|
|
90
95
|
// covers fires that reached their own trailing printf — an externally
|
|
91
96
|
// enforced timeout (the agent harness killing the process) never gets
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -217,12 +217,15 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
217
217
|
*
|
|
218
218
|
* A bare number stays seconds (`timeout: 30` → 30) for backward compatibility.
|
|
219
219
|
* A Go-style duration string is parsed into seconds: `5s`, `2m`, `1h30m`,
|
|
220
|
-
* `90s`, `1h`.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* `90s`, `1h`. Suffixed durations longer than 24 hours are rejected as likely
|
|
221
|
+
* typos, while bare seconds stay uncapped for backward compatibility. This
|
|
222
|
+
* intentionally does NOT reuse {@link parseTimeout} from routines.ts — that one
|
|
223
|
+
* returns milliseconds, has no seconds (`s`) unit, and floors at one minute,
|
|
224
|
+
* none of which fit hook timeouts (typically 5–600s).
|
|
223
225
|
*
|
|
224
|
-
* Returns the seconds value, or `null` when the input is not a positive number
|
|
225
|
-
* or a
|
|
226
|
+
* Returns the seconds value, or `null` when the input is not a positive number,
|
|
227
|
+
* is not parseable, or is a suffixed duration longer than 24 hours — the caller
|
|
228
|
+
* decides how to surface that.
|
|
226
229
|
*/
|
|
227
230
|
export declare function normalizeHookTimeoutSeconds(value: unknown): number | null;
|
|
228
231
|
/**
|
package/dist/lib/hooks.js
CHANGED
|
@@ -1160,17 +1160,21 @@ export function listCentralHooks() {
|
|
|
1160
1160
|
}
|
|
1161
1161
|
return results;
|
|
1162
1162
|
}
|
|
1163
|
+
const MAX_HOOK_DURATION_SECONDS = 24 * 60 * 60;
|
|
1163
1164
|
/**
|
|
1164
1165
|
* Normalize a hook `timeout` from agents.yaml into a whole number of seconds.
|
|
1165
1166
|
*
|
|
1166
1167
|
* A bare number stays seconds (`timeout: 30` → 30) for backward compatibility.
|
|
1167
1168
|
* A Go-style duration string is parsed into seconds: `5s`, `2m`, `1h30m`,
|
|
1168
|
-
* `90s`, `1h`.
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1169
|
+
* `90s`, `1h`. Suffixed durations longer than 24 hours are rejected as likely
|
|
1170
|
+
* typos, while bare seconds stay uncapped for backward compatibility. This
|
|
1171
|
+
* intentionally does NOT reuse {@link parseTimeout} from routines.ts — that one
|
|
1172
|
+
* returns milliseconds, has no seconds (`s`) unit, and floors at one minute,
|
|
1173
|
+
* none of which fit hook timeouts (typically 5–600s).
|
|
1171
1174
|
*
|
|
1172
|
-
* Returns the seconds value, or `null` when the input is not a positive number
|
|
1173
|
-
* or a
|
|
1175
|
+
* Returns the seconds value, or `null` when the input is not a positive number,
|
|
1176
|
+
* is not parseable, or is a suffixed duration longer than 24 hours — the caller
|
|
1177
|
+
* decides how to surface that.
|
|
1174
1178
|
*/
|
|
1175
1179
|
export function normalizeHookTimeoutSeconds(value) {
|
|
1176
1180
|
if (typeof value === 'number') {
|
|
@@ -1194,7 +1198,7 @@ export function normalizeHookTimeoutSeconds(value) {
|
|
|
1194
1198
|
const minutes = Number(m[4] || 0);
|
|
1195
1199
|
const seconds = Number(m[5] || 0);
|
|
1196
1200
|
const total = ((weeks * 7 + days) * 24 + hours) * 3600 + minutes * 60 + seconds;
|
|
1197
|
-
return total > 0 ? total : null;
|
|
1201
|
+
return total > 0 && total <= MAX_HOOK_DURATION_SECONDS ? total : null;
|
|
1198
1202
|
}
|
|
1199
1203
|
return null;
|
|
1200
1204
|
}
|
|
@@ -1606,6 +1610,7 @@ function registerHooksForOpenCode(versionHome, manifest, resolveScript) {
|
|
|
1606
1610
|
const pluginSource = `// Generated by agents-cli. Re-run agents sync to update.
|
|
1607
1611
|
import fs from "node:fs"
|
|
1608
1612
|
import os from "node:os"
|
|
1613
|
+
import path from "node:path"
|
|
1609
1614
|
|
|
1610
1615
|
const directHooks = ${serializedDirect}
|
|
1611
1616
|
const lifecycleHooks = ${serializedLifecycle}
|
|
@@ -1618,7 +1623,9 @@ function recordTimeoutSample(hook, payload) {
|
|
|
1618
1623
|
// printf, so a timed-out fire would otherwise leave ZERO trace in the
|
|
1619
1624
|
// warehouse. Write the sample ourselves from the side that knows it timed out.
|
|
1620
1625
|
try {
|
|
1621
|
-
|
|
1626
|
+
// path.dirname — not lastIndexOf('/') — so Windows backslash spool paths
|
|
1627
|
+
// still resolve to the parent dir (see #1869).
|
|
1628
|
+
fs.mkdirSync(path.dirname(PERF_SPOOL), { recursive: true })
|
|
1622
1629
|
const line = JSON.stringify({
|
|
1623
1630
|
ts_ms: Date.now(),
|
|
1624
1631
|
kind: "hook.fire",
|
|
@@ -74,6 +74,15 @@ export declare function remoteRunShellPrelude(agent: string): string;
|
|
|
74
74
|
* reliable whole-tree operation for both normal stops and rollback cleanup.
|
|
75
75
|
*/
|
|
76
76
|
export declare function buildDetachedLaunchCommand(inner: string): string;
|
|
77
|
+
/** Build the detached PowerShell launch protocol used by Windows SSH hosts. */
|
|
78
|
+
export declare function buildWindowsDetachedLaunchCommand(opts: {
|
|
79
|
+
forwardedArgs: string[];
|
|
80
|
+
remoteCwd?: string;
|
|
81
|
+
mirrorCwd?: boolean;
|
|
82
|
+
remoteLog: string;
|
|
83
|
+
remoteExit: string;
|
|
84
|
+
env: Record<string, string>;
|
|
85
|
+
}): string;
|
|
77
86
|
export interface DispatchResult {
|
|
78
87
|
task: HostTask;
|
|
79
88
|
/** Exit code when followed; undefined when detached (--no-follow). */
|
|
@@ -92,6 +101,7 @@ export declare function terminateDispatchedTask(task: HostTask): void;
|
|
|
92
101
|
* Exit 1 if the group is still alive after TERM/KILL (can't stop it).
|
|
93
102
|
*/
|
|
94
103
|
export declare function buildStopRemoteCommand(pid: number, remoteExit: string): string;
|
|
104
|
+
export declare function buildWindowsStopRemoteCommand(pid: number, remoteExit: string): string;
|
|
95
105
|
/**
|
|
96
106
|
* Stop a running host task from the origin machine (`agents hosts stop <id>`).
|
|
97
107
|
*
|
|
@@ -10,9 +10,9 @@
|
|
|
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
|
-
import { remoteShellFor, posixEnvExports } from './remote-cmd.js';
|
|
15
|
+
import { encodePowershell, powershellQuote, remoteShellFor, posixEnvExports } from './remote-cmd.js';
|
|
16
16
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
17
17
|
import { resolveActor, actorEnv } from '../actor.js';
|
|
18
18
|
import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
@@ -148,15 +148,53 @@ export function buildDetachedLaunchCommand(inner) {
|
|
|
148
148
|
].join(' ');
|
|
149
149
|
return `bash -lc ${shellQuote(`node -e ${shellQuote(nodeScript)}`)}`;
|
|
150
150
|
}
|
|
151
|
+
function windowsRemotePath(path) {
|
|
152
|
+
return path.startsWith('$HOME/')
|
|
153
|
+
? `(Join-Path $HOME ${powershellQuote(path.slice('$HOME/'.length))})`
|
|
154
|
+
: powershellQuote(path);
|
|
155
|
+
}
|
|
156
|
+
/** Build the detached PowerShell launch protocol used by Windows SSH hosts. */
|
|
157
|
+
export function buildWindowsDetachedLaunchCommand(opts) {
|
|
158
|
+
const log = windowsRemotePath(opts.remoteLog);
|
|
159
|
+
const exit = windowsRemotePath(opts.remoteExit);
|
|
160
|
+
const env = Object.entries(opts.env).map(([key, value]) => `$env:${key} = ${powershellQuote(value)}`);
|
|
161
|
+
const cwd = opts.remoteCwd
|
|
162
|
+
? opts.mirrorCwd
|
|
163
|
+
? `try { Set-Location -LiteralPath ${powershellQuote(opts.remoteCwd)} } catch { Set-Location -LiteralPath $HOME }`
|
|
164
|
+
: `Set-Location -LiteralPath ${powershellQuote(opts.remoteCwd)}`
|
|
165
|
+
: '';
|
|
166
|
+
const inner = [
|
|
167
|
+
`$ProgressPreference = 'SilentlyContinue'`,
|
|
168
|
+
`$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'`,
|
|
169
|
+
...env,
|
|
170
|
+
cwd,
|
|
171
|
+
`$log = ${log}`,
|
|
172
|
+
`$exit = ${exit}`,
|
|
173
|
+
`$code = 1`,
|
|
174
|
+
`try { & ${['agents', ...opts.forwardedArgs].map(powershellQuote).join(' ')} *> $log; if ($null -ne $LASTEXITCODE) { $code = $LASTEXITCODE } } catch { $_ | Out-File -LiteralPath $log -Append; $code = 1 } finally { Set-Content -LiteralPath $exit -Value $code -NoNewline -Encoding ascii }`,
|
|
175
|
+
].filter(Boolean).join('; ');
|
|
176
|
+
const encodedInner = encodePowershell(inner);
|
|
177
|
+
const outer = [
|
|
178
|
+
`$dir = Join-Path $HOME '.agents/.cache/hosts'`,
|
|
179
|
+
`New-Item -ItemType Directory -Force -Path $dir | Out-Null`,
|
|
180
|
+
`Remove-Item -LiteralPath ${exit} -Force -ErrorAction SilentlyContinue`,
|
|
181
|
+
`$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ${powershellQuote(`powershell.exe -NoProfile -EncodedCommand ${encodedInner}`)} }`,
|
|
182
|
+
`if ($result.ReturnValue -ne 0) { throw \"Win32_Process.Create failed with code $($result.ReturnValue)\" }`,
|
|
183
|
+
`Write-Output $result.ProcessId`,
|
|
184
|
+
].join('; ');
|
|
185
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell(outer)}`;
|
|
186
|
+
}
|
|
151
187
|
function terminateRemoteLaunch(task) {
|
|
152
188
|
if (!task.pid)
|
|
153
189
|
throw new Error(`Cannot terminate remote task ${task.id}: launch returned no PID.`);
|
|
154
190
|
const pid = task.pid;
|
|
155
|
-
const command =
|
|
156
|
-
`
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
191
|
+
const command = task.remoteShell === 'powershell'
|
|
192
|
+
? `powershell -NoProfile -EncodedCommand ${encodePowershell(`Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue; Remove-Item -LiteralPath ${windowsRemotePath(task.remoteLog)}, ${windowsRemotePath(task.remoteExit)} -Force -ErrorAction SilentlyContinue`)}`
|
|
193
|
+
: `if kill -TERM -- -${pid} 2>/dev/null; then ` +
|
|
194
|
+
`sleep 1; kill -KILL -- -${pid} 2>/dev/null || true; ` +
|
|
195
|
+
`elif kill -0 -- -${pid} 2>/dev/null; then exit 1; fi; ` +
|
|
196
|
+
`rm -f ${task.remoteLog} ${task.remoteExit}`;
|
|
197
|
+
const result = sshExec(task.target, command, { timeoutMs: 10000, multiplex: true, extraSshArgs: task.identityFile ? ['-i', task.identityFile, '-o', 'IdentitiesOnly=yes'] : undefined });
|
|
160
198
|
if (result.code !== 0) {
|
|
161
199
|
throw new Error(`Failed to terminate remote task ${task.id} on ${task.host}: ` +
|
|
162
200
|
`${(result.stderr || result.stdout).trim() || 'ssh error'}`);
|
|
@@ -194,6 +232,17 @@ export function buildStopRemoteCommand(pid, remoteExit) {
|
|
|
194
232
|
`else echo 143 > ${remoteExit}; echo GONE; fi; ` +
|
|
195
233
|
`fi`);
|
|
196
234
|
}
|
|
235
|
+
export function buildWindowsStopRemoteCommand(pid, remoteExit) {
|
|
236
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
237
|
+
throw new Error(`Invalid remote task pid: ${pid}`);
|
|
238
|
+
const exit = windowsRemotePath(remoteExit);
|
|
239
|
+
const script = `$process = Get-Process -Id ${pid} -ErrorAction SilentlyContinue; ` +
|
|
240
|
+
`function Get-DescendantProcessIds([int]$ParentId) { $children = Get-CimInstance Win32_Process -Filter \"ParentProcessId = $ParentId\"; foreach ($child in $children) { Get-DescendantProcessIds $child.ProcessId; $child.ProcessId } }; ` +
|
|
241
|
+
`if ($process) { $descendants = @(Get-DescendantProcessIds ${pid}); foreach ($childId in $descendants) { Stop-Process -Id $childId -Force -ErrorAction SilentlyContinue }; Stop-Process -Id ${pid} -Force; Set-Content -LiteralPath ${exit} -Value 143 -NoNewline -Encoding ascii; Write-Output 'SIGNALED' } ` +
|
|
242
|
+
`elseif (Test-Path -LiteralPath ${exit}) { $code = (Get-Content -LiteralPath ${exit} -Raw).Trim(); if ($code) { Write-Output (\"ALREADY $code\") } else { Set-Content -LiteralPath ${exit} -Value 143 -NoNewline -Encoding ascii; Write-Output 'GONE' } } ` +
|
|
243
|
+
`else { Set-Content -LiteralPath ${exit} -Value 143 -NoNewline -Encoding ascii; Write-Output 'GONE' }`;
|
|
244
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
245
|
+
}
|
|
197
246
|
/**
|
|
198
247
|
* Stop a running host task from the origin machine (`agents hosts stop <id>`).
|
|
199
248
|
*
|
|
@@ -209,8 +258,10 @@ export function stopDispatchedTask(task) {
|
|
|
209
258
|
if (!task.pid) {
|
|
210
259
|
throw new Error(`Cannot stop remote task ${task.id}: launch returned no PID.`);
|
|
211
260
|
}
|
|
212
|
-
const command =
|
|
213
|
-
|
|
261
|
+
const command = task.remoteShell === 'powershell'
|
|
262
|
+
? buildWindowsStopRemoteCommand(task.pid, task.remoteExit)
|
|
263
|
+
: buildStopRemoteCommand(task.pid, task.remoteExit);
|
|
264
|
+
const result = sshExec(task.target, command, { timeoutMs: 10000, multiplex: true, extraSshArgs: task.identityFile ? ['-i', task.identityFile, '-o', 'IdentitiesOnly=yes'] : undefined });
|
|
214
265
|
if (result.code !== 0) {
|
|
215
266
|
throw new Error(`Failed to stop remote task ${task.id} on ${task.host}: ` +
|
|
216
267
|
`${(result.stderr || result.stdout).trim() || 'ssh error'}`);
|
|
@@ -230,21 +281,11 @@ export function stopDispatchedTask(task) {
|
|
|
230
281
|
* and `dispatchAgentsCommand` (teams) build their `forwardedArgs` and call here,
|
|
231
282
|
* so the nohup/exit-file/offset-tail machinery lives in exactly one place.
|
|
232
283
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* tails the log with POSIX `tail -c`/`printf`/`cat`/`stat`, which do not exist
|
|
236
|
-
* in cmd.exe/PowerShell. Shipping the launch alone would leave `run --host
|
|
237
|
-
* <windows>` dispatching but hanging on follow forever, so we fail fast with an
|
|
238
|
-
* actionable message instead. Read-only `--host` commands (view/sessions/…) run
|
|
239
|
-
* a single round-trip with no follow protocol and DO work against Windows.
|
|
284
|
+
* POSIX hosts use a detached bash process group; Windows hosts use a hidden
|
|
285
|
+
* detached PowerShell process and the same durable log/exit-file protocol.
|
|
240
286
|
*/
|
|
241
287
|
async function launchDetached(host, target, opts) {
|
|
242
|
-
|
|
243
|
-
throw new Error(`Detached dispatch to Windows host "${host.name}" is not supported yet — the run ` +
|
|
244
|
-
`follow/reconcile layer is POSIX-only (offset-tails the remote log with tail/cat/stat). ` +
|
|
245
|
-
`Read-only --host commands (view, sessions, usage, cost, doctor, list, teams) do work ` +
|
|
246
|
-
`against Windows.`);
|
|
247
|
-
}
|
|
288
|
+
const remoteShell = remoteShellFor(host.os ?? resolveRemoteOsSync(host.name));
|
|
248
289
|
const id = randomUUID().slice(0, 8);
|
|
249
290
|
const remoteLog = `${REMOTE_DIR}/${id}.log`;
|
|
250
291
|
const remoteExit = `${REMOTE_DIR}/${id}.exit`;
|
|
@@ -266,11 +307,21 @@ async function launchDetached(host, target, opts) {
|
|
|
266
307
|
const credHostKeyOpts = opts.copyCreds ? hostKeyCheckingOpts(true) : undefined;
|
|
267
308
|
// Outer: ensure dir, launch the login-shell wrapper as a new process-group
|
|
268
309
|
// leader, and print that leader PID.
|
|
269
|
-
const launch =
|
|
310
|
+
const launch = remoteShell === 'powershell'
|
|
311
|
+
? buildWindowsDetachedLaunchCommand({
|
|
312
|
+
forwardedArgs: opts.forwardedArgs,
|
|
313
|
+
remoteCwd: opts.remoteCwd,
|
|
314
|
+
mirrorCwd: opts.mirrorCwd,
|
|
315
|
+
remoteLog,
|
|
316
|
+
remoteExit,
|
|
317
|
+
env: withActorEnv(opts.agentLabel === RUN_AUTO_KEYWORD ? { [RUN_AUTO_HOST_RESOLVED_ENV]: '1' } : {}),
|
|
318
|
+
})
|
|
319
|
+
: `mkdir -p ${REMOTE_DIR}; ${buildDetachedLaunchCommand(inner)}`;
|
|
270
320
|
const res = sshExec(target, launch, {
|
|
271
321
|
timeoutMs: 30000,
|
|
272
322
|
multiplex: !opts.copyCreds,
|
|
273
323
|
hostKeyOpts: credHostKeyOpts,
|
|
324
|
+
extraSshArgs: hostIdentityArgs(host),
|
|
274
325
|
});
|
|
275
326
|
if (res.code !== 0) {
|
|
276
327
|
throw new Error(`Failed to launch on "${host.name}": ${(res.stderr || res.stdout).trim() || 'ssh error'}`);
|
|
@@ -280,6 +331,8 @@ async function launchDetached(host, target, opts) {
|
|
|
280
331
|
id,
|
|
281
332
|
host: host.name,
|
|
282
333
|
target,
|
|
334
|
+
identityFile: host.identityFile,
|
|
335
|
+
remoteShell,
|
|
283
336
|
agent: opts.agentLabel,
|
|
284
337
|
prompt: opts.promptLabel,
|
|
285
338
|
pid: Number.isFinite(pid) ? pid : undefined,
|
|
@@ -311,6 +364,8 @@ async function launchDetached(host, target, opts) {
|
|
|
311
364
|
taskId: id,
|
|
312
365
|
echo: true,
|
|
313
366
|
timeoutMs: opts.timeoutMs,
|
|
367
|
+
extraSshArgs: hostIdentityArgs(host),
|
|
368
|
+
remoteShell,
|
|
314
369
|
});
|
|
315
370
|
// -1 = the follow window closed while the run continues on the host. Leave the
|
|
316
371
|
// record 'running' (do NOT freeze it terminal) so a later `hosts ps`/`logs`
|
|
@@ -443,7 +498,9 @@ export function buildInteractiveRunForwardedArgs(opts) {
|
|
|
443
498
|
*/
|
|
444
499
|
export async function runInteractiveOnHost(host, opts) {
|
|
445
500
|
const target = sshTargetFor(host);
|
|
446
|
-
|
|
501
|
+
// Concrete version pins fail loud here (RUSH-2313) so we never open a TTY
|
|
502
|
+
// to a box that cannot run the pin.
|
|
503
|
+
const { warnings } = ensureHostReady(host, { agent: opts.agent, version: opts.version });
|
|
447
504
|
for (const w of warnings)
|
|
448
505
|
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
449
506
|
const invocation = ['agents', ...buildInteractiveRunForwardedArgs(opts)].map(shellQuote).join(' ');
|
|
@@ -464,12 +521,15 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
464
521
|
tty: process.stdin.isTTY,
|
|
465
522
|
multiplex: !opts.copyCreds,
|
|
466
523
|
hostKeyOpts: credHostKeyOpts,
|
|
524
|
+
extraSshArgs: hostIdentityArgs(host),
|
|
467
525
|
});
|
|
468
526
|
}
|
|
469
527
|
/** Dispatch an `agents run <agent> "<prompt>"` onto a host (the `run --host` path). */
|
|
470
528
|
export async function dispatchToHost(host, opts) {
|
|
471
529
|
const target = sshTargetFor(host);
|
|
472
|
-
|
|
530
|
+
// Concrete agent@version pins fail loud before we print "Dispatched" and
|
|
531
|
+
// leave a dead remote log (RUSH-2313). Aliases stay remote-resolved.
|
|
532
|
+
const { warnings } = ensureHostReady(host, { agent: opts.agent, version: opts.version });
|
|
473
533
|
for (const w of warnings)
|
|
474
534
|
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
475
535
|
return launchDetached(host, target, {
|
package/dist/lib/hosts/logs.js
CHANGED
|
@@ -16,6 +16,7 @@ import { loadTask, localLogPath, updateTask, terminalPatch } from './tasks.js';
|
|
|
16
16
|
import { followHostTask } from './progress.js';
|
|
17
17
|
import { reconcileTask } from './reconcile.js';
|
|
18
18
|
import { sshExecRaw } from '../ssh-exec.js';
|
|
19
|
+
import { encodePowershell } from './remote-cmd.js';
|
|
19
20
|
/** Lines of raw combined-stdout to show in the concise (non-`full`) view. */
|
|
20
21
|
const HOST_LOG_TAIL_LINES = 40;
|
|
21
22
|
/**
|
|
@@ -32,6 +33,8 @@ export async function showHostTaskLog(id, follow, full = false) {
|
|
|
32
33
|
remoteExit: task.remoteExit,
|
|
33
34
|
taskId: id,
|
|
34
35
|
echo: true,
|
|
36
|
+
remoteShell: task.remoteShell,
|
|
37
|
+
extraSshArgs: task.identityFile ? ['-i', task.identityFile, '-o', 'IdentitiesOnly=yes'] : [],
|
|
35
38
|
});
|
|
36
39
|
// -1 = follow window closed; the run continues on the host (not a failure,
|
|
37
40
|
// so exit 0). Any real code is the finished run — persist the terminal
|
|
@@ -106,7 +109,14 @@ export function tailLines(text, n) {
|
|
|
106
109
|
* and progress.ts.
|
|
107
110
|
*/
|
|
108
111
|
function fetchAndCacheRemoteLog(task) {
|
|
109
|
-
const
|
|
112
|
+
const command = task.remoteShell === 'powershell'
|
|
113
|
+
? `powershell -NoProfile -EncodedCommand ${encodePowershell(`$path = Join-Path $HOME '${task.remoteLog.replace(/^\$HOME\//, '').replace(/'/g, "''")}'; if (Test-Path -LiteralPath $path) { $bytes = [IO.File]::ReadAllBytes($path); [Console]::OpenStandardOutput().Write($bytes, 0, $bytes.Length) }`)}`
|
|
114
|
+
: `cat ${task.remoteLog} 2>/dev/null`;
|
|
115
|
+
const res = sshExecRaw(task.target, command, {
|
|
116
|
+
timeoutMs: 30000,
|
|
117
|
+
multiplex: true,
|
|
118
|
+
extraSshArgs: task.identityFile ? ['-i', task.identityFile, '-o', 'IdentitiesOnly=yes'] : undefined,
|
|
119
|
+
});
|
|
110
120
|
if (res.code !== 0 || res.stdout.length === 0)
|
|
111
121
|
return null;
|
|
112
122
|
// The hosts cache dir already exists (saveTask created it) — write is best-effort.
|