@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-machine fan-out for the default `agents sessions` listing.
|
|
3
|
+
*
|
|
4
|
+
* `discoverSessions()` only scans the local disk. To browse the whole fleet in
|
|
5
|
+
* one list — without syncing anything — we run `agents sessions <same query>
|
|
6
|
+
* --json` on each peer over SSH and merge the parsed `SessionMeta[]`, tagging
|
|
7
|
+
* every row with the machine it came from so the picker/table can label and
|
|
8
|
+
* group by computer.
|
|
9
|
+
*
|
|
10
|
+
* This is the browse-listing sibling of `remote-active.ts` (which fans out
|
|
11
|
+
* `--active`): same transport, same device set, same recursion guard. The peer
|
|
12
|
+
* runs with `AGENTS_SESSIONS_LOCAL=1` so it answers only for itself and the
|
|
13
|
+
* sweep never recurses. A dead or slow host is skipped with a stderr note,
|
|
14
|
+
* never fatal — one asleep laptop must not blank the list.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
19
|
+
import { sshTargetFor } from '../devices/connect.js';
|
|
20
|
+
import { loadDevices } from '../devices/registry.js';
|
|
21
|
+
import { remoteShellFor, buildWindowsAgentsCommand } from '../hosts/remote-cmd.js';
|
|
22
|
+
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
23
|
+
import { machineId, normalizeHost } from './sync/config.js';
|
|
24
|
+
import { NO_FANOUT_ENV } from './remote-active.js';
|
|
25
|
+
import { terminalWidth } from './width.js';
|
|
26
|
+
/** Per-host SSH budget. Slightly above SSH_OPTS' ConnectTimeout=10 so a
|
|
27
|
+
* reachable-but-slow remote still answers before we give up. */
|
|
28
|
+
const REMOTE_TIMEOUT_MS = 12_000;
|
|
29
|
+
/**
|
|
30
|
+
* The command run on each peer: answer for itself, as JSON, without recursing.
|
|
31
|
+
* `forwardedArgs` carry the caller's own query/filters (already including the
|
|
32
|
+
* leading `sessions` and a `--json`) so each peer returns a comparable slice.
|
|
33
|
+
* A Windows peer gets a PowerShell invocation (ssh lands in cmd.exe/PowerShell
|
|
34
|
+
* there, where `bash -lc` is not a command); every other OS keeps `bash -lc`.
|
|
35
|
+
*/
|
|
36
|
+
export function remoteListCommand(forwardedArgs, os) {
|
|
37
|
+
if (remoteShellFor(os) === 'powershell') {
|
|
38
|
+
return buildWindowsAgentsCommand({
|
|
39
|
+
args: forwardedArgs,
|
|
40
|
+
env: { [NO_FANOUT_ENV]: '1' },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const inner = [`${NO_FANOUT_ENV}=1`, 'agents', ...forwardedArgs].map((t, i) => i === 0 ? t : shellQuote(t)).join(' ');
|
|
44
|
+
return `bash -lc ${shellQuote(inner)}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a peer's `sessions --json` stdout into `SessionMeta[]`, tagging each
|
|
48
|
+
* with `machine`. Defensive against version skew / partial output: non-JSON or
|
|
49
|
+
* a non-array yields `[]`, and non-object entries are dropped rather than
|
|
50
|
+
* throwing. The `machine` we dialed always wins over any value the peer set on
|
|
51
|
+
* its own rows, so grouping keys off the computer we asked. Exported for unit
|
|
52
|
+
* testing without a live tailnet.
|
|
53
|
+
*/
|
|
54
|
+
export function parseRemoteList(stdout, machine) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(stdout);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(parsed))
|
|
63
|
+
return [];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const x of parsed) {
|
|
66
|
+
if (x && typeof x === 'object' && !Array.isArray(x)) {
|
|
67
|
+
// `_remote` marks these as living on the peer's disk (not a local mirror),
|
|
68
|
+
// so the picker routes read/resume back over SSH instead of the local FS.
|
|
69
|
+
out.push({ ...x, machine, _remote: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** Run one remote `agents sessions … --json` and capture stdout. Resolves
|
|
75
|
+
* `{ code: null }` on spawn error or timeout (host treated as dead). */
|
|
76
|
+
function sshCapture(target, remoteCmd, timeoutMs) {
|
|
77
|
+
assertValidSshTarget(target);
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
|
|
80
|
+
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
81
|
+
let stdout = '';
|
|
82
|
+
let settled = false;
|
|
83
|
+
const done = (code) => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
settled = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
resolve({ code, stdout });
|
|
89
|
+
};
|
|
90
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); done(null); }, timeoutMs);
|
|
91
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
92
|
+
child.on('error', () => done(null));
|
|
93
|
+
child.on('close', (code) => done(code));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function fetchByTarget(target, machine, display, forwardedArgs, os) {
|
|
97
|
+
const { code, stdout } = await sshCapture(target, remoteListCommand(forwardedArgs, os), REMOTE_TIMEOUT_MS);
|
|
98
|
+
if (code !== 0) {
|
|
99
|
+
process.stderr.write(chalk.gray(` ${display}: unreachable or no agents CLI — skipped\n`));
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return parseRemoteList(stdout, machine);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Gather listing sessions from other machines. With an explicit `hosts` list
|
|
106
|
+
* (from `--host`), fan out to exactly those. Otherwise sweep the registered,
|
|
107
|
+
* online devices from `ag devices`, excluding this machine and any without an
|
|
108
|
+
* address. `forwardedArgs` are the caller's own sessions args (query + filters,
|
|
109
|
+
* already `--json`) so every peer returns the same slice this machine asked for.
|
|
110
|
+
*/
|
|
111
|
+
export async function gatherRemoteList(forwardedArgs, hosts) {
|
|
112
|
+
const self = machineId();
|
|
113
|
+
const targets = [];
|
|
114
|
+
if (hosts && hosts.length > 0) {
|
|
115
|
+
for (const h of hosts) {
|
|
116
|
+
try {
|
|
117
|
+
assertValidSshTarget(h);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
process.stderr.write(chalk.gray(` ${h}: not a valid ssh target — skipped\n`));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const bareHost = h.split('@').pop() || h;
|
|
124
|
+
targets.push({ target: h, machine: normalizeHost(bareHost), name: h, os: resolveRemoteOsSync(h) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
let reg;
|
|
129
|
+
try {
|
|
130
|
+
reg = await loadDevices();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return { sessions: [], deviceCount: 0 };
|
|
134
|
+
}
|
|
135
|
+
for (const d of Object.values(reg)) {
|
|
136
|
+
if (d.tailscale?.online !== true)
|
|
137
|
+
continue;
|
|
138
|
+
if (normalizeHost(d.name) === self)
|
|
139
|
+
continue;
|
|
140
|
+
// Only machines that can actually run the CLI. iOS/tablet nodes register as
|
|
141
|
+
// `unknown` platform and can never answer, so skip them rather than burn a
|
|
142
|
+
// full ConnectTimeout on each.
|
|
143
|
+
if (d.platform !== 'windows' && d.platform !== 'linux' && d.platform !== 'macos')
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
targets.push({ target: sshTargetFor(d), machine: normalizeHost(d.name), name: d.name, os: d.platform });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// No address on the profile — nothing to dial; skip silently.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const results = await Promise.all(targets.map((t) => fetchByTarget(t.target, t.machine, t.name, forwardedArgs, t.os)));
|
|
154
|
+
return { sessions: results.flat(), deviceCount: targets.length };
|
|
155
|
+
}
|
|
156
|
+
/** Resolve a peer's SSH target (and OS) from the device registry by its
|
|
157
|
+
* normalized machine id — the same id the fan-out tags rows with. Returns
|
|
158
|
+
* undefined when no registered device with an address matches. */
|
|
159
|
+
export async function resolvePeerTarget(machine) {
|
|
160
|
+
let reg;
|
|
161
|
+
try {
|
|
162
|
+
reg = await loadDevices();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
for (const d of Object.values(reg)) {
|
|
168
|
+
if (normalizeHost(d.name) !== machine)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
return { target: sshTargetFor(d), os: d.platform };
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined; // matched the machine, but it has no address to dial
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Run `agents <args>` ON a peer over SSH, attached to this terminal (inherited
|
|
181
|
+
* stdio). `args` is the full arg vector after the binary — callers pass e.g.
|
|
182
|
+
* `['sessions', id, '--markdown']` or `['sessions', 'resume', id]`. Used when a
|
|
183
|
+
* picked session lives on another machine: its transcript and agent binary are
|
|
184
|
+
* there, so both reading (no TTY) and resuming (TTY) must execute on the peer —
|
|
185
|
+
* not via a local `--host` hop, which would discover locally and dead-end for a
|
|
186
|
+
* session that exists only on the peer. Resolves 'no-target' when the machine
|
|
187
|
+
* isn't a dialable registered device; the caller surfaces a clear message.
|
|
188
|
+
*/
|
|
189
|
+
export async function runOnPeer(args, machine, opts = {}) {
|
|
190
|
+
const peer = await resolvePeerTarget(machine);
|
|
191
|
+
if (!peer)
|
|
192
|
+
return 'no-target';
|
|
193
|
+
assertValidSshTarget(peer.target); // registry-sourced, but validate like the fan-out does
|
|
194
|
+
const cols = terminalWidth();
|
|
195
|
+
const remoteCmd = remoteShellFor(peer.os) === 'powershell'
|
|
196
|
+
? buildWindowsAgentsCommand({ args, env: cols > 0 ? { COLUMNS: String(cols) } : undefined })
|
|
197
|
+
: `bash -lc ${shellQuote((cols > 0 ? [`COLUMNS=${cols}`] : []).concat(['agents', ...args].map(shellQuote)).join(' '))}`;
|
|
198
|
+
const sshArgs = [...SSH_OPTS, ...controlOpts()];
|
|
199
|
+
if (opts.tty)
|
|
200
|
+
sshArgs.push('-tt'); // force a PTY so the resumed agent is interactive
|
|
201
|
+
sshArgs.push(peer.target, remoteCmd);
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const child = spawn('ssh', sshArgs, { stdio: 'inherit' });
|
|
204
|
+
// ssh prints its own connection errors to the inherited stderr; a spawn
|
|
205
|
+
// failure (e.g. ssh not on PATH) has no such output, so name it. Either way
|
|
206
|
+
// we resolve once it settles so the picker flow completes.
|
|
207
|
+
child.on('error', (err) => {
|
|
208
|
+
process.stderr.write(chalk.red(`Failed to reach ${machine}: ${err?.message ?? 'ssh failed to launch'}\n`));
|
|
209
|
+
resolve('ok');
|
|
210
|
+
});
|
|
211
|
+
child.on('close', () => resolve('ok'));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -22,8 +22,14 @@ export declare function buildForwardedArgs(argv: string[], hosts?: Set<string>):
|
|
|
22
22
|
* Build the single remote command string for `ssh <host> <cmd>`. Forwarded args
|
|
23
23
|
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
24
24
|
* quoted again so it survives `bash -lc <...>`.
|
|
25
|
+
*
|
|
26
|
+
* `os` selects the remote shell: a Windows host gets a PowerShell invocation
|
|
27
|
+
* (ssh lands in cmd.exe/PowerShell there, where `bash -lc` does not exist);
|
|
28
|
+
* anything else — including unknown/absent — keeps the POSIX form unchanged.
|
|
29
|
+
* The forwarded terminal width rides across as an env var either way so the
|
|
30
|
+
* remote renders its table to the local screen.
|
|
25
31
|
*/
|
|
26
|
-
export declare function buildRemoteCommand(forwardedArgs: string[], columns?: number): string;
|
|
32
|
+
export declare function buildRemoteCommand(forwardedArgs: string[], columns?: number, os?: string): string;
|
|
27
33
|
/** The four outcomes of one `ssh <host> agents sessions …` invocation. */
|
|
28
34
|
export type SshOutcome = 'ok' | 'unreachable' | 'query-failed' | 'spawn-error';
|
|
29
35
|
/**
|
|
@@ -28,6 +28,8 @@ import { createHash } from 'crypto';
|
|
|
28
28
|
import chalk from 'chalk';
|
|
29
29
|
import { getCacheDir } from '../state.js';
|
|
30
30
|
import { SSH_OPTS, controlOpts } from '../ssh-exec.js';
|
|
31
|
+
import { remoteShellFor, buildWindowsAgentsCommand } from '../hosts/remote-cmd.js';
|
|
32
|
+
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
31
33
|
import { formatRelativeTime } from './relative-time.js';
|
|
32
34
|
import { terminalWidth } from './width.js';
|
|
33
35
|
/**
|
|
@@ -88,8 +90,18 @@ export function buildForwardedArgs(argv, hosts = new Set()) {
|
|
|
88
90
|
* Build the single remote command string for `ssh <host> <cmd>`. Forwarded args
|
|
89
91
|
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
90
92
|
* quoted again so it survives `bash -lc <...>`.
|
|
93
|
+
*
|
|
94
|
+
* `os` selects the remote shell: a Windows host gets a PowerShell invocation
|
|
95
|
+
* (ssh lands in cmd.exe/PowerShell there, where `bash -lc` does not exist);
|
|
96
|
+
* anything else — including unknown/absent — keeps the POSIX form unchanged.
|
|
97
|
+
* The forwarded terminal width rides across as an env var either way so the
|
|
98
|
+
* remote renders its table to the local screen.
|
|
91
99
|
*/
|
|
92
|
-
export function buildRemoteCommand(forwardedArgs, columns) {
|
|
100
|
+
export function buildRemoteCommand(forwardedArgs, columns, os) {
|
|
101
|
+
if (remoteShellFor(os) === 'powershell') {
|
|
102
|
+
const env = columns && columns > 0 ? { COLUMNS: String(columns) } : undefined;
|
|
103
|
+
return buildWindowsAgentsCommand({ args: forwardedArgs, env });
|
|
104
|
+
}
|
|
93
105
|
const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
94
106
|
// Forward the caller's terminal width so the remote renders the table to the
|
|
95
107
|
// local screen (over SSH the remote's own COLUMNS is unset/wrong). `VAR=val
|
|
@@ -176,12 +188,14 @@ export function runRemoteSessions(hosts, argv = process.argv) {
|
|
|
176
188
|
for (const host of hosts)
|
|
177
189
|
assertValidSshTarget(host); // fail fast on any bad target
|
|
178
190
|
const forwarded = buildForwardedArgs(argv, new Set(hosts));
|
|
179
|
-
const
|
|
191
|
+
const cols = terminalWidth();
|
|
180
192
|
const multi = hosts.length > 1;
|
|
181
193
|
let failures = 0;
|
|
182
194
|
for (const host of hosts) {
|
|
183
195
|
if (multi)
|
|
184
196
|
process.stdout.write(chalk.cyan(`\n── ${host} ──\n`));
|
|
197
|
+
// Per-host: a Windows peer needs a PowerShell command, POSIX peers `bash -lc`.
|
|
198
|
+
const remoteCmd = buildRemoteCommand(forwarded, cols, resolveRemoteOsSync(host));
|
|
185
199
|
const res = spawnSync('ssh', [...SSH_OPTS, ...controlOpts(), host, remoteCmd], {
|
|
186
200
|
encoding: 'utf8',
|
|
187
201
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -33,18 +33,4 @@ export declare function loadR2Config(): R2Config;
|
|
|
33
33
|
* does not re-storm every cycle. `now` is injectable for tests.
|
|
34
34
|
*/
|
|
35
35
|
export declare function isSyncConfigured(now?: number): boolean;
|
|
36
|
-
|
|
37
|
-
* Normalize a raw hostname into a stable device id: first label only,
|
|
38
|
-
* lowercased, non-alphanumerics collapsed to hyphens. `zion.tail…ts.net` and
|
|
39
|
-
* `ZION` both become `zion`. The single source for this transform — machineId()
|
|
40
|
-
* and cross-machine session grouping must agree or the local machine won't
|
|
41
|
-
* match its own registry key.
|
|
42
|
-
*/
|
|
43
|
-
export declare function normalizeHost(raw: string): string;
|
|
44
|
-
/**
|
|
45
|
-
* This machine's stable, human-readable id, used as its R2 prefix and mirror
|
|
46
|
-
* directory name. Tailnet hostnames (zion, yosemite-s0, mac-mini) are already
|
|
47
|
-
* unique and readable; we lowercase and strip any domain suffix. Overridable
|
|
48
|
-
* via AGENTS_SYNC_MACHINE_ID for tests and unusual setups.
|
|
49
|
-
*/
|
|
50
|
-
export declare function machineId(): string;
|
|
36
|
+
export { machineId, normalizeHost } from '../../machine-id.js';
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* machine's stable identity. Credentials come from the `r2.backups` secrets
|
|
4
4
|
* bundle (OS keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
5
|
*/
|
|
6
|
-
import * as os from 'os';
|
|
7
6
|
import { readAndResolveBundleEnv } from '../../secrets/bundles.js';
|
|
8
7
|
/** Secrets bundle holding the R2 credentials. */
|
|
9
8
|
export const SYNC_BUNDLE = 'r2.backups';
|
|
@@ -93,22 +92,7 @@ export function isSyncConfigured(now = Date.now()) {
|
|
|
93
92
|
return false;
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* and cross-machine session grouping must agree or the local machine won't
|
|
101
|
-
* match its own registry key.
|
|
102
|
-
*/
|
|
103
|
-
export function normalizeHost(raw) {
|
|
104
|
-
return raw.split('.')[0].trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-') || 'unknown';
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* This machine's stable, human-readable id, used as its R2 prefix and mirror
|
|
108
|
-
* directory name. Tailnet hostnames (zion, yosemite-s0, mac-mini) are already
|
|
109
|
-
* unique and readable; we lowercase and strip any domain suffix. Overridable
|
|
110
|
-
* via AGENTS_SYNC_MACHINE_ID for tests and unusual setups.
|
|
111
|
-
*/
|
|
112
|
-
export function machineId() {
|
|
113
|
-
return normalizeHost(process.env.AGENTS_SYNC_MACHINE_ID || os.hostname());
|
|
114
|
-
}
|
|
95
|
+
// machineId() and normalizeHost() now live in the dependency-free leaf
|
|
96
|
+
// ../../machine-id.ts so low-level modules (state.ts) can use them without an
|
|
97
|
+
// import cycle. Re-exported here for existing importers.
|
|
98
|
+
export { machineId, normalizeHost } from '../../machine-id.js';
|
|
@@ -78,6 +78,23 @@ export interface SessionMeta {
|
|
|
78
78
|
* `entrypoint` field ('sdk-cli' for team spawns, 'cli' for real sessions).
|
|
79
79
|
*/
|
|
80
80
|
isTeamOrigin?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The machine (normalized hostname) this session's transcript originated on:
|
|
83
|
+
* the local machine for live-home sessions, or the origin machine parsed from
|
|
84
|
+
* the cross-machine mirror path (backups/<agent>/<machine>/…, see
|
|
85
|
+
* sync/agents.ts). Populated by discoverSessions for the listing/picker;
|
|
86
|
+
* undefined for sessions obtained outside that path.
|
|
87
|
+
*/
|
|
88
|
+
machine?: string;
|
|
89
|
+
/**
|
|
90
|
+
* True only for rows pulled from another machine over the live cross-machine
|
|
91
|
+
* fan-out (`remote-list.ts`) — their transcript is on that peer's disk, so
|
|
92
|
+
* reading/resuming has to hop back over SSH. Distinct from `machine`, which is
|
|
93
|
+
* also set on locally-readable synced mirrors: a mirror is machine-tagged but
|
|
94
|
+
* its `filePath` is a local path, so it must NOT be treated as remote. Set by
|
|
95
|
+
* `parseRemoteList`; transient (never persisted, stripped from --json).
|
|
96
|
+
*/
|
|
97
|
+
_remote?: boolean;
|
|
81
98
|
/** Terms that matched the current search query */
|
|
82
99
|
_matchedTerms?: string[];
|
|
83
100
|
/** BM25 relevance score from the most recent content-index search */
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface ConflictInfo {
|
|
|
77
77
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
78
78
|
* trigger auto-resync, run `agents sync` for that.
|
|
79
79
|
*/
|
|
80
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
80
|
+
export declare const SHIM_SCHEMA_VERSION = 22;
|
|
81
81
|
/**
|
|
82
82
|
* Generate the full bash shim script for the given agent. The returned string
|
|
83
83
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
|
@@ -134,8 +134,19 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
134
134
|
* generic node_modules/.bin branch.
|
|
135
135
|
* v10 — guard grok's `command -v grok` fallback against resolving to our own
|
|
136
136
|
* shims dir (same infinite re-exec loop), mirroring droid.
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
* v11 — export DISABLE_AUTOUPDATER=1 for claude aliases so a pinned per-version
|
|
138
|
+
* install can't self-mutate via Claude Code's background auto-updater.
|
|
139
|
+
* Explicit user value wins.
|
|
140
|
+
* v12 — Windows: stop writing the extensionless bash alias next to the `.cmd`
|
|
141
|
+
* (and delete a lingering one). The version suffix contains dots, so
|
|
142
|
+
* cmd.exe and PowerShell treat `claude@2.1.201` as a complete filename
|
|
143
|
+
* with extension `.201`, exact-match the bash script AHEAD of PATHEXT
|
|
144
|
+
* probing, and ShellExecute it to the `.sh` editor association — the
|
|
145
|
+
* editor opens the script and the agent never launches. The `.cmd` is
|
|
146
|
+
* now the only Windows artifact and carries this alias marker so
|
|
147
|
+
* staleness checks read it directly.
|
|
148
|
+
*/
|
|
149
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
|
|
139
150
|
/**
|
|
140
151
|
* Generate a versioned alias script that directly execs a specific version.
|
|
141
152
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -157,20 +168,39 @@ export declare function isVersionedAliasCurrent(agent: AgentId, version: string)
|
|
|
157
168
|
*/
|
|
158
169
|
export declare function ensureVersionedAliasCurrent(agent: AgentId, version: string): 'created' | 'updated' | 'current';
|
|
159
170
|
/**
|
|
160
|
-
* Get the filesystem path for a versioned alias script
|
|
171
|
+
* Get the filesystem path for a versioned alias script — the logical
|
|
172
|
+
* (extensionless) launch name. On Windows this is not a real file (see
|
|
173
|
+
* versionedAliasOnDiskFile); stat/read checks must use the on-disk path.
|
|
161
174
|
*/
|
|
162
175
|
export declare function getVersionedAliasPath(agent: AgentId, version: string): string;
|
|
176
|
+
/**
|
|
177
|
+
* The file createVersionedAlias actually materializes for a platform:
|
|
178
|
+
* `<cmd>@<version>.cmd` on Windows, the bare bash script on POSIX. Pure —
|
|
179
|
+
* testable on any host. Mirrors onDiskShimFile for the main shim.
|
|
180
|
+
*/
|
|
181
|
+
export declare function versionedAliasOnDiskFile(cliCommand: string, version: string, platform: NodeJS.Platform): string;
|
|
163
182
|
/**
|
|
164
183
|
* Create a versioned alias for a specific agent version.
|
|
165
184
|
* e.g., claude@2.0.65
|
|
185
|
+
*
|
|
186
|
+
* Same platform split as createShim (shimTargetsFor): POSIX writes the
|
|
187
|
+
* extensionless bash script; Windows writes ONLY the `.cmd`. Unlike the main
|
|
188
|
+
* shim — where the bash file was merely dead weight — a bash alias next to the
|
|
189
|
+
* versioned `.cmd` is actively harmful: the dotted version suffix makes
|
|
190
|
+
* cmd.exe/PowerShell treat `claude@2.1.201` as a complete filename (extension
|
|
191
|
+
* `.201`), so the exact match wins over PATHEXT probing and the shell
|
|
192
|
+
* ShellExecutes the bash script to the `.sh` editor association — the editor
|
|
193
|
+
* opens, the agent never launches. Any legacy bash alias is deleted here.
|
|
166
194
|
*/
|
|
167
195
|
export declare function createVersionedAlias(agent: AgentId, version: string): string;
|
|
168
196
|
/**
|
|
169
|
-
* Remove a versioned alias for a specific agent version.
|
|
197
|
+
* Remove a versioned alias for a specific agent version. Removes whichever
|
|
198
|
+
* companions exist — the extensionless script (POSIX, or a legacy Windows
|
|
199
|
+
* install that wrote it) AND the `.cmd` (Windows) — mirroring removeShim.
|
|
170
200
|
*/
|
|
171
201
|
export declare function removeVersionedAlias(agent: AgentId, version: string): boolean;
|
|
172
202
|
/**
|
|
173
|
-
* Check if a versioned alias exists.
|
|
203
|
+
* Check if a versioned alias exists (the on-disk artifact for this platform).
|
|
174
204
|
*/
|
|
175
205
|
export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
|
|
176
206
|
/**
|
package/dist/lib/shims.js
CHANGED
|
@@ -208,7 +208,10 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
208
208
|
// re-exec-looped through `command -v kimi` (the dispatcher itself).
|
|
209
209
|
// v21 — guard grok's `command -v grok` fallback against resolving to our own
|
|
210
210
|
// shims dir (same infinite re-exec loop), mirroring droid.
|
|
211
|
-
|
|
211
|
+
// v22 — export DISABLE_AUTOUPDATER=1 for claude shims so a pinned per-version
|
|
212
|
+
// install can't self-mutate: Claude Code's background auto-updater would
|
|
213
|
+
// otherwise rewrite the pinned binary in place. Explicit user value wins.
|
|
214
|
+
export const SHIM_SCHEMA_VERSION = 22;
|
|
212
215
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
213
216
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
214
217
|
function shellQuote(value) {
|
|
@@ -236,6 +239,10 @@ export function generateShimScript(agent) {
|
|
|
236
239
|
# selected version's config directory so switching versions also switches the
|
|
237
240
|
# live Claude account.
|
|
238
241
|
export CLAUDE_CONFIG_DIR="$VERSION_DIR/home/${configDirName}"
|
|
242
|
+
# Managed installs are pinned in a per-version dir; Claude Code's background
|
|
243
|
+
# auto-updater would rewrite the pinned binary in place. Disable it so a pin
|
|
244
|
+
# stays a pin. An explicit user value always wins.
|
|
245
|
+
export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
|
|
239
246
|
# On Linux sandboxes (no keychain), fall back to a per-version token file.
|
|
240
247
|
# The env var always wins if already set; no-op on macOS.
|
|
241
248
|
if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -f "\$CLAUDE_CONFIG_DIR/.oauth_token" ]; then
|
|
@@ -580,12 +587,18 @@ export function createShim(agent) {
|
|
|
580
587
|
* `cliCommand@version` for a versioned alias. node + the dist entrypoint are
|
|
581
588
|
* resolved at generation time so the launcher does not depend on `agents`
|
|
582
589
|
* already being on PATH.
|
|
590
|
+
*
|
|
591
|
+
* `extraMarkerLines` lets callers stamp additional schema markers into the
|
|
592
|
+
* header — versioned aliases embed their own alias-schema marker so
|
|
593
|
+
* readVersionedAliasSchemaVersion can stat the `.cmd` (the only Windows
|
|
594
|
+
* artifact) the same way it reads the bash script on POSIX.
|
|
583
595
|
*/
|
|
584
|
-
function writeWindowsCmdShim(cmdPath, spec) {
|
|
596
|
+
function writeWindowsCmdShim(cmdPath, spec, extraMarkerLines = []) {
|
|
585
597
|
const indexJs = getAgentsBinForGeneratedShim();
|
|
586
598
|
const content = `@echo off\r\n` +
|
|
587
599
|
`rem Auto-generated by agents-cli - do not edit\r\n` +
|
|
588
600
|
`rem ${SHIM_VERSION_MARKER} ${SHIM_SCHEMA_VERSION}\r\n` +
|
|
601
|
+
extraMarkerLines.map((line) => `rem ${line}\r\n`).join('') +
|
|
589
602
|
`node "${indexJs}" __shim ${spec} %*\r\n`;
|
|
590
603
|
fs.writeFileSync(cmdPath, content);
|
|
591
604
|
}
|
|
@@ -640,8 +653,19 @@ export function removeShim(agent) {
|
|
|
640
653
|
* generic node_modules/.bin branch.
|
|
641
654
|
* v10 — guard grok's `command -v grok` fallback against resolving to our own
|
|
642
655
|
* shims dir (same infinite re-exec loop), mirroring droid.
|
|
656
|
+
* v11 — export DISABLE_AUTOUPDATER=1 for claude aliases so a pinned per-version
|
|
657
|
+
* install can't self-mutate via Claude Code's background auto-updater.
|
|
658
|
+
* Explicit user value wins.
|
|
659
|
+
* v12 — Windows: stop writing the extensionless bash alias next to the `.cmd`
|
|
660
|
+
* (and delete a lingering one). The version suffix contains dots, so
|
|
661
|
+
* cmd.exe and PowerShell treat `claude@2.1.201` as a complete filename
|
|
662
|
+
* with extension `.201`, exact-match the bash script AHEAD of PATHEXT
|
|
663
|
+
* probing, and ShellExecute it to the `.sh` editor association — the
|
|
664
|
+
* editor opens the script and the agent never launches. The `.cmd` is
|
|
665
|
+
* now the only Windows artifact and carries this alias marker so
|
|
666
|
+
* staleness checks read it directly.
|
|
643
667
|
*/
|
|
644
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
668
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
|
|
645
669
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
646
670
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
647
671
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -669,6 +693,10 @@ export function generateVersionedAliasScript(agent, version) {
|
|
|
669
693
|
# Claude stores OAuth credentials in the macOS keychain. Scope them to this
|
|
670
694
|
# version's config directory so direct aliases also switch the live account.
|
|
671
695
|
export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
696
|
+
# Managed installs are pinned in a per-version dir; Claude Code's background
|
|
697
|
+
# auto-updater would rewrite the pinned binary in place. Disable it so a pin
|
|
698
|
+
# stays a pin. An explicit user value always wins.
|
|
699
|
+
export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
|
|
672
700
|
`
|
|
673
701
|
: agent === 'codex'
|
|
674
702
|
? `
|
|
@@ -765,7 +793,7 @@ exec "$BINARY"${launchArgs} "$@"
|
|
|
765
793
|
* alias doesn't exist or is a pre-v2 alias (no marker — treated as stale).
|
|
766
794
|
*/
|
|
767
795
|
export function readVersionedAliasSchemaVersion(agent, version) {
|
|
768
|
-
const aliasPath =
|
|
796
|
+
const aliasPath = versionedAliasOnDiskPath(agent, version);
|
|
769
797
|
if (!fs.existsSync(aliasPath))
|
|
770
798
|
return null;
|
|
771
799
|
try {
|
|
@@ -792,11 +820,19 @@ export function isVersionedAliasCurrent(agent, version) {
|
|
|
792
820
|
* was upgraded.
|
|
793
821
|
*/
|
|
794
822
|
export function ensureVersionedAliasCurrent(agent, version) {
|
|
795
|
-
|
|
796
|
-
if (!fs.existsSync(aliasPath)) {
|
|
823
|
+
if (!fs.existsSync(versionedAliasOnDiskPath(agent, version))) {
|
|
797
824
|
createVersionedAlias(agent, version);
|
|
798
825
|
return 'created';
|
|
799
826
|
}
|
|
827
|
+
// A lingering extensionless bash alias on Windows shadows the `.cmd` in both
|
|
828
|
+
// cmd.exe and PowerShell (the dotted version reads as a file extension, and
|
|
829
|
+
// an exact filename match beats PATHEXT probing), ShellExecuting the bash
|
|
830
|
+
// script to the `.sh` editor association instead of launching the agent.
|
|
831
|
+
// Regenerate regardless of the `.cmd`'s stamp so the shadow gets deleted.
|
|
832
|
+
if (shimTargetsFor(process.platform).cmd && fs.existsSync(getVersionedAliasPath(agent, version))) {
|
|
833
|
+
createVersionedAlias(agent, version);
|
|
834
|
+
return 'updated';
|
|
835
|
+
}
|
|
800
836
|
// Upgrade-only (newest-wins), same rationale as ensureShimCurrent: never
|
|
801
837
|
// downgrade an alias stamped by a newer install sharing the shims dir.
|
|
802
838
|
const onDisk = readVersionedAliasSchemaVersion(agent, version);
|
|
@@ -807,14 +843,38 @@ export function ensureVersionedAliasCurrent(agent, version) {
|
|
|
807
843
|
return 'current';
|
|
808
844
|
}
|
|
809
845
|
/**
|
|
810
|
-
* Get the filesystem path for a versioned alias script
|
|
846
|
+
* Get the filesystem path for a versioned alias script — the logical
|
|
847
|
+
* (extensionless) launch name. On Windows this is not a real file (see
|
|
848
|
+
* versionedAliasOnDiskFile); stat/read checks must use the on-disk path.
|
|
811
849
|
*/
|
|
812
850
|
export function getVersionedAliasPath(agent, version) {
|
|
813
851
|
return path.join(getShimsDir(), `${AGENTS[agent].cliCommand}@${version}`);
|
|
814
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* The file createVersionedAlias actually materializes for a platform:
|
|
855
|
+
* `<cmd>@<version>.cmd` on Windows, the bare bash script on POSIX. Pure —
|
|
856
|
+
* testable on any host. Mirrors onDiskShimFile for the main shim.
|
|
857
|
+
*/
|
|
858
|
+
export function versionedAliasOnDiskFile(cliCommand, version, platform) {
|
|
859
|
+
const name = `${cliCommand}@${version}`;
|
|
860
|
+
return shimTargetsFor(platform).cmd ? `${name}.cmd` : name;
|
|
861
|
+
}
|
|
862
|
+
/** The on-disk versioned-alias path for the current platform. */
|
|
863
|
+
function versionedAliasOnDiskPath(agent, version) {
|
|
864
|
+
return path.join(getShimsDir(), versionedAliasOnDiskFile(AGENTS[agent].cliCommand, version, process.platform));
|
|
865
|
+
}
|
|
815
866
|
/**
|
|
816
867
|
* Create a versioned alias for a specific agent version.
|
|
817
868
|
* e.g., claude@2.0.65
|
|
869
|
+
*
|
|
870
|
+
* Same platform split as createShim (shimTargetsFor): POSIX writes the
|
|
871
|
+
* extensionless bash script; Windows writes ONLY the `.cmd`. Unlike the main
|
|
872
|
+
* shim — where the bash file was merely dead weight — a bash alias next to the
|
|
873
|
+
* versioned `.cmd` is actively harmful: the dotted version suffix makes
|
|
874
|
+
* cmd.exe/PowerShell treat `claude@2.1.201` as a complete filename (extension
|
|
875
|
+
* `.201`), so the exact match wins over PATHEXT probing and the shell
|
|
876
|
+
* ShellExecutes the bash script to the `.sh` editor association — the editor
|
|
877
|
+
* opens, the agent never launches. Any legacy bash alias is deleted here.
|
|
818
878
|
*/
|
|
819
879
|
export function createVersionedAlias(agent, version) {
|
|
820
880
|
assertSafeVersion(version);
|
|
@@ -822,40 +882,42 @@ export function createVersionedAlias(agent, version) {
|
|
|
822
882
|
const shimsDir = getShimsDir();
|
|
823
883
|
const agentConfig = AGENTS[agent];
|
|
824
884
|
const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
885
|
+
const targets = shimTargetsFor(process.platform);
|
|
886
|
+
if (targets.bash) {
|
|
887
|
+
fs.writeFileSync(aliasPath, generateVersionedAliasScript(agent, version), { mode: 0o755 });
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
try {
|
|
891
|
+
fs.unlinkSync(aliasPath);
|
|
892
|
+
}
|
|
893
|
+
catch { }
|
|
894
|
+
}
|
|
895
|
+
if (targets.cmd) {
|
|
896
|
+
writeWindowsCmdShim(aliasPath + '.cmd', `${agentConfig.cliCommand}@${version}`, [`${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}`]);
|
|
829
897
|
}
|
|
830
898
|
return aliasPath;
|
|
831
899
|
}
|
|
832
900
|
/**
|
|
833
|
-
* Remove a versioned alias for a specific agent version.
|
|
901
|
+
* Remove a versioned alias for a specific agent version. Removes whichever
|
|
902
|
+
* companions exist — the extensionless script (POSIX, or a legacy Windows
|
|
903
|
+
* install that wrote it) AND the `.cmd` (Windows) — mirroring removeShim.
|
|
834
904
|
*/
|
|
835
905
|
export function removeVersionedAlias(agent, version) {
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
const aliasPath
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
try {
|
|
843
|
-
fs.unlinkSync(aliasPath + '.cmd');
|
|
844
|
-
}
|
|
845
|
-
catch { }
|
|
906
|
+
const aliasPath = getVersionedAliasPath(agent, version);
|
|
907
|
+
let removed = false;
|
|
908
|
+
for (const p of [aliasPath, aliasPath + '.cmd']) {
|
|
909
|
+
if (fs.existsSync(p)) {
|
|
910
|
+
fs.unlinkSync(p);
|
|
911
|
+
removed = true;
|
|
846
912
|
}
|
|
847
|
-
return true;
|
|
848
913
|
}
|
|
849
|
-
return
|
|
914
|
+
return removed;
|
|
850
915
|
}
|
|
851
916
|
/**
|
|
852
|
-
* Check if a versioned alias exists.
|
|
917
|
+
* Check if a versioned alias exists (the on-disk artifact for this platform).
|
|
853
918
|
*/
|
|
854
919
|
export function versionedAliasExists(agent, version) {
|
|
855
|
-
|
|
856
|
-
const agentConfig = AGENTS[agent];
|
|
857
|
-
const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
|
|
858
|
-
return fs.existsSync(aliasPath);
|
|
920
|
+
return fs.existsSync(versionedAliasOnDiskPath(agent, version));
|
|
859
921
|
}
|
|
860
922
|
/**
|
|
861
923
|
* Get the path to the agent's config directory in HOME.
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -103,6 +103,7 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
103
103
|
encoding: 'utf-8',
|
|
104
104
|
timeout: opts.timeoutMs,
|
|
105
105
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
|
+
windowsHide: true,
|
|
106
107
|
});
|
|
107
108
|
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT');
|
|
108
109
|
return {
|
|
@@ -128,6 +129,7 @@ export function sshExecRaw(target, remoteCmd, opts = {}) {
|
|
|
128
129
|
// No `encoding` → spawnSync returns Buffers.
|
|
129
130
|
timeout: opts.timeoutMs,
|
|
130
131
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
windowsHide: true,
|
|
131
133
|
});
|
|
132
134
|
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT');
|
|
133
135
|
return {
|