@phnx-labs/agents-cli 1.20.71 → 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 +26 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +2 -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/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +8 -0
- package/dist/commands/skills.js +2 -0
- 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/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/daemon.d.ts +9 -0
- package/dist/lib/daemon.js +43 -0
- package/dist/lib/hosts/logs.d.ts +12 -0
- package/dist/lib/hosts/logs.js +18 -0
- package/dist/lib/shims.d.ts +27 -0
- package/dist/lib/shims.js +29 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- 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
|
@@ -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/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();
|
package/dist/lib/hosts/logs.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* runs to be discoverable there first; until then the bounded tail is the safe
|
|
11
11
|
* concise default.) Kept in one place so the two commands can never drift.
|
|
12
12
|
*/
|
|
13
|
+
import { type HostTask } from './tasks.js';
|
|
13
14
|
export interface HostLogResult {
|
|
14
15
|
/** False when no host task with this id exists (caller may fall through to sessions). */
|
|
15
16
|
found: boolean;
|
|
@@ -21,5 +22,16 @@ export interface HostLogResult {
|
|
|
21
22
|
* default; `full` dumps the entire raw combined-stdout log.
|
|
22
23
|
*/
|
|
23
24
|
export declare function showHostTaskLog(id: string, follow: boolean, full?: boolean): Promise<HostLogResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Machine-readable form of a host-dispatch task's log — the task record plus its
|
|
27
|
+
* combined stdout. Powers `agents logs <id> --json` for the host-task branch.
|
|
28
|
+
* Reconciles a still-'running' record from the remote `.exit` first, like the
|
|
29
|
+
* text path does.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hostTaskLogJson(id: string): {
|
|
32
|
+
found: boolean;
|
|
33
|
+
task?: HostTask;
|
|
34
|
+
log?: string | null;
|
|
35
|
+
};
|
|
24
36
|
/** Last `n` lines of `text`, prefixed with an elision note when truncated. */
|
|
25
37
|
export declare function tailLines(text: string, n: number): string;
|
package/dist/lib/hosts/logs.js
CHANGED
|
@@ -54,6 +54,24 @@ export async function showHostTaskLog(id, follow, full = false) {
|
|
|
54
54
|
process.stdout.write(full ? raw : tailLines(raw, HOST_LOG_TAIL_LINES));
|
|
55
55
|
return { found: true, exitCode: 0 };
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Machine-readable form of a host-dispatch task's log — the task record plus its
|
|
59
|
+
* combined stdout. Powers `agents logs <id> --json` for the host-task branch.
|
|
60
|
+
* Reconciles a still-'running' record from the remote `.exit` first, like the
|
|
61
|
+
* text path does.
|
|
62
|
+
*/
|
|
63
|
+
export function hostTaskLogJson(id) {
|
|
64
|
+
const task = loadTask(id);
|
|
65
|
+
if (!task)
|
|
66
|
+
return { found: false };
|
|
67
|
+
// reconcileTask returns the healed record; it does NOT mutate its argument in
|
|
68
|
+
// place. Emit the reconciled task so a run that finished between dispatch and
|
|
69
|
+
// this one-shot read surfaces its terminal status/exitCode, not a stale
|
|
70
|
+
// 'running'. (The text path can discard the return — it only reads the log by
|
|
71
|
+
// id — but this JSON payload carries task.status straight to the consumer.)
|
|
72
|
+
const reconciled = reconcileTask(task);
|
|
73
|
+
return { found: true, task: reconciled, log: readTaskLog(reconciled) };
|
|
74
|
+
}
|
|
57
75
|
/** Read the task's combined-stdout — local mirror first, else fetch+cache remote. */
|
|
58
76
|
function readTaskLog(task) {
|
|
59
77
|
try {
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -153,6 +153,27 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
153
153
|
* self-update under the shim).
|
|
154
154
|
*/
|
|
155
155
|
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 13;
|
|
156
|
+
/**
|
|
157
|
+
* Agents whose config directory can be relocated by an environment variable
|
|
158
|
+
* (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
|
|
159
|
+
*
|
|
160
|
+
* Only these can be installed with `agents add --isolated`: the versioned alias
|
|
161
|
+
* points that env var at the copy's private home, so the copy never reads or
|
|
162
|
+
* writes the user's real `~/.<agent>`. Every other agent isolates ONLY by
|
|
163
|
+
* adopting `~/.<agent>` via a symlink — which an isolated install deliberately
|
|
164
|
+
* skips — so an isolated copy would silently fall back to (and mutate) the real
|
|
165
|
+
* config dir. For those agents `--isolated` is refused up front.
|
|
166
|
+
*
|
|
167
|
+
* KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
|
|
168
|
+
* The colocated test `shims.isolation-capability.test.ts` enforces this.
|
|
169
|
+
*/
|
|
170
|
+
export declare const CONFIG_ENV_ISOLATED_AGENTS: readonly AgentId[];
|
|
171
|
+
/**
|
|
172
|
+
* Whether an agent supports a clean `--isolated` install — i.e. its config
|
|
173
|
+
* location can be redirected by an env var so the isolated copy stays fully
|
|
174
|
+
* separate from the user's real `~/.<agent>`. See {@link CONFIG_ENV_ISOLATED_AGENTS}.
|
|
175
|
+
*/
|
|
176
|
+
export declare function supportsIsolatedInstall(agent: AgentId): boolean;
|
|
156
177
|
/**
|
|
157
178
|
* Generate a versioned alias script that directly execs a specific version.
|
|
158
179
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -209,6 +230,11 @@ export declare function removeVersionedAlias(agent: AgentId, version: string): b
|
|
|
209
230
|
* Check if a versioned alias exists (the on-disk artifact for this platform).
|
|
210
231
|
*/
|
|
211
232
|
export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
|
|
233
|
+
/**
|
|
234
|
+
* Get the path to the agent's config directory in HOME.
|
|
235
|
+
* e.g., ~/.claude for claude, ~/.codex for codex
|
|
236
|
+
*/
|
|
237
|
+
export declare function getAgentConfigPath(agent: AgentId): string;
|
|
212
238
|
/**
|
|
213
239
|
* Read the user's configured Codex model from their active `~/.codex/config.toml`.
|
|
214
240
|
*
|
|
@@ -449,6 +475,7 @@ export declare function hasAliasShadowingShim(agent: AgentId, overrides?: {
|
|
|
449
475
|
* Check if shims directory is in PATH.
|
|
450
476
|
*/
|
|
451
477
|
export declare function isShimsInPath(): boolean;
|
|
478
|
+
export declare function stripShimPathLines(content: string, shimsDir: string): string;
|
|
452
479
|
/**
|
|
453
480
|
* Get shell configuration instructions for adding shims to PATH.
|
|
454
481
|
*/
|
package/dist/lib/shims.js
CHANGED
|
@@ -785,6 +785,29 @@ function assertSafeVersion(version) {
|
|
|
785
785
|
throw new Error(`Refusing to generate shim for unsafe version: ${JSON.stringify(version)}`);
|
|
786
786
|
}
|
|
787
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Agents whose config directory can be relocated by an environment variable
|
|
790
|
+
* (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
|
|
791
|
+
*
|
|
792
|
+
* Only these can be installed with `agents add --isolated`: the versioned alias
|
|
793
|
+
* points that env var at the copy's private home, so the copy never reads or
|
|
794
|
+
* writes the user's real `~/.<agent>`. Every other agent isolates ONLY by
|
|
795
|
+
* adopting `~/.<agent>` via a symlink — which an isolated install deliberately
|
|
796
|
+
* skips — so an isolated copy would silently fall back to (and mutate) the real
|
|
797
|
+
* config dir. For those agents `--isolated` is refused up front.
|
|
798
|
+
*
|
|
799
|
+
* KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
|
|
800
|
+
* The colocated test `shims.isolation-capability.test.ts` enforces this.
|
|
801
|
+
*/
|
|
802
|
+
export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi'];
|
|
803
|
+
/**
|
|
804
|
+
* Whether an agent supports a clean `--isolated` install — i.e. its config
|
|
805
|
+
* location can be redirected by an env var so the isolated copy stays fully
|
|
806
|
+
* separate from the user's real `~/.<agent>`. See {@link CONFIG_ENV_ISOLATED_AGENTS}.
|
|
807
|
+
*/
|
|
808
|
+
export function supportsIsolatedInstall(agent) {
|
|
809
|
+
return CONFIG_ENV_ISOLATED_AGENTS.includes(agent);
|
|
810
|
+
}
|
|
788
811
|
/**
|
|
789
812
|
* Generate a versioned alias script that directly execs a specific version.
|
|
790
813
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -1039,7 +1062,7 @@ export function versionedAliasExists(agent, version) {
|
|
|
1039
1062
|
* Get the path to the agent's config directory in HOME.
|
|
1040
1063
|
* e.g., ~/.claude for claude, ~/.codex for codex
|
|
1041
1064
|
*/
|
|
1042
|
-
function getAgentConfigPath(agent) {
|
|
1065
|
+
export function getAgentConfigPath(agent) {
|
|
1043
1066
|
const agentConfig = AGENTS[agent];
|
|
1044
1067
|
const home = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1045
1068
|
return agentConfig.configDir.replace(os.homedir(), home);
|
|
@@ -1567,7 +1590,10 @@ export function getConfigSymlinkVersion(agent) {
|
|
|
1567
1590
|
if (!stat.isSymbolicLink()) {
|
|
1568
1591
|
return null;
|
|
1569
1592
|
}
|
|
1570
|
-
|
|
1593
|
+
// Normalize separators so this matches on Windows too — readlinkSync there
|
|
1594
|
+
// returns backslash paths, which the forward-slash-only regex never matched
|
|
1595
|
+
// (misclassifying an owned symlink as foreign, e.g. in `agents uninstall`).
|
|
1596
|
+
const target = fs.readlinkSync(configPath).replace(/\\/g, '/');
|
|
1571
1597
|
// Extract version from path like ~/.agents/versions/claude/2.0.65/home/.claude
|
|
1572
1598
|
const match = target.match(/versions\/[^/]+\/([^/]+)\/home/);
|
|
1573
1599
|
return match ? match[1] : null;
|
|
@@ -2194,7 +2220,7 @@ function isShimPathCommandLine(line, shimsDir) {
|
|
|
2194
2220
|
}
|
|
2195
2221
|
return trimmed.startsWith('export PATH=') || trimmed.startsWith('fish_add_path ');
|
|
2196
2222
|
}
|
|
2197
|
-
function stripShimPathLines(content, shimsDir) {
|
|
2223
|
+
export function stripShimPathLines(content, shimsDir) {
|
|
2198
2224
|
const lines = content.split('\n');
|
|
2199
2225
|
const kept = [];
|
|
2200
2226
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -85,6 +85,7 @@ export declare const loadPull: ModuleLoader;
|
|
|
85
85
|
export declare const loadPush: ModuleLoader;
|
|
86
86
|
export declare const loadRepo: ModuleLoader;
|
|
87
87
|
export declare const loadSetup: ModuleLoader;
|
|
88
|
+
export declare const loadUninstall: ModuleLoader;
|
|
88
89
|
export declare const loadSessions: ModuleLoader;
|
|
89
90
|
export declare const loadTeams: ModuleLoader;
|
|
90
91
|
export declare const loadCloud: ModuleLoader;
|
|
@@ -63,6 +63,7 @@ export const loadPull = async () => (await import('../../commands/pull.js')).reg
|
|
|
63
63
|
export const loadPush = async () => (await import('../../commands/push.js')).registerPushCommand;
|
|
64
64
|
export const loadRepo = async () => (await import('../../commands/repo.js')).registerRepoCommands;
|
|
65
65
|
export const loadSetup = async () => (await import('../../commands/setup.js')).registerSetupCommand;
|
|
66
|
+
export const loadUninstall = async () => (await import('../../commands/uninstall.js')).registerUninstallCommands;
|
|
66
67
|
export const loadSessions = async () => (await import('../../commands/sessions.js')).registerSessionsCommands;
|
|
67
68
|
export const loadTeams = async () => (await import('../../commands/teams.js')).registerTeamsCommands;
|
|
68
69
|
export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
|
|
@@ -174,6 +175,7 @@ export const COMMAND_LOADERS = {
|
|
|
174
175
|
repos: [loadRepo],
|
|
175
176
|
repo: [loadRepo],
|
|
176
177
|
setup: [loadSetup],
|
|
178
|
+
uninstall: [loadUninstall],
|
|
177
179
|
sessions: [loadSessions],
|
|
178
180
|
teams: [loadTeams],
|
|
179
181
|
cloud: [loadCloud],
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AgentId } from './types.js';
|
|
2
|
+
/** What the uninstall intends to do with one agent's config directory. */
|
|
3
|
+
export type ConfigAction = {
|
|
4
|
+
agent: AgentId;
|
|
5
|
+
realPath: string;
|
|
6
|
+
kind: 'restore-backup';
|
|
7
|
+
source: string;
|
|
8
|
+
} | {
|
|
9
|
+
agent: AgentId;
|
|
10
|
+
realPath: string;
|
|
11
|
+
kind: 'restore-version-home';
|
|
12
|
+
source: string;
|
|
13
|
+
} | {
|
|
14
|
+
agent: AgentId;
|
|
15
|
+
realPath: string;
|
|
16
|
+
kind: 'remove-dangling';
|
|
17
|
+
} | {
|
|
18
|
+
agent: AgentId;
|
|
19
|
+
realPath: string;
|
|
20
|
+
kind: 'leave-real';
|
|
21
|
+
} | {
|
|
22
|
+
agent: AgentId;
|
|
23
|
+
realPath: string;
|
|
24
|
+
kind: 'leave-foreign';
|
|
25
|
+
} | {
|
|
26
|
+
agent: AgentId;
|
|
27
|
+
realPath: string;
|
|
28
|
+
kind: 'absent';
|
|
29
|
+
};
|
|
30
|
+
/** A home-level file symlink (e.g. `~/.claude.json`) agents-cli owns. */
|
|
31
|
+
export interface HomeFileAction {
|
|
32
|
+
realPath: string;
|
|
33
|
+
/** Resolved symlink target whose contents are copied back to `realPath`. */
|
|
34
|
+
source: string;
|
|
35
|
+
}
|
|
36
|
+
/** The full, read-only plan describing what an uninstall would change. */
|
|
37
|
+
export interface UninstallPlan {
|
|
38
|
+
isInstalled: boolean;
|
|
39
|
+
agentsDir: string;
|
|
40
|
+
legacySymlink: string | null;
|
|
41
|
+
configs: ConfigAction[];
|
|
42
|
+
homeFiles: HomeFileAction[];
|
|
43
|
+
launchers: string[];
|
|
44
|
+
rcFiles: string[];
|
|
45
|
+
}
|
|
46
|
+
/** Structured result of an executed uninstall (for reporting). */
|
|
47
|
+
export interface UninstallResult {
|
|
48
|
+
restoredConfigs: Array<{
|
|
49
|
+
agent: AgentId;
|
|
50
|
+
realPath: string;
|
|
51
|
+
}>;
|
|
52
|
+
removedDanglingConfigs: Array<{
|
|
53
|
+
agent: AgentId;
|
|
54
|
+
realPath: string;
|
|
55
|
+
}>;
|
|
56
|
+
restoredHomeFiles: string[];
|
|
57
|
+
releasedLaunchers: string[];
|
|
58
|
+
cleanedRcFiles: string[];
|
|
59
|
+
agentsDir: {
|
|
60
|
+
path: string;
|
|
61
|
+
disposition: 'moved' | 'purged' | 'absent';
|
|
62
|
+
movedTo?: string;
|
|
63
|
+
};
|
|
64
|
+
legacySymlinkRemoved: boolean;
|
|
65
|
+
/** True when `--purge` was requested but downgraded to move-aside after errors. */
|
|
66
|
+
purgeDowngraded: boolean;
|
|
67
|
+
errors: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build a read-only plan of everything a complete uninstall would change.
|
|
71
|
+
* Performs no mutations; safe to run for `--dry-run` and to print for confirm.
|
|
72
|
+
*/
|
|
73
|
+
export declare function planUninstall(): UninstallPlan;
|
|
74
|
+
/**
|
|
75
|
+
* Execute a plan built by {@link planUninstall}. Restores adopted config dirs
|
|
76
|
+
* and home files, releases adopted launchers, strips shim PATH lines, then
|
|
77
|
+
* disposes of `~/.agents` — moved aside to `~/.agents.removed-<ts>` (recoverable)
|
|
78
|
+
* by default, or hard-deleted when `purge` is set. Config restore always runs
|
|
79
|
+
* before disposal because the backups live inside `~/.agents`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function executeUninstall(plan: UninstallPlan, opts: {
|
|
82
|
+
purge?: boolean;
|
|
83
|
+
timestamp: number;
|
|
84
|
+
}): UninstallResult;
|