@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
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.20.
|
|
3
|
+
## 1.20.72
|
|
4
|
+
|
|
5
|
+
- **Stop `agents doctor` from reporting phantom drift and `agents prune` from
|
|
6
|
+
deleting source-managed resources.** Three reconciler false positives are
|
|
7
|
+
fixed: the instruction file (`CLAUDE.md`/`AGENTS.md`) is now compared against
|
|
8
|
+
the composed active-preset output the rules writer actually emits — not the raw
|
|
9
|
+
whole-repo `rules/AGENTS.md` — so a correctly-synced home no longer shows as
|
|
10
|
+
permanent drift; plugin-bundled commands installed as `<plugin>-<command>`
|
|
11
|
+
command-skills (e.g. `swarm-plan`, `code-review`) are no longer flagged as
|
|
12
|
+
orphans/extras that `prune cleanup` would delete; and command-as-skill wrappers
|
|
13
|
+
(the `agents_command` marker) are no longer miscounted as skills and surfaced as
|
|
14
|
+
deletable skill orphans. Source: `apps/cli/src/lib/staleness/`,
|
|
15
|
+
`apps/cli/src/lib/commands.ts`, `apps/cli/src/lib/skills.ts`.
|
|
4
16
|
|
|
5
17
|
- **Capture your whole fleet into `agents.yaml`, then rebuild it anywhere with
|
|
6
18
|
`agents apply` (#1305).** New `agents fleet capture` (alias `agents devices
|
|
@@ -20,6 +32,17 @@
|
|
|
20
32
|
|
|
21
33
|
- **`agents fleet status` no longer hangs on a stale `~/.ssh/config`.** The fleet probes (version / doctor / `fleet ping`) now dial each device at its registry Tailscale address (`dnsName`/IP) instead of the bare host name — so a hand-written `Host <name>` block carrying a drifted LAN IP can no longer shadow the correct entry and make a reachable box look dead. It also fails fast: a device the stats probe already found unreachable is skipped straight to an unreachable row instead of eating a 15s+30s version+doctor timeout. Source: `apps/cli/src/commands/ssh.ts`.
|
|
22
34
|
|
|
35
|
+
- **No more macOS keychain password prompt from an interactive command.** Writing a non-`agents-cli.` keychain item (e.g. a refreshed Claude OAuth token during `agents view`, or an `agents secrets add`) via `/usr/bin/security add-generic-password -w` piped the value over stdin — but `readpassphrase(3)` reads the *controlling terminal* when one exists, so in an interactive shell `security` prompted the user ("password data for new item:") and hung to the timeout, ignoring the piped value. The write now runs `detached` (a new session with no controlling terminal) so the piped stdin is always used. Verified under a pty. Source: `apps/cli/src/lib/secrets/index.ts`.
|
|
36
|
+
|
|
37
|
+
- **`agents logs <id> --json` now reports the true final status of a host task.**
|
|
38
|
+
For a run that finished remotely between dispatch and the one-shot `--json`
|
|
39
|
+
read, the payload emitted a stale `status: "running"` with no `exitCode` — even
|
|
40
|
+
though the completed log was already present — because `hostTaskLogJson`
|
|
41
|
+
discarded the reconciled record `reconcileTask` returns (it heals a new object
|
|
42
|
+
rather than mutating in place). It now emits the reconciled task, so a polling
|
|
43
|
+
agent sees `completed`/`failed` + `exitCode` + `finishedAt`. Source:
|
|
44
|
+
`apps/cli/src/lib/hosts/logs.ts`.
|
|
45
|
+
|
|
23
46
|
- **Fix the macOS menu-bar auto-heal so upgrades actually restart the helper.**
|
|
24
47
|
`agents` has an on-startup self-heal that re-copies `MenubarHelper.app` when
|
|
25
48
|
the CLI version changes, but on modern macOS `launchctl bootstrap` fails when
|
|
@@ -60,6 +83,8 @@
|
|
|
60
83
|
|
|
61
84
|
- **`agents share` cover capture finds Playwright's `chrome-headless-shell` packages.** `scanCaches()` only knew the classic `chrome-mac/Chromium.app` and `Google Chrome for Testing` layouts, so on machines whose Playwright cache holds only the newer `chromium_headless_shell-*` packages (a raw `chrome-headless-shell` binary, not an `.app` bundle) — and no system Chrome/Brave/Edge — the OG cover capture silently returned null and shared plans published without a preview card. The scan now matches `chrome-headless-shell-mac-arm64`, `chrome-headless-shell-mac-x64`, and `chrome-headless-shell-linux64` layouts alongside the existing ones. Source: `apps/cli/src/lib/share/capture.ts`, `apps/cli/src/lib/share/capture.test.ts`.
|
|
62
85
|
|
|
86
|
+
- **New `agents uninstall` — cleanly reverse adoption and restore your original setup.** Installing agents-cli *adopts* your agent config: it moves `~/.<agent>` aside and replaces it with a symlink into the version homes, adopts the launcher on `PATH`, and adds the shim dir to your shell rc — but until now nothing put any of that back, so removing the CLI stranded your original config under `~/.agents/.history/backups/` and left `~/.claude` a dangling symlink. `agents uninstall` is the reverse of `agents setup`: it restores every adopted `~/.<agent>` (from the timestamped backup, or the version home for imported installs), restores owned home files, releases adopted launchers, strips the shim dir from every shell rc, then disposes of `~/.agents` — moved aside to `~/.agents.removed-<ts>` (recoverable) by default, or hard-deleted with `--purge`. A config agents-cli never adopted is never touched (ownership is decided structurally by `getConfigSymlinkVersion`, the same check `removeVersion` uses); `--dry-run` prints the full plan without changing anything; and if any restore step errors, `--purge` self-downgrades to the recoverable move-aside so a swallowed error can never take your only copy. Works on macOS, Linux, and Windows (junctions and cross-volume `~/.agents` handled). Source: `apps/cli/src/lib/uninstall.ts`, `apps/cli/src/commands/uninstall.ts`.
|
|
87
|
+
|
|
63
88
|
## 1.20.70
|
|
64
89
|
|
|
65
90
|
- **Fix `agents setup computer` / `agents computer setup` refusing to install a
|
package/dist/bin/agents
CHANGED
|
Binary file
|
|
@@ -43,6 +43,7 @@ When to use:
|
|
|
43
43
|
`);
|
|
44
44
|
commandsCmd
|
|
45
45
|
.command('list [agent]')
|
|
46
|
+
.option('--json', 'Emit machine-readable JSON instead of the table/picker')
|
|
46
47
|
.description('Show which slash commands are installed and which agent versions they are synced to')
|
|
47
48
|
.option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
|
|
48
49
|
.action(async (agentArg, options) => {
|
|
@@ -73,6 +74,7 @@ When to use:
|
|
|
73
74
|
centralPath: getCommandsDir(),
|
|
74
75
|
filterAgent,
|
|
75
76
|
filterVersion,
|
|
77
|
+
json: options.json,
|
|
76
78
|
});
|
|
77
79
|
});
|
|
78
80
|
commandsCmd
|
package/dist/commands/logs.js
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import { discoverSessions, resolveSessionById } from '../lib/session/discover.js';
|
|
26
|
-
import { parseAgentFilter, renderSessionLog } from './sessions.js';
|
|
26
|
+
import { parseAgentFilter, renderSessionLog, renderSessionLogJson } from './sessions.js';
|
|
27
27
|
import { streamSessionTail, isTailable } from './sessions-tail.js';
|
|
28
|
-
import { showHostTaskLog } from '../lib/hosts/logs.js';
|
|
28
|
+
import { showHostTaskLog, hostTaskLogJson } from '../lib/hosts/logs.js';
|
|
29
29
|
import { listTasks } from '../lib/hosts/tasks.js';
|
|
30
30
|
import { itemPicker } from '../lib/picker.js';
|
|
31
31
|
import { query, stats, getLogsPath, rotate, levelFor, } from '../lib/events.js';
|
|
@@ -43,8 +43,20 @@ function candidateLabel(c) {
|
|
|
43
43
|
const title = s.label || s.topic || '';
|
|
44
44
|
return `${chalk.gray('sess')} ${s.shortId.padEnd(9)} ${(s.agent + ver).padEnd(14)} ${chalk.gray(s.timestamp.slice(0, 16))} ${title.slice(0, 40)}`;
|
|
45
45
|
}
|
|
46
|
+
/** Emit a host-dispatch task's log as JSON: `{ kind, task, log }`. */
|
|
47
|
+
function emitHostTaskJson(id) {
|
|
48
|
+
const hj = hostTaskLogJson(id);
|
|
49
|
+
if (!hj.found)
|
|
50
|
+
return false;
|
|
51
|
+
process.stdout.write(JSON.stringify({ kind: 'task', task: hj.task, log: hj.log ?? null }, null, 2) + '\n');
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
46
54
|
/** Show a resolved session — follow (tail), concise summary, or (`full`) transcript. */
|
|
47
|
-
async function showSession(session, follow, full) {
|
|
55
|
+
async function showSession(session, follow, full, json = false) {
|
|
56
|
+
if (json) {
|
|
57
|
+
await renderSessionLogJson(session);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
48
60
|
if (follow) {
|
|
49
61
|
if (!isTailable(session.agent)) {
|
|
50
62
|
console.error(chalk.red(`Tailing is supported for claude and codex sessions only (got ${session.agent}).`));
|
|
@@ -55,17 +67,33 @@ async function showSession(session, follow, full) {
|
|
|
55
67
|
}
|
|
56
68
|
await renderSessionLog(session, full ? 'markdown' : 'summary');
|
|
57
69
|
}
|
|
58
|
-
async function showCandidate(c, follow, full) {
|
|
70
|
+
async function showCandidate(c, follow, full, json = false) {
|
|
59
71
|
if (c.kind === 'task') {
|
|
72
|
+
if (json) {
|
|
73
|
+
emitHostTaskJson(c.task.id);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
60
76
|
const res = await showHostTaskLog(c.task.id, follow, full);
|
|
61
77
|
if (res.exitCode !== undefined)
|
|
62
78
|
process.exitCode = res.exitCode;
|
|
63
79
|
return;
|
|
64
80
|
}
|
|
65
|
-
await showSession(c.session, follow, full);
|
|
81
|
+
await showSession(c.session, follow, full, json);
|
|
66
82
|
}
|
|
67
83
|
/** Resolve an explicit id/--session: host task first, then a session. */
|
|
68
|
-
async function showById(id, follow, full) {
|
|
84
|
+
async function showById(id, follow, full, json = false) {
|
|
85
|
+
if (json) {
|
|
86
|
+
if (emitHostTaskJson(id))
|
|
87
|
+
return;
|
|
88
|
+
const sessions = await discoverSessions({ all: true, limit: 5000 });
|
|
89
|
+
const matches = resolveSessionById(sessions, id);
|
|
90
|
+
if (matches.length === 0) {
|
|
91
|
+
console.error(chalk.red(`No run or session found matching "${id}".`));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
await renderSessionLogJson(matches[0]);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
69
97
|
const hostRes = await showHostTaskLog(id, follow, full);
|
|
70
98
|
if (hostRes.found) {
|
|
71
99
|
if (hostRes.exitCode !== undefined)
|
|
@@ -85,7 +113,7 @@ async function runLogs(id, opts) {
|
|
|
85
113
|
const full = !!opts.full;
|
|
86
114
|
const directId = opts.session ?? id;
|
|
87
115
|
if (directId) {
|
|
88
|
-
await showById(directId, follow, full);
|
|
116
|
+
await showById(directId, follow, full, !!opts.json);
|
|
89
117
|
return;
|
|
90
118
|
}
|
|
91
119
|
const { agent, version } = parseAgentFilter(opts.agent);
|
|
@@ -110,7 +138,7 @@ async function runLogs(id, opts) {
|
|
|
110
138
|
process.exit(1);
|
|
111
139
|
}
|
|
112
140
|
if (candidates.length === 1) {
|
|
113
|
-
await showCandidate(candidates[0], follow, full);
|
|
141
|
+
await showCandidate(candidates[0], follow, full, !!opts.json);
|
|
114
142
|
return;
|
|
115
143
|
}
|
|
116
144
|
// Multiple sessions matched → picker if interactive, else a list to pick from.
|
|
@@ -134,7 +162,7 @@ async function runLogs(id, opts) {
|
|
|
134
162
|
});
|
|
135
163
|
if (!picked)
|
|
136
164
|
return;
|
|
137
|
-
await showCandidate(picked.item, follow, full);
|
|
165
|
+
await showCandidate(picked.item, follow, full, !!opts.json);
|
|
138
166
|
}
|
|
139
167
|
function parseSince(s) {
|
|
140
168
|
const m = s.match(/^(\d+)([smhdw])$/);
|
|
@@ -343,6 +371,7 @@ export function registerLogsCommand(program) {
|
|
|
343
371
|
.option('--session <id>', 'Select a session/run by id (same as the positional id)')
|
|
344
372
|
.option('-f, --follow', 'Follow live output')
|
|
345
373
|
.option('-m, --full', 'Show the full raw transcript / stdout instead of the concise summary')
|
|
374
|
+
.option('--json', 'Machine-readable JSON: a host task as { kind, task, log }, a session as the redacted { session, events } (same shape as `sessions <id> --json`)')
|
|
346
375
|
.action((id, opts) => runLogs(id, opts));
|
|
347
376
|
logsCmd
|
|
348
377
|
.command('audit')
|
package/dist/commands/mcp.js
CHANGED
|
@@ -147,6 +147,7 @@ When to use:
|
|
|
147
147
|
.command('list [agent]')
|
|
148
148
|
.description('Show which MCP servers are registered and which agent versions they are synced to')
|
|
149
149
|
.option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
|
|
150
|
+
.option('--json', 'Emit machine-readable JSON instead of the table/picker')
|
|
150
151
|
.action(async (agentArg, options) => {
|
|
151
152
|
const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
|
|
152
153
|
const agentInput = agentArg || options.agent;
|
|
@@ -176,6 +177,7 @@ When to use:
|
|
|
176
177
|
centralPath: getMcpDir(),
|
|
177
178
|
filterAgent,
|
|
178
179
|
filterVersion,
|
|
180
|
+
json: options.json,
|
|
179
181
|
});
|
|
180
182
|
});
|
|
181
183
|
mcpCmd
|
|
@@ -33,6 +33,8 @@ export interface ResourceViewOptions {
|
|
|
33
33
|
/** When the user specified agent or agent@version, we scope per-agent. */
|
|
34
34
|
filterAgent?: AgentId;
|
|
35
35
|
filterVersion?: string;
|
|
36
|
+
/** Emit machine-readable JSON instead of the picker/table (for agents/scripts). */
|
|
37
|
+
json?: boolean;
|
|
36
38
|
}
|
|
37
39
|
/** Display a resource list: interactive picker in TTY mode, plain table otherwise. */
|
|
38
40
|
export declare function showResourceList(opts: ResourceViewOptions): Promise<void>;
|
|
@@ -14,6 +14,14 @@ import { isInteractiveTerminal, isPromptCancelled, printWithPager } from './util
|
|
|
14
14
|
import { terminalWidth, truncateToWidth, padToWidth, stringWidth, stripAnsi } from '../lib/session/width.js';
|
|
15
15
|
/** Display a resource list: interactive picker in TTY mode, plain table otherwise. */
|
|
16
16
|
export async function showResourceList(opts) {
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
// Strip the non-serializable buildDetail thunk; emit the row metadata plus
|
|
19
|
+
// each resource's per-agent-version sync targets. One shared JSON contract
|
|
20
|
+
// for every resource `list` built on this helper.
|
|
21
|
+
const rows = opts.rows.map(({ buildDetail, ...row }) => row);
|
|
22
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
17
25
|
if (opts.rows.length === 0) {
|
|
18
26
|
console.log(chalk.gray(opts.emptyMessage));
|
|
19
27
|
return;
|
|
@@ -155,6 +155,12 @@ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap:
|
|
|
155
155
|
* (`agents logs <id> --full`). Reuses the shared `renderSession` renderer.
|
|
156
156
|
*/
|
|
157
157
|
export declare function renderSessionLog(session: SessionMeta, mode?: ViewMode): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Emit one session exactly as `agents sessions <id> --json` does — the same
|
|
160
|
+
* redact-by-default `{ session, events }` shape — so `agents logs <id> --json`
|
|
161
|
+
* shares one machine-readable session contract instead of inventing another.
|
|
162
|
+
*/
|
|
163
|
+
export declare function renderSessionLogJson(session: SessionMeta): Promise<void>;
|
|
158
164
|
/** Column-visibility flags for the picker row, computed once over the whole pool. */
|
|
159
165
|
export interface PickerColumns {
|
|
160
166
|
/** Render the machine column (only when the pool spans more than one machine). */
|
|
@@ -1422,6 +1422,14 @@ function resolveViewMode(options, filters) {
|
|
|
1422
1422
|
export async function renderSessionLog(session, mode = 'summary') {
|
|
1423
1423
|
await renderSession(session, mode, {});
|
|
1424
1424
|
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Emit one session exactly as `agents sessions <id> --json` does — the same
|
|
1427
|
+
* redact-by-default `{ session, events }` shape — so `agents logs <id> --json`
|
|
1428
|
+
* shares one machine-readable session contract instead of inventing another.
|
|
1429
|
+
*/
|
|
1430
|
+
export async function renderSessionLogJson(session) {
|
|
1431
|
+
await renderSession(session, 'json', {});
|
|
1432
|
+
}
|
|
1425
1433
|
async function renderSession(session, mode, filters, options = {}) {
|
|
1426
1434
|
// OpenCode stores sessions in SQLite; filePath is "db_path#session_id"
|
|
1427
1435
|
const realPath = session.filePath.split('#')[0];
|
package/dist/commands/skills.js
CHANGED
|
@@ -45,6 +45,7 @@ When to use:
|
|
|
45
45
|
.command('list [agent]')
|
|
46
46
|
.description('Show which skills are installed and which agent versions they are synced to')
|
|
47
47
|
.option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
|
|
48
|
+
.option('--json', 'Emit machine-readable JSON instead of the table/picker')
|
|
48
49
|
.action(async (agentArg, options) => {
|
|
49
50
|
const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
|
|
50
51
|
const agentInput = agentArg || options.agent;
|
|
@@ -74,6 +75,7 @@ When to use:
|
|
|
74
75
|
centralPath: getSkillsDir(),
|
|
75
76
|
filterAgent,
|
|
76
77
|
filterVersion,
|
|
78
|
+
json: options.json,
|
|
77
79
|
});
|
|
78
80
|
});
|
|
79
81
|
skillsCmd
|
package/dist/commands/status.js
CHANGED
|
@@ -13,6 +13,7 @@ import { AGENTS } from '../lib/agents.js';
|
|
|
13
13
|
import { setHelpSections } from '../lib/help.js';
|
|
14
14
|
import { computeSyncStatus } from '../lib/sync-status.js';
|
|
15
15
|
import { promptDriftSync } from '../lib/drift-sync.js';
|
|
16
|
+
import { resolveSurface } from './utils.js';
|
|
16
17
|
const agentName = (id) => AGENTS[id]?.name ?? id;
|
|
17
18
|
function versionSummary(v) {
|
|
18
19
|
if (!v.everSynced)
|
|
@@ -46,9 +47,14 @@ export function registerStatusCommand(program) {
|
|
|
46
47
|
agents status --yes
|
|
47
48
|
`,
|
|
48
49
|
});
|
|
49
|
-
cmd.action(async (opts) => {
|
|
50
|
+
cmd.action(async (opts, command) => {
|
|
51
|
+
// Centralized surface read (the human/agent split in one place). Note we still
|
|
52
|
+
// pass the *raw* `opts.yes` to promptDriftSync below — it distinguishes an
|
|
53
|
+
// explicit `--yes` (act) from a non-TTY shell (report only), so `surface.assumeYes`
|
|
54
|
+
// (which conflates the two) would wrongly auto-reconcile in a plain pipe.
|
|
55
|
+
const surface = resolveSurface(command);
|
|
50
56
|
const cwd = opts.cwd ?? process.cwd();
|
|
51
|
-
if (
|
|
57
|
+
if (surface.json) {
|
|
52
58
|
const status = await computeSyncStatus({ cwd });
|
|
53
59
|
console.log(JSON.stringify(status, null, 2));
|
|
54
60
|
return;
|
|
@@ -53,7 +53,7 @@ When to use:
|
|
|
53
53
|
- Team sharing: distribute subagent definitions via GitHub repos
|
|
54
54
|
`);
|
|
55
55
|
// Shared list implementation, registered as `list` and hidden `view` alias.
|
|
56
|
-
const runList = async () => {
|
|
56
|
+
const runList = async (opts) => {
|
|
57
57
|
const rows = buildSubagentRows();
|
|
58
58
|
await showResourceList({
|
|
59
59
|
resourcePlural: 'subagents',
|
|
@@ -62,12 +62,14 @@ When to use:
|
|
|
62
62
|
rows,
|
|
63
63
|
emptyMessage: 'No subagents in ~/.agents/subagents/. Add one with: agents subagents add gh:user/repo',
|
|
64
64
|
centralPath: getSubagentsDir(),
|
|
65
|
+
json: opts?.json,
|
|
65
66
|
});
|
|
66
67
|
};
|
|
67
68
|
// agents subagents list
|
|
68
69
|
subagentsCmd
|
|
69
70
|
.command('list')
|
|
70
71
|
.description('Show subagents in a table with sync status across agent versions')
|
|
72
|
+
.option('--json', 'Emit machine-readable JSON instead of the table/picker')
|
|
71
73
|
.addHelpText('after', `
|
|
72
74
|
Examples:
|
|
73
75
|
# Interactive picker (TTY) or sync-status table (piped)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents uninstall` — completely remove agents-cli and restore the user's
|
|
3
|
+
* original agent configs. The reverse of `agents setup`.
|
|
4
|
+
*
|
|
5
|
+
* Thin command layer: the restore/teardown logic lives in `lib/uninstall.ts`
|
|
6
|
+
* (planUninstall / executeUninstall) so it can be tested against a real temp
|
|
7
|
+
* HOME without the CLI wrapper.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
/** Register `agents uninstall`. */
|
|
11
|
+
export declare function registerUninstallCommands(program: Command): void;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
import { planUninstall, executeUninstall } from '../lib/uninstall.js';
|
|
5
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
6
|
+
/** Render the read-only plan so the user sees exactly what will change. */
|
|
7
|
+
function printPlan(plan, purge) {
|
|
8
|
+
const restores = plan.configs.filter((c) => c.kind === 'restore-backup' || c.kind === 'restore-version-home');
|
|
9
|
+
const dangling = plan.configs.filter((c) => c.kind === 'remove-dangling');
|
|
10
|
+
const untouched = plan.configs.filter((c) => c.kind === 'leave-real' || c.kind === 'leave-foreign');
|
|
11
|
+
console.log(chalk.bold('\nagents uninstall — planned changes\n'));
|
|
12
|
+
if (restores.length > 0) {
|
|
13
|
+
console.log(chalk.green('Restore your original config (adopted by agents-cli):'));
|
|
14
|
+
for (const c of restores) {
|
|
15
|
+
const how = c.kind === 'restore-backup' ? 'from backup' : 'from version home';
|
|
16
|
+
console.log(chalk.gray(` ${c.realPath} ${chalk.dim(`(${how})`)}`));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (dangling.length > 0) {
|
|
20
|
+
console.log(chalk.yellow('Remove dangling symlink (no original found to restore):'));
|
|
21
|
+
for (const c of dangling)
|
|
22
|
+
console.log(chalk.gray(` ${c.realPath}`));
|
|
23
|
+
}
|
|
24
|
+
if (untouched.length > 0) {
|
|
25
|
+
console.log(chalk.cyan('Left untouched (real, un-adopted configs — never modified):'));
|
|
26
|
+
for (const c of untouched)
|
|
27
|
+
console.log(chalk.gray(` ${c.realPath}`));
|
|
28
|
+
}
|
|
29
|
+
if (plan.homeFiles.length > 0) {
|
|
30
|
+
console.log(chalk.green('Restore home files:'));
|
|
31
|
+
for (const hf of plan.homeFiles)
|
|
32
|
+
console.log(chalk.gray(` ${hf.realPath}`));
|
|
33
|
+
}
|
|
34
|
+
if (plan.launchers.length > 0) {
|
|
35
|
+
console.log(chalk.green('Release adopted launchers (restore native binaries on PATH):'));
|
|
36
|
+
console.log(chalk.gray(` ${plan.launchers.join(', ')}`));
|
|
37
|
+
}
|
|
38
|
+
if (plan.rcFiles.length > 0) {
|
|
39
|
+
console.log(chalk.green('Remove the shim directory from PATH in:'));
|
|
40
|
+
for (const rc of plan.rcFiles)
|
|
41
|
+
console.log(chalk.gray(` ${rc}`));
|
|
42
|
+
}
|
|
43
|
+
console.log(chalk.bold('\nData:'));
|
|
44
|
+
if (purge) {
|
|
45
|
+
console.log(chalk.red(` Permanently delete ${plan.agentsDir} (installed versions, session history, secrets metadata).`));
|
|
46
|
+
if (plan.legacySymlink)
|
|
47
|
+
console.log(chalk.red(` Permanently delete ${plan.legacySymlink}.`));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(chalk.gray(` Move ${plan.agentsDir} aside to ${plan.agentsDir}.removed-<timestamp> (recoverable).`));
|
|
51
|
+
if (plan.legacySymlink)
|
|
52
|
+
console.log(chalk.gray(` Remove the legacy symlink ${plan.legacySymlink}.`));
|
|
53
|
+
console.log(chalk.gray(' Use --purge to hard-delete instead.'));
|
|
54
|
+
}
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
/** Report what actually happened, then the final manual npm step. */
|
|
58
|
+
function printResult(result, cleanedPath) {
|
|
59
|
+
for (const r of result.restoredConfigs)
|
|
60
|
+
console.log(chalk.green(`Restored ${r.realPath}`));
|
|
61
|
+
for (const r of result.removedDanglingConfigs)
|
|
62
|
+
console.log(chalk.yellow(`Removed dangling symlink ${r.realPath}`));
|
|
63
|
+
for (const f of result.restoredHomeFiles)
|
|
64
|
+
console.log(chalk.green(`Restored ${f}`));
|
|
65
|
+
if (result.releasedLaunchers.length > 0)
|
|
66
|
+
console.log(chalk.green(`Released launchers: ${result.releasedLaunchers.join(', ')}`));
|
|
67
|
+
for (const rc of result.cleanedRcFiles)
|
|
68
|
+
console.log(chalk.green(`Cleaned shim PATH entry from ${rc}`));
|
|
69
|
+
if (result.agentsDir.disposition === 'moved') {
|
|
70
|
+
console.log(chalk.gray(`Moved ${result.agentsDir.path} to ${result.agentsDir.movedTo}`));
|
|
71
|
+
}
|
|
72
|
+
else if (result.agentsDir.disposition === 'purged') {
|
|
73
|
+
console.log(chalk.gray(`Deleted ${result.agentsDir.path}`));
|
|
74
|
+
}
|
|
75
|
+
for (const e of result.errors)
|
|
76
|
+
console.log(chalk.red(` ! ${e}`));
|
|
77
|
+
if (result.purgeDowngraded) {
|
|
78
|
+
console.log(chalk.yellow(`\n--purge was downgraded to move-aside because a restore step errored — ${result.agentsDir.path} was kept so nothing is lost. Resolve the errors above, then delete ${result.agentsDir.movedTo} manually.`));
|
|
79
|
+
}
|
|
80
|
+
console.log(chalk.bold('\nFinish by removing the CLI package:'));
|
|
81
|
+
console.log(chalk.gray(' npm uninstall -g @phnx-labs/agents-cli # or: bun remove -g @phnx-labs/agents-cli'));
|
|
82
|
+
if (cleanedPath) {
|
|
83
|
+
console.log(chalk.gray('Open a new shell (or re-source your rc file) so the removed PATH entry takes effect.'));
|
|
84
|
+
}
|
|
85
|
+
if (result.agentsDir.disposition === 'moved') {
|
|
86
|
+
console.log(chalk.gray(`Your data is still at ${result.agentsDir.movedTo} — delete it once you are sure.`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Register `agents uninstall`. */
|
|
90
|
+
export function registerUninstallCommands(program) {
|
|
91
|
+
const cmd = program
|
|
92
|
+
.command('uninstall')
|
|
93
|
+
.description('Completely remove agents-cli and restore your original agent configs. Reverses `agents setup`.')
|
|
94
|
+
.option('--purge', 'Hard-delete ~/.agents (installed versions, sessions, secrets metadata) instead of moving it aside')
|
|
95
|
+
.option('--dry-run', 'Show exactly what would change without modifying anything')
|
|
96
|
+
.option('-y, --yes', 'Skip the confirmation prompt (required to run non-interactively)');
|
|
97
|
+
setHelpSections(cmd, {
|
|
98
|
+
examples: `
|
|
99
|
+
# Preview everything that would change, without touching anything
|
|
100
|
+
agents uninstall --dry-run
|
|
101
|
+
|
|
102
|
+
# Restore your configs and move ~/.agents aside (recoverable)
|
|
103
|
+
agents uninstall
|
|
104
|
+
|
|
105
|
+
# Same, but hard-delete ~/.agents (no recovery)
|
|
106
|
+
agents uninstall --purge
|
|
107
|
+
`,
|
|
108
|
+
notes: `
|
|
109
|
+
- Restores every ~/.<agent> that agents-cli adopted; a real config it never adopted is left untouched.
|
|
110
|
+
- Releases adopted launchers and strips the shim directory from your shell PATH.
|
|
111
|
+
- Without --purge, ~/.agents is moved to ~/.agents.removed-<timestamp> so nothing is lost.
|
|
112
|
+
- The CLI cannot delete its own running binary; it prints the final 'npm uninstall -g' step for you.
|
|
113
|
+
`,
|
|
114
|
+
});
|
|
115
|
+
cmd.action(async (options) => {
|
|
116
|
+
// We are tearing ~/.agents down. Silence the JSONL audit log for the rest
|
|
117
|
+
// of this process so a late emit() (its events path is memoized to the old
|
|
118
|
+
// location) can't re-create ~/.agents after we move it aside.
|
|
119
|
+
process.env.AGENTS_DISABLE_EVENT_LOG = '1';
|
|
120
|
+
const plan = planUninstall();
|
|
121
|
+
if (!plan.isInstalled) {
|
|
122
|
+
console.log(chalk.gray('agents-cli is not set up (no ~/.agents directory) — nothing to uninstall.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
printPlan(plan, !!options.purge);
|
|
126
|
+
if (options.dryRun) {
|
|
127
|
+
console.log(chalk.gray('Dry run — nothing was changed.'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!options.yes) {
|
|
131
|
+
if (!isInteractiveTerminal()) {
|
|
132
|
+
console.log(chalk.red('Refusing to uninstall non-interactively. Re-run with --yes to confirm.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const ok = await confirm({
|
|
137
|
+
message: options.purge
|
|
138
|
+
? 'Permanently remove agents-cli and restore your original configs?'
|
|
139
|
+
: 'Remove agents-cli (data moved to a recoverable directory) and restore your original configs?',
|
|
140
|
+
default: false,
|
|
141
|
+
});
|
|
142
|
+
if (!ok) {
|
|
143
|
+
console.log(chalk.gray('Cancelled.'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (isPromptCancelled(err)) {
|
|
149
|
+
console.log(chalk.gray('Cancelled.'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const result = executeUninstall(plan, { purge: options.purge, timestamp: Date.now() });
|
|
156
|
+
printResult(result, result.cleanedRcFiles.length > 0);
|
|
157
|
+
});
|
|
158
|
+
}
|
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
|
*/
|