@phnx-labs/agents-cli 1.20.70 → 1.20.72
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 +85 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +36 -3
- package/dist/commands/check.js +3 -1
- package/dist/commands/commands.js +2 -0
- package/dist/commands/exec.js +12 -0
- package/dist/commands/fleet-capture.d.ts +14 -0
- package/dist/commands/fleet-capture.js +107 -0
- package/dist/commands/fork.js +13 -7
- package/dist/commands/hosts.js +11 -0
- package/dist/commands/logs.js +38 -9
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +8 -0
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +24 -4
- package/dist/commands/sessions-tail.js +1 -2
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +14 -6
- package/dist/commands/skills.js +2 -0
- package/dist/commands/ssh.js +24 -6
- package/dist/commands/status.js +8 -2
- package/dist/commands/subagents.js +3 -1
- package/dist/commands/uninstall.d.ts +11 -0
- package/dist/commands/uninstall.js +158 -0
- package/dist/commands/usage.d.ts +13 -0
- package/dist/commands/usage.js +50 -28
- package/dist/commands/utils.d.ts +29 -0
- package/dist/commands/utils.js +24 -0
- package/dist/commands/versions.js +120 -11
- package/dist/index.js +5 -3
- package/dist/lib/commands.d.ts +10 -0
- package/dist/lib/commands.js +31 -7
- package/dist/lib/daemon.d.ts +9 -0
- package/dist/lib/daemon.js +43 -0
- package/dist/lib/devices/connect.d.ts +13 -0
- package/dist/lib/devices/connect.js +20 -0
- package/dist/lib/devices/fleet.d.ts +36 -3
- package/dist/lib/devices/fleet.js +42 -4
- package/dist/lib/devices/sync.d.ts +30 -0
- package/dist/lib/devices/sync.js +50 -0
- package/dist/lib/doctor-diff.js +38 -16
- package/dist/lib/fleet/apply.d.ts +4 -0
- package/dist/lib/fleet/apply.js +28 -5
- package/dist/lib/fleet/capture.d.ts +35 -0
- package/dist/lib/fleet/capture.js +51 -0
- package/dist/lib/fleet/manifest.d.ts +6 -0
- package/dist/lib/fleet/manifest.js +24 -1
- package/dist/lib/fleet/types.d.ts +24 -1
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +39 -1
- package/dist/lib/hosts/logs.d.ts +12 -0
- package/dist/lib/hosts/logs.js +18 -0
- package/dist/lib/menubar/install-menubar.d.ts +12 -0
- package/dist/lib/menubar/install-menubar.js +26 -13
- package/dist/lib/secrets/agent.d.ts +5 -0
- package/dist/lib/secrets/agent.js +35 -3
- package/dist/lib/secrets/bundles.js +28 -0
- package/dist/lib/secrets/index.d.ts +14 -0
- package/dist/lib/secrets/index.js +36 -5
- package/dist/lib/secrets/session-store.d.ts +90 -0
- package/dist/lib/secrets/session-store.js +221 -0
- package/dist/lib/session/render.d.ts +9 -1
- package/dist/lib/session/render.js +35 -4
- package/dist/lib/share/capture.d.ts +2 -0
- package/dist/lib/share/capture.js +12 -5
- package/dist/lib/shims.d.ts +27 -0
- package/dist/lib/shims.js +29 -3
- package/dist/lib/skills.d.ts +8 -1
- package/dist/lib/skills.js +11 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/types.d.ts +5 -1
- package/dist/lib/uninstall.d.ts +84 -0
- package/dist/lib/uninstall.js +347 -0
- package/dist/lib/usage.d.ts +13 -1
- package/dist/lib/usage.js +32 -14
- package/dist/lib/versions.d.ts +16 -0
- package/dist/lib/versions.js +40 -3
- package/package.json +1 -1
package/dist/commands/utils.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Small helpers used across multiple commands: prompt cancellation detection,
|
|
5
5
|
* table formatting, spinner management, and platform-specific workarounds.
|
|
6
6
|
*/
|
|
7
|
+
import type { Command } from 'commander';
|
|
7
8
|
import type { AgentId } from '../lib/types.js';
|
|
8
9
|
import { type InstalledAgentTargetResult, type VersionSelectionResult } from '../lib/versions.js';
|
|
9
10
|
/**
|
|
@@ -22,6 +23,34 @@ export declare function isPromptCancelled(err: unknown): boolean;
|
|
|
22
23
|
* True when stdin/stdout are attached to a real terminal.
|
|
23
24
|
*/
|
|
24
25
|
export declare function isInteractiveTerminal(): boolean;
|
|
26
|
+
/** The resolved I/O surface for one command invocation — the human/agent split. */
|
|
27
|
+
export interface Surface {
|
|
28
|
+
/** Machine-readable output was requested (`--json`). */
|
|
29
|
+
json: boolean;
|
|
30
|
+
/** Skip confirmation prompts — explicit `--yes`/`-y`, or a non-interactive shell. */
|
|
31
|
+
assumeYes: boolean;
|
|
32
|
+
/** Suppress non-essential human chrome (`--quiet`). */
|
|
33
|
+
quiet: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Safe to show an interactive picker/prompt: a real terminal AND not asking for
|
|
36
|
+
* `--json` (a JSON consumer is a machine and never wants a picker).
|
|
37
|
+
*/
|
|
38
|
+
interactive: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Compute a command's I/O surface once, in one place, instead of each command
|
|
42
|
+
* re-deriving the human-vs-agent split (and raw-sniffing `process.std*.isTTY`
|
|
43
|
+
* with inconsistent stream choices — the audit's R4). Reads the merged option
|
|
44
|
+
* set via `optsWithGlobals()`, so it sees the command's own flags and any
|
|
45
|
+
* inherited global ones, and folds in the terminal state:
|
|
46
|
+
*
|
|
47
|
+
* - `assumeYes` = explicit `--yes` OR a non-interactive shell (no one to prompt).
|
|
48
|
+
* - `interactive` = a real TTY AND not `--json`.
|
|
49
|
+
*
|
|
50
|
+
* Adopt incrementally: a command switches its ad-hoc `isTTY`/`options.yes` checks
|
|
51
|
+
* to a single `const s = resolveSurface(cmd)` without changing its flag surface.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveSurface(cmd: Command): Surface;
|
|
25
54
|
/**
|
|
26
55
|
* Exit with a clean message when a picker would be required in a non-interactive shell.
|
|
27
56
|
*/
|
package/dist/commands/utils.js
CHANGED
|
@@ -45,6 +45,30 @@ export function isPromptCancelled(err) {
|
|
|
45
45
|
export function isInteractiveTerminal() {
|
|
46
46
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Compute a command's I/O surface once, in one place, instead of each command
|
|
50
|
+
* re-deriving the human-vs-agent split (and raw-sniffing `process.std*.isTTY`
|
|
51
|
+
* with inconsistent stream choices — the audit's R4). Reads the merged option
|
|
52
|
+
* set via `optsWithGlobals()`, so it sees the command's own flags and any
|
|
53
|
+
* inherited global ones, and folds in the terminal state:
|
|
54
|
+
*
|
|
55
|
+
* - `assumeYes` = explicit `--yes` OR a non-interactive shell (no one to prompt).
|
|
56
|
+
* - `interactive` = a real TTY AND not `--json`.
|
|
57
|
+
*
|
|
58
|
+
* Adopt incrementally: a command switches its ad-hoc `isTTY`/`options.yes` checks
|
|
59
|
+
* to a single `const s = resolveSurface(cmd)` without changing its flag surface.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveSurface(cmd) {
|
|
62
|
+
const opts = cmd.optsWithGlobals();
|
|
63
|
+
const tty = isInteractiveTerminal();
|
|
64
|
+
const json = opts.json === true;
|
|
65
|
+
return {
|
|
66
|
+
json,
|
|
67
|
+
assumeYes: opts.yes === true || !tty,
|
|
68
|
+
quiet: opts.quiet === true,
|
|
69
|
+
interactive: tty && !json,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
48
72
|
/**
|
|
49
73
|
* Exit with a clean message when a picker would be required in a non-interactive shell.
|
|
50
74
|
*/
|
|
@@ -8,9 +8,9 @@ import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo
|
|
|
8
8
|
import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
9
|
import { viewAction } from './view.js';
|
|
10
10
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
11
|
-
import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
|
|
11
|
+
import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
|
|
12
12
|
import { carryForwardSettings } from '../lib/settings-manifest.js';
|
|
13
|
-
import { createShim, createVersionedAlias, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
|
|
13
|
+
import { createShim, createVersionedAlias, supportsIsolatedInstall, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
|
|
14
14
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
15
15
|
import { tryAutoPull } from '../lib/git.js';
|
|
16
16
|
import { getAgentsDir, getTrashVersionsDir } from '../lib/state.js';
|
|
@@ -98,8 +98,30 @@ function warnIfShimShadowed(agent) {
|
|
|
98
98
|
console.log(chalk.green(` Added shim directory to ${result.location}.`));
|
|
99
99
|
console.log(chalk.gray(` ${result.reloadHint}`));
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Install an isolated copy of an agent version: a fully self-contained install
|
|
103
|
+
* that never touches the user's existing setup.
|
|
104
|
+
*
|
|
105
|
+
* Unlike a normal install it does NOT set (or offer to set) the global default,
|
|
106
|
+
* does NOT create/replace the bare `<agent>` shim, does NOT back up or symlink
|
|
107
|
+
* the user's real `~/.<agent>`, and does NOT carry over settings or resources.
|
|
108
|
+
* It only creates the versioned alias (so the copy is launchable) and records
|
|
109
|
+
* the isolated marker. Invoke the copy explicitly with `agents run <agent>@<v>`.
|
|
110
|
+
*/
|
|
111
|
+
function finalizeIsolatedInstall(agent, version) {
|
|
112
|
+
const agentConfig = AGENTS[agent];
|
|
113
|
+
const label = agentLabel(agentConfig.id);
|
|
114
|
+
createVersionedAlias(agent, version);
|
|
115
|
+
markVersionIsolated(agent, version);
|
|
116
|
+
console.log(chalk.green(` Installed ${label}@${version} as an isolated copy.`));
|
|
117
|
+
console.log(chalk.gray(` Your existing ${agentConfig.configDir} and default ${label} are untouched.`));
|
|
118
|
+
console.log(chalk.gray(` Run it: agents run ${agent}@${version} "your prompt"`));
|
|
119
|
+
console.log(chalk.gray(` It has its own config and login — sign in the first time you run it.`));
|
|
120
|
+
console.log(chalk.gray(` Remove it: agents remove ${agent}@${version} --isolated`));
|
|
121
|
+
}
|
|
101
122
|
async function versionPruneAction(specs, options, commandName) {
|
|
102
123
|
const isProject = options.project;
|
|
124
|
+
const isIsolated = options.isolated;
|
|
103
125
|
const moved = [];
|
|
104
126
|
for (const spec of specs) {
|
|
105
127
|
const parsed = parseAgentSpec(spec);
|
|
@@ -110,6 +132,12 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
110
132
|
}
|
|
111
133
|
const { agent, version } = parsed;
|
|
112
134
|
const agentConfig = AGENTS[agent];
|
|
135
|
+
// --isolated only ever applies to agents that can BE installed isolated;
|
|
136
|
+
// for the rest no isolated version can exist, so say so plainly.
|
|
137
|
+
if (isIsolated && !supportsIsolatedInstall(agent)) {
|
|
138
|
+
console.log(chalk.gray(`${agentLabel(agentConfig.id)} has no isolated installs (--isolated is not supported for it).`));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
113
141
|
// Script-installed agents (droid, grok) can have a *literal* `latest`
|
|
114
142
|
// version dir on disk when the post-install version probe failed. An
|
|
115
143
|
// explicit `<agent>@latest` should remove that dir directly rather than
|
|
@@ -117,9 +145,14 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
117
145
|
// so treat an installed literal `latest` as a concrete pinned version.
|
|
118
146
|
const isLiteralLatestInstalled = version === 'latest' && spec.includes('@') && isVersionInstalled(agent, 'latest');
|
|
119
147
|
if (!isLiteralLatestInstalled && (version === 'latest' || version === 'oldest' || !spec.includes('@'))) {
|
|
120
|
-
|
|
148
|
+
// With --isolated, only isolated installs are eligible for the picker, so
|
|
149
|
+
// a normal/default install can never be selected here by accident.
|
|
150
|
+
const versions = listInstalledVersions(agent)
|
|
151
|
+
.filter((v) => !isIsolated || isVersionIsolated(agent, v));
|
|
121
152
|
if (versions.length === 0) {
|
|
122
|
-
console.log(chalk.gray(
|
|
153
|
+
console.log(chalk.gray(isIsolated
|
|
154
|
+
? `No isolated ${agentLabel(agentConfig.id)} installs`
|
|
155
|
+
: `No versions of ${agentLabel(agentConfig.id)} installed`));
|
|
123
156
|
continue;
|
|
124
157
|
}
|
|
125
158
|
if (!isInteractiveTerminal()) {
|
|
@@ -157,6 +190,9 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
157
190
|
}
|
|
158
191
|
fixSessionFilePaths(agent, v, versionDir);
|
|
159
192
|
console.log(chalk.green(`Moved ${agentLabel(agentConfig.id)}@${v} to trash`));
|
|
193
|
+
if (isIsolated) {
|
|
194
|
+
console.log(chalk.gray(` Your real ${agentConfig.configDir} and default ${agentLabel(agentConfig.id)} were untouched.`));
|
|
195
|
+
}
|
|
160
196
|
moved.push({ agent, version: v });
|
|
161
197
|
}
|
|
162
198
|
// Default reassignment/clearing is handled at the source in
|
|
@@ -177,6 +213,13 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
177
213
|
else if (!isVersionInstalled(agent, version)) {
|
|
178
214
|
console.log(chalk.gray(`${agentLabel(agentConfig.id)}@${version} not installed`));
|
|
179
215
|
}
|
|
216
|
+
else if (isIsolated && !isVersionIsolated(agent, version)) {
|
|
217
|
+
// Safety guard: `--isolated` refuses to remove a normal/default install,
|
|
218
|
+
// so an accidental `remove <agent>@<default> --isolated` can never delete
|
|
219
|
+
// the user's primary version or disturb their real ~/.<agent>.
|
|
220
|
+
console.log(chalk.yellow(`${agentLabel(agentConfig.id)}@${version} is not an isolated install; refusing to remove it under --isolated.`));
|
|
221
|
+
console.log(chalk.gray(` Drop --isolated to remove a normal version: agents ${commandName} ${agent}@${version}`));
|
|
222
|
+
}
|
|
180
223
|
else {
|
|
181
224
|
const versionDir = getVersionDir(agent, version);
|
|
182
225
|
const removed = removeVersion(agent, version);
|
|
@@ -186,6 +229,9 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
186
229
|
}
|
|
187
230
|
fixSessionFilePaths(agent, version, versionDir);
|
|
188
231
|
console.log(chalk.green(`Moved ${agentLabel(agentConfig.id)}@${version} to trash`));
|
|
232
|
+
if (isIsolated) {
|
|
233
|
+
console.log(chalk.gray(` Your real ${agentConfig.configDir} and default ${agentLabel(agentConfig.id)} were untouched.`));
|
|
234
|
+
}
|
|
189
235
|
moved.push({ agent, version });
|
|
190
236
|
const remaining = listInstalledVersions(agent);
|
|
191
237
|
if (remaining.length === 0) {
|
|
@@ -212,7 +258,8 @@ function configureVersionPruneCommand(cmd, commandName) {
|
|
|
212
258
|
.description(isAlias
|
|
213
259
|
? 'Alias for agents prune. Uninstalls agent CLI versions.'
|
|
214
260
|
: 'Uninstall agent CLI versions. Moves version data to trash for recovery.')
|
|
215
|
-
.option('-p, --project', 'Also clear the pinned version from .agents/agents.yaml in the current project')
|
|
261
|
+
.option('-p, --project', 'Also clear the pinned version from .agents/agents.yaml in the current project')
|
|
262
|
+
.option('--isolated', 'Only act on isolated installs (created with `agents add --isolated`). Refuses to remove a normal/default install and never touches your real ~/.<agent>.');
|
|
216
263
|
setHelpSections(cmd, {
|
|
217
264
|
examples: `
|
|
218
265
|
# Prune a specific version
|
|
@@ -223,11 +270,15 @@ function configureVersionPruneCommand(cmd, commandName) {
|
|
|
223
270
|
|
|
224
271
|
# Prune and also clear the project pin
|
|
225
272
|
agents ${commandName} claude@2.0.50 --project
|
|
273
|
+
|
|
274
|
+
# Cleanly remove an isolated copy, leaving your normal install alone
|
|
275
|
+
agents ${commandName} claude@2.1.112 --isolated
|
|
226
276
|
`,
|
|
227
277
|
notes: `
|
|
228
278
|
- Pruned version directories move to trash with their home/ data intact.
|
|
229
279
|
- Session file paths are rewritten so session history remains readable.
|
|
230
280
|
- Removing the default version unsets the default; run 'agents use' to pick a new one.
|
|
281
|
+
- --isolated restricts the operation to isolated installs and refuses to remove a normal/default version, so your existing setup is never disturbed.
|
|
231
282
|
- Reinstall any time with 'agents add'.
|
|
232
283
|
`,
|
|
233
284
|
});
|
|
@@ -239,6 +290,7 @@ export function registerVersionsCommands(program) {
|
|
|
239
290
|
.command('add <specs...>')
|
|
240
291
|
.description('Download and install agent CLI versions. Enables subsidized API usage through managed binaries.')
|
|
241
292
|
.option('-p, --project', 'Lock this version to the current project directory only, stored in project-root agents.yaml')
|
|
293
|
+
.option('--isolated', 'Install a fully self-contained copy that never touches your existing ~/.<agent> or default. Launch it explicitly with `agents run <agent>@<version>`. Cannot be combined with --project.')
|
|
242
294
|
.option('-y, --yes', 'Auto-accept defaults without prompting (useful for scripts and CI)');
|
|
243
295
|
setHelpSections(addCmd, {
|
|
244
296
|
examples: `
|
|
@@ -256,16 +308,26 @@ export function registerVersionsCommands(program) {
|
|
|
256
308
|
|
|
257
309
|
# Lock a version to this project only (won't affect global default)
|
|
258
310
|
agents add claude@2.1.100 --project
|
|
311
|
+
|
|
312
|
+
# Install a clean, separate copy that leaves your existing setup alone
|
|
313
|
+
agents add claude@2.1.112 --isolated
|
|
259
314
|
`,
|
|
260
315
|
notes: `
|
|
261
316
|
- The first version you install becomes the default automatically.
|
|
262
317
|
- 'add' does NOT change the default if a default already exists. Use 'agents use' to switch.
|
|
263
318
|
- Multi-account: each installed version has separate auth, so you can install the same agent twice for two accounts.
|
|
319
|
+
- --isolated installs a self-contained copy: it never sets the default, never creates the bare '<agent>' shim, and never backs up or symlinks your real ~/.<agent>. Run it with 'agents run <agent>@<version>' and remove it with 'agents remove <agent>@<version> --isolated'. Mutually exclusive with --project.
|
|
264
320
|
`,
|
|
265
321
|
});
|
|
266
322
|
addCmd.action(async (specs, options) => {
|
|
267
323
|
const isProject = options.project;
|
|
324
|
+
const isIsolated = options.isolated;
|
|
268
325
|
const skipPrompts = options.yes || !isInteractiveTerminal();
|
|
326
|
+
if (isIsolated && isProject) {
|
|
327
|
+
console.log(chalk.red('--isolated and --project cannot be combined.'));
|
|
328
|
+
console.log(chalk.gray('An isolated copy is global-but-separate; a project pin selects a shared install for one directory.'));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
269
331
|
for (const spec of specs) {
|
|
270
332
|
const parsed = parseAgentSpec(spec);
|
|
271
333
|
if (!parsed) {
|
|
@@ -276,6 +338,16 @@ export function registerVersionsCommands(program) {
|
|
|
276
338
|
const { agent, version } = parsed;
|
|
277
339
|
const agentConfig = AGENTS[agent];
|
|
278
340
|
warnAgentDeprecated(agent);
|
|
341
|
+
// Isolation relies on a config-dir env var to redirect the copy away
|
|
342
|
+
// from the user's real ~/.<agent>. Agents without one isolate only by
|
|
343
|
+
// adopting ~/.<agent> (which --isolated skips), so an isolated copy
|
|
344
|
+
// would silently read/write the real config. Refuse rather than lie.
|
|
345
|
+
if (isIsolated && !supportsIsolatedInstall(agent)) {
|
|
346
|
+
console.log(chalk.red(`${agentLabel(agentConfig.id)} does not support --isolated installs.`));
|
|
347
|
+
console.log(chalk.gray(` It has no config-directory env var, so it can only isolate by adopting ${agentConfig.configDir} — which --isolated deliberately avoids.`));
|
|
348
|
+
console.log(chalk.gray(` Supported with --isolated: ${CONFIG_ENV_ISOLATED_AGENTS.join(', ')}.`));
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
279
351
|
if (!agentConfig.npmPackage && !agentConfig.installScript) {
|
|
280
352
|
console.log(chalk.yellow(`${agentLabel(agentConfig.id)} has no npm package. Install manually.`));
|
|
281
353
|
continue;
|
|
@@ -301,6 +373,19 @@ export function registerVersionsCommands(program) {
|
|
|
301
373
|
alreadyInstalled = isVersionInstalled(agent, version);
|
|
302
374
|
}
|
|
303
375
|
if (alreadyInstalled) {
|
|
376
|
+
if (isIsolated) {
|
|
377
|
+
if (!isVersionIsolated(agent, installedAsVersion)) {
|
|
378
|
+
// A normal and an isolated install of the SAME version share one
|
|
379
|
+
// on-disk dir, so they can't coexist. Refuse rather than silently
|
|
380
|
+
// convert the user's existing (possibly default) install.
|
|
381
|
+
console.log(chalk.yellow(`${agentLabel(agentConfig.id)}@${installedAsVersion} is already installed as a normal (default-eligible) version.`));
|
|
382
|
+
console.log(chalk.gray(` Remove it first (agents remove ${agent}@${installedAsVersion}) then re-add with --isolated, or pick a different version.`));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
// Already isolated: re-affirm the alias + marker idempotently.
|
|
386
|
+
finalizeIsolatedInstall(agent, installedAsVersion);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
304
389
|
console.log(chalk.gray(`${agentLabel(agentConfig.id)}@${installedAsVersion} already installed`));
|
|
305
390
|
// Ensure shim exists (in case it was deleted or needs updating)
|
|
306
391
|
createShim(agent);
|
|
@@ -312,15 +397,23 @@ export function registerVersionsCommands(program) {
|
|
|
312
397
|
});
|
|
313
398
|
if (result.success) {
|
|
314
399
|
spinner.succeed(`Installed ${agentLabel(agentConfig.id)}@${result.installedVersion}`);
|
|
400
|
+
const installedVersion = result.installedVersion || version;
|
|
401
|
+
// Track the concrete version so a `--project` pin records it instead
|
|
402
|
+
// of the `latest`/`oldest` alias.
|
|
403
|
+
installedAsVersion = installedVersion;
|
|
404
|
+
// Isolated installs stop here: no bare shim, no settings carry-over,
|
|
405
|
+
// no resource sync, no default switch, no PATH edits. Just a
|
|
406
|
+
// launchable versioned alias + the isolated marker, leaving the
|
|
407
|
+
// user's real ~/.<agent> and default untouched.
|
|
408
|
+
if (isIsolated) {
|
|
409
|
+
finalizeIsolatedInstall(agent, installedVersion);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
315
412
|
// Create shim if first install
|
|
316
413
|
if (!shimExists(agent)) {
|
|
317
414
|
createShim(agent);
|
|
318
415
|
console.log(chalk.gray(` Created shim: ${getShimsDir()}/${agentConfig.cliCommand}`));
|
|
319
416
|
}
|
|
320
|
-
const installedVersion = result.installedVersion || version;
|
|
321
|
-
// Track the concrete version so a `--project` pin records it instead
|
|
322
|
-
// of the `latest`/`oldest` alias.
|
|
323
|
-
installedAsVersion = installedVersion;
|
|
324
417
|
// Seed the fresh version home with user settings from the current
|
|
325
418
|
// default version (settings.json, keybindings, codex config/auth).
|
|
326
419
|
// Gap-filling only — never overwrites what the new home has.
|
|
@@ -540,8 +633,11 @@ export function registerVersionsCommands(program) {
|
|
|
540
633
|
const agentConfig = AGENTS[agentId];
|
|
541
634
|
let selectedVersion = version;
|
|
542
635
|
if (!version) {
|
|
543
|
-
// Interactive version picker
|
|
544
|
-
|
|
636
|
+
// Interactive version picker. Isolated installs are walled off from the
|
|
637
|
+
// real ~/.<agent> on purpose, so they must never be selectable here —
|
|
638
|
+
// setting one as the default would repoint the real config at it. Filter
|
|
639
|
+
// them out (same as the `remove` picker), leaving only usable versions.
|
|
640
|
+
const versions = listInstalledVersions(agentId).filter((v) => !isVersionIsolated(agentId, v));
|
|
545
641
|
if (versions.length === 0) {
|
|
546
642
|
console.log(chalk.red(`No versions of ${agentLabel(agentConfig.id)} installed`));
|
|
547
643
|
console.log(chalk.gray(`Run: agents add ${agentId}@latest`));
|
|
@@ -602,6 +698,19 @@ export function registerVersionsCommands(program) {
|
|
|
602
698
|
}
|
|
603
699
|
// selectedVersion is guaranteed to be defined after the check above
|
|
604
700
|
const finalVersion = selectedVersion;
|
|
701
|
+
// An isolated install is deliberately walled off from the real
|
|
702
|
+
// ~/.<agent>. `agents use` would repoint the real config symlink at it
|
|
703
|
+
// (switchConfigSymlink) and carry settings forward INTO it
|
|
704
|
+
// (carryForwardSettings) — a direct breach of the isolation guarantee.
|
|
705
|
+
// Refuse for both the explicit `use <agent>@<isolated>` path (the picker
|
|
706
|
+
// above already filters isolated versions out of the interactive path).
|
|
707
|
+
// Isolated copies are launched explicitly via `agents run <agent>@<v>`.
|
|
708
|
+
if (isVersionIsolated(agentId, finalVersion)) {
|
|
709
|
+
console.log(chalk.red(`${agentLabel(agentConfig.id)}@${finalVersion} is an isolated install and can't be set as your active version.`));
|
|
710
|
+
console.log(chalk.gray(`Isolated copies stay walled off from your real ${agentConfig.configDir}. Run it directly: agents run ${agentId}@${finalVersion}`));
|
|
711
|
+
console.log(chalk.gray(`To install this version normally: agents add ${agentId}@${finalVersion}`));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
605
714
|
if (options.project) {
|
|
606
715
|
// Set in project manifest
|
|
607
716
|
const projectManifestDir = path.join(process.cwd(), '.agents');
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadShare, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -738,6 +738,7 @@ async function registerAllEagerCommands() {
|
|
|
738
738
|
await reg(loadPush);
|
|
739
739
|
await reg(loadRepo);
|
|
740
740
|
await reg(loadSetup);
|
|
741
|
+
await reg(loadUninstall);
|
|
741
742
|
}
|
|
742
743
|
/** Calculate the Levenshtein edit distance between two strings. */
|
|
743
744
|
function levenshtein(a, b) {
|
|
@@ -865,8 +866,9 @@ if (firstRun) {
|
|
|
865
866
|
process.exit(0);
|
|
866
867
|
}
|
|
867
868
|
// Every command requires the system repo to be cloned first. `setup` is the
|
|
868
|
-
//
|
|
869
|
-
|
|
869
|
+
// command that does the cloning; `uninstall` is its reverse and must run even
|
|
870
|
+
// from a broken/half-setup state (that is exactly when you want to tear down).
|
|
871
|
+
const SETUP_EXEMPT_COMMANDS = new Set(['setup', 'help', 'uninstall']);
|
|
870
872
|
// Fold legacy ~/.agents-system/ into ~/.agents/.system/ BEFORE ensureInitialized
|
|
871
873
|
// runs. ensureInitialized checks for .git inside the new path; if the user is
|
|
872
874
|
// upgrading from a layout where .git lives under the legacy path, the check
|
package/dist/lib/commands.d.ts
CHANGED
|
@@ -89,6 +89,16 @@ export interface VersionCommandDiff {
|
|
|
89
89
|
/**
|
|
90
90
|
* Compare a version home's commands against central. Returns the reconciliation diff.
|
|
91
91
|
*/
|
|
92
|
+
/**
|
|
93
|
+
* Flattened names of plugin-bundled commands (`<plugin>-<command>`), matching
|
|
94
|
+
* exactly how `syncPluginToVersion` installs a plugin's `commands/<cmd>.md` as a
|
|
95
|
+
* command-skill (plugins.ts → `installCommandSkillToVersion(agentDir,
|
|
96
|
+
* `${plugin.name}-${cmd}`, …)`). These are source-managed by their plugin, so
|
|
97
|
+
* the orphan detector must NOT flag them — else `prune cleanup` proposes
|
|
98
|
+
* deleting live plugin commands (swarm-plan, code-review, …). Scans user +
|
|
99
|
+
* system + extra marketplaces (no project layer), matching the trusted sources.
|
|
100
|
+
*/
|
|
101
|
+
export declare function listPluginCommandNames(): Set<string>;
|
|
92
102
|
export declare function diffVersionCommands(agent: AgentId, version: string): VersionCommandDiff;
|
|
93
103
|
/**
|
|
94
104
|
* Install a single command from central into a specific version home.
|
package/dist/lib/commands.js
CHANGED
|
@@ -14,6 +14,7 @@ import { capableAgents, isCapable, supports } from './capabilities.js';
|
|
|
14
14
|
import { markdownToToml } from './convert.js';
|
|
15
15
|
import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir, getTrashCommandsDir } from './state.js';
|
|
16
16
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions, resolveVersion } from './versions.js';
|
|
17
|
+
import { discoverPlugins } from './plugins.js';
|
|
17
18
|
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
18
19
|
import { installGooseCommandToVersion, listGooseCommandsInVersion, gooseCommandMatches, removeGooseCommandFromVersion, } from './goose-commands.js';
|
|
19
20
|
function compareVersions(a, b) {
|
|
@@ -311,8 +312,26 @@ function versionCommandMatches(agent, version, commandName) {
|
|
|
311
312
|
/**
|
|
312
313
|
* Compare a version home's commands against central. Returns the reconciliation diff.
|
|
313
314
|
*/
|
|
315
|
+
/**
|
|
316
|
+
* Flattened names of plugin-bundled commands (`<plugin>-<command>`), matching
|
|
317
|
+
* exactly how `syncPluginToVersion` installs a plugin's `commands/<cmd>.md` as a
|
|
318
|
+
* command-skill (plugins.ts → `installCommandSkillToVersion(agentDir,
|
|
319
|
+
* `${plugin.name}-${cmd}`, …)`). These are source-managed by their plugin, so
|
|
320
|
+
* the orphan detector must NOT flag them — else `prune cleanup` proposes
|
|
321
|
+
* deleting live plugin commands (swarm-plan, code-review, …). Scans user +
|
|
322
|
+
* system + extra marketplaces (no project layer), matching the trusted sources.
|
|
323
|
+
*/
|
|
324
|
+
export function listPluginCommandNames() {
|
|
325
|
+
const names = new Set();
|
|
326
|
+
for (const plugin of discoverPlugins()) {
|
|
327
|
+
for (const cmd of plugin.commands)
|
|
328
|
+
names.add(`${plugin.name}-${cmd}`);
|
|
329
|
+
}
|
|
330
|
+
return names;
|
|
331
|
+
}
|
|
314
332
|
export function diffVersionCommands(agent, version) {
|
|
315
333
|
const central = new Set(listCentralCommands());
|
|
334
|
+
const pluginCommands = listPluginCommandNames();
|
|
316
335
|
const installed = new Set(listCommandsInVersionHome(agent, version));
|
|
317
336
|
const toAdd = [];
|
|
318
337
|
const toUpdate = [];
|
|
@@ -335,15 +354,20 @@ export function diffVersionCommands(agent, version) {
|
|
|
335
354
|
}
|
|
336
355
|
}
|
|
337
356
|
for (const name of installed) {
|
|
338
|
-
if (
|
|
339
|
-
|
|
357
|
+
if (central.has(name)) {
|
|
358
|
+
const sourcePath = path.join(getCommandsDir(), `${name}.md`);
|
|
359
|
+
const metadata = parseCommandMetadata(sourcePath);
|
|
360
|
+
if (!commandAppliesTo(agent, version, metadata).ok) {
|
|
361
|
+
toRemove.push(name);
|
|
362
|
+
}
|
|
340
363
|
continue;
|
|
341
364
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
365
|
+
// A plugin-bundled command (installed as `<plugin>-<cmd>`) is source-managed
|
|
366
|
+
// by its plugin — not an orphan. Only a name with no central AND no plugin
|
|
367
|
+
// source is genuinely unmanaged.
|
|
368
|
+
if (pluginCommands.has(name))
|
|
369
|
+
continue;
|
|
370
|
+
orphans.push(name);
|
|
347
371
|
}
|
|
348
372
|
return {
|
|
349
373
|
agent,
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
* log output, reload (SIGHUP), and graceful shutdown are handled here.
|
|
8
8
|
*/
|
|
9
9
|
import { getAgentsBinPath } from './cli-entry.js';
|
|
10
|
+
/**
|
|
11
|
+
* RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
|
|
12
|
+
* broker. The startup host decision is one-shot; this drives the periodic
|
|
13
|
+
* self-heal re-check. Take over ONLY when the daemon is not already hosting AND
|
|
14
|
+
* no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
|
|
15
|
+
* start has since died or crash-looped. Never take over while our in-process
|
|
16
|
+
* broker is hosting, and never clobber a reachable (healthy) broker.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shouldTakeOverBroker(isHosting: boolean, brokerReachable: boolean): boolean;
|
|
10
19
|
/** Read the stored daemon PID from disk. Returns null if not present or invalid. */
|
|
11
20
|
export declare function readDaemonPid(): number | null;
|
|
12
21
|
/** Write the daemon PID to the pid file. */
|
package/dist/lib/daemon.js
CHANGED
|
@@ -38,6 +38,17 @@ const WEDGE_THRESHOLD_TICKS = 3;
|
|
|
38
38
|
// session (which expires between runs and produces intermittent 401s).
|
|
39
39
|
const DAEMON_OAUTH_BUNDLE = 'claude';
|
|
40
40
|
const DAEMON_OAUTH_KEY = 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
41
|
+
/**
|
|
42
|
+
* RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
|
|
43
|
+
* broker. The startup host decision is one-shot; this drives the periodic
|
|
44
|
+
* self-heal re-check. Take over ONLY when the daemon is not already hosting AND
|
|
45
|
+
* no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
|
|
46
|
+
* start has since died or crash-looped. Never take over while our in-process
|
|
47
|
+
* broker is hosting, and never clobber a reachable (healthy) broker.
|
|
48
|
+
*/
|
|
49
|
+
export function shouldTakeOverBroker(isHosting, brokerReachable) {
|
|
50
|
+
return !isHosting && !brokerReachable;
|
|
51
|
+
}
|
|
41
52
|
function getDaemonDir() {
|
|
42
53
|
const dir = getDaemonDirRoot();
|
|
43
54
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -661,6 +672,37 @@ export async function runDaemon() {
|
|
|
661
672
|
};
|
|
662
673
|
const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
|
|
663
674
|
const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
|
|
675
|
+
// RUSH-1817: the startup host decision above is one-shot. If a standalone
|
|
676
|
+
// broker answered agentPing() at daemon start, the daemon declined to host —
|
|
677
|
+
// but should that standalone later die or crash-loop, nothing takes over and
|
|
678
|
+
// every `agents secrets unlock|export|start` fails until a manual restart
|
|
679
|
+
// (this wedged all keychain-backed secrets on zion and blocked a release).
|
|
680
|
+
// Re-probe on a cadence: whenever the daemon is NOT itself hosting AND no
|
|
681
|
+
// healthy broker answers a ping, take over hosting. startHostedBroker binds
|
|
682
|
+
// the socket only when it is free, so a take-over never races a live broker.
|
|
683
|
+
let selfHealingBroker = false;
|
|
684
|
+
const runBrokerSelfHeal = async () => {
|
|
685
|
+
if (selfHealingBroker)
|
|
686
|
+
return;
|
|
687
|
+
selfHealingBroker = true;
|
|
688
|
+
try {
|
|
689
|
+
const { agentPing, startHostedBroker } = await import('./secrets/agent.js');
|
|
690
|
+
const reachable = (await agentPing()).reachable;
|
|
691
|
+
if (!shouldTakeOverBroker(hostedBroker != null, reachable))
|
|
692
|
+
return;
|
|
693
|
+
hostedBroker = await startHostedBroker();
|
|
694
|
+
if (hostedBroker) {
|
|
695
|
+
log('WARN', 'Secrets broker was unreachable; daemon took over hosting (self-heal)');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
log('WARN', `Secrets broker self-heal skipped: ${err.message}`);
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
selfHealingBroker = false;
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
const brokerSelfHealInterval = setInterval(() => { void runBrokerSelfHeal(); }, 60_000);
|
|
664
706
|
const handleReload = () => {
|
|
665
707
|
log('INFO', 'Reloading jobs (SIGHUP)');
|
|
666
708
|
scheduler.reloadAll();
|
|
@@ -695,6 +737,7 @@ export async function runDaemon() {
|
|
|
695
737
|
clearTimeout(launchHealthKickoff);
|
|
696
738
|
clearInterval(fleetCacheInterval);
|
|
697
739
|
clearTimeout(fleetCacheKickoff);
|
|
740
|
+
clearInterval(brokerSelfHealInterval);
|
|
698
741
|
hostedBroker?.close();
|
|
699
742
|
removeDaemonPid();
|
|
700
743
|
removeHeartbeat();
|
|
@@ -8,6 +8,19 @@ export declare const ASKPASS_KEY_ENV = "AGENTS_SSH_KEY";
|
|
|
8
8
|
* it against the shared injection guard. Throws if the device has no address.
|
|
9
9
|
*/
|
|
10
10
|
export declare function sshTargetFor(device: DeviceProfile): string;
|
|
11
|
+
/**
|
|
12
|
+
* The address a fleet fan-out probe should hand to `ssh` for a device: the
|
|
13
|
+
* registry's known-good Tailscale dnsName/IP (via {@link sshTargetFor}) when
|
|
14
|
+
* present, else the bare name (an address-less manual device dials by name as
|
|
15
|
+
* before — never worse than the old behaviour).
|
|
16
|
+
*
|
|
17
|
+
* Prefer the registry address so a stale `~/.ssh/config` alias can never shadow
|
|
18
|
+
* it: dialing the bare device name lets ssh resolve it through the user's config,
|
|
19
|
+
* where a hand-written `Host <name>` block with a DHCP-drifted LAN IP silently
|
|
20
|
+
* shadows the correct entry, times out, and makes a reachable box look dead.
|
|
21
|
+
* Pure/testable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function fleetDialTarget(device: DeviceProfile): string;
|
|
11
24
|
/**
|
|
12
25
|
* Wrap a remote command for the device's shell. Windows devices speak
|
|
13
26
|
* PowerShell, so a bare command is run through `powershell -NoProfile
|
|
@@ -38,6 +38,26 @@ export function sshTargetFor(device) {
|
|
|
38
38
|
assertValidSshTarget(target);
|
|
39
39
|
return target;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The address a fleet fan-out probe should hand to `ssh` for a device: the
|
|
43
|
+
* registry's known-good Tailscale dnsName/IP (via {@link sshTargetFor}) when
|
|
44
|
+
* present, else the bare name (an address-less manual device dials by name as
|
|
45
|
+
* before — never worse than the old behaviour).
|
|
46
|
+
*
|
|
47
|
+
* Prefer the registry address so a stale `~/.ssh/config` alias can never shadow
|
|
48
|
+
* it: dialing the bare device name lets ssh resolve it through the user's config,
|
|
49
|
+
* where a hand-written `Host <name>` block with a DHCP-drifted LAN IP silently
|
|
50
|
+
* shadows the correct entry, times out, and makes a reachable box look dead.
|
|
51
|
+
* Pure/testable.
|
|
52
|
+
*/
|
|
53
|
+
export function fleetDialTarget(device) {
|
|
54
|
+
try {
|
|
55
|
+
return sshTargetFor(device);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return device.user ? `${device.user}@${device.name}` : device.name;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
41
61
|
/**
|
|
42
62
|
* Wrap a remote command for the device's shell. Windows devices speak
|
|
43
63
|
* PowerShell, so a bare command is run through `powershell -NoProfile
|
|
@@ -70,17 +70,50 @@ export declare function runOnDevice(device: DeviceProfile, cmd: string[], opts?:
|
|
|
70
70
|
stdout: string;
|
|
71
71
|
stderr: string;
|
|
72
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Run `cmd` on THIS machine directly — no ssh. Used by {@link runFleet} for the
|
|
75
|
+
* self target: a box frequently can't ssh to itself (no self-authorized key, as
|
|
76
|
+
* `agents fleet update` hit trying to reach zion from zion) and doesn't need to —
|
|
77
|
+
* `agents upgrade` etc. runs identically as a local process. Mirrors
|
|
78
|
+
* {@link runOnDevice}'s return shape and never throws. The argv is space-joined
|
|
79
|
+
* and evaluated by a shell — matching the POSIX-shell ssh path (so PATH-resolved
|
|
80
|
+
* `agents`, quoting, and `;`/`&&` behave the same). It does NOT replicate the
|
|
81
|
+
* powershell-device encoding runOnDevice uses (`connect.ts` base64 path): a
|
|
82
|
+
* Windows self runs under the default OS shell (cmd.exe), which still resolves
|
|
83
|
+
* `agents` on PATH for the only self commands that matter (`agents upgrade …`).
|
|
84
|
+
*/
|
|
85
|
+
export declare function runLocalCommand(cmd: string[], opts?: {
|
|
86
|
+
timeoutMs?: number;
|
|
87
|
+
}): {
|
|
88
|
+
code: number | null;
|
|
89
|
+
stdout: string;
|
|
90
|
+
stderr: string;
|
|
91
|
+
};
|
|
73
92
|
/**
|
|
74
93
|
* Build `agents upgrade --yes` argv, optionally pinned to a version/dist-tag.
|
|
75
94
|
* Rejects anything that is not a plain npm version/tag token so a version pin
|
|
76
95
|
* cannot inject shell metacharacters into the remote command line.
|
|
77
96
|
*/
|
|
78
97
|
export declare function upgradeCommand(version?: string): string[];
|
|
98
|
+
export interface RunFleetOptions {
|
|
99
|
+
/**
|
|
100
|
+
* Name of THIS machine. Its target runs the command **locally** (no ssh) — a
|
|
101
|
+
* box can't reliably ssh to itself and doesn't need to. Omit to ssh every
|
|
102
|
+
* target (the old behaviour). Callers pass `machineId()`.
|
|
103
|
+
*/
|
|
104
|
+
self?: string;
|
|
105
|
+
/** Injectable ssh runner (tests). */
|
|
106
|
+
runner?: typeof runOnDevice;
|
|
107
|
+
/** Injectable local runner (tests). */
|
|
108
|
+
localRunner?: typeof runLocalCommand;
|
|
109
|
+
}
|
|
79
110
|
/**
|
|
80
111
|
* Execute a command across planned targets. Pure orchestration over
|
|
81
|
-
* {@link runOnDevice}; testable by injecting
|
|
82
|
-
*
|
|
112
|
+
* {@link runOnDevice} / {@link runLocalCommand}; testable by injecting either.
|
|
113
|
+
* The `self` target runs locally so `agents fleet update` upgrades this machine
|
|
114
|
+
* too instead of failing to ssh to itself. Per-device throws from a runner are
|
|
115
|
+
* recorded as `failed` so one bad device never aborts the rest.
|
|
83
116
|
*/
|
|
84
|
-
export declare function runFleet(targets: FleetTarget[], cmd: string[],
|
|
117
|
+
export declare function runFleet(targets: FleetTarget[], cmd: string[], opts?: RunFleetOptions): FleetRunResult[];
|
|
85
118
|
/** Run one async probe per device in parallel, preserving input order. */
|
|
86
119
|
export declare function fanOutDevices<T, Target extends FanOutDeviceTarget = FanOutDeviceTarget>(targets: Target[], probe: (target: Target) => Promise<T>): Promise<FanOutDeviceResult<T>[]>;
|