@event4u/agent-switch 1.6.1 → 1.7.0
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/README.md +2 -0
- package/dist/args.js +1 -0
- package/dist/doctor.js +7 -0
- package/dist/index.js +68 -0
- package/dist/tooling.js +324 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ Most naive switchers snapshot the macOS Keychain entry (`Claude Code-credentials
|
|
|
27
27
|
|
|
28
28
|
`agent-switch` uses **`CLAUDE_CONFIG_DIR` isolation** instead. Each profile gets its own config directory under `~/.agent-switch/<name>/config`. Claude Code derives a *separate* keychain entry per config dir, so every account stays logged in **live and independently**. Switching only changes which directory new `claude` invocations point at — nothing is snapshotted, nothing goes stale, and each account logs in via browser exactly once.
|
|
29
29
|
|
|
30
|
+
The same restraint governs the whole tool: no proxy, no traffic interception, no fingerprinting, no prompt rewriting — see [NON-GOALS.md](NON-GOALS.md) for the explicit list and the alternatives this ecosystem offers instead.
|
|
31
|
+
|
|
30
32
|
## Install
|
|
31
33
|
|
|
32
34
|
**npm / npx** (any OS — needs Node ≥ 20). The package is published as
|
package/dist/args.js
CHANGED
package/dist/doctor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { allProviders } from "./providers.js";
|
|
|
12
12
|
import { credentialStore } from "./credentials.js";
|
|
13
13
|
import { sharedLinkHealth } from "./share.js";
|
|
14
14
|
import { checkAuth } from "./api.js";
|
|
15
|
+
import { checkTooling, doctorToolingLine } from "./tooling.js";
|
|
15
16
|
const OK = "✅";
|
|
16
17
|
const WARN = "⚠️";
|
|
17
18
|
const ERR = "❌";
|
|
@@ -112,6 +113,12 @@ export async function runDoctor() {
|
|
|
112
113
|
line(WARN, `claude/${n}: ${parts.join("; ")} — run \`agent-switch share sync\`.`);
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
// Ecosystem tooling (agent-config + rtk) — same detection module as
|
|
117
|
+
// `agent-switch tooling`, never a second implementation. Both tools are
|
|
118
|
+
// optional, so an unhealthy row is a WARN, never a blocking error.
|
|
119
|
+
for (const t of checkTooling({ ids: ["agent-config", "rtk"] })) {
|
|
120
|
+
line(t.healthy ? OK : WARN, doctorToolingLine(t));
|
|
121
|
+
}
|
|
115
122
|
console.log(`\n profile root: ${ROOT}`);
|
|
116
123
|
console.log("");
|
|
117
124
|
line(hardError ? ERR : OK, hardError ? "doctor found a blocking problem (see above)." : "no blocking problems.");
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ import { withProperLock } from "./locks.js";
|
|
|
23
23
|
import { applySharing, removeSharing, syncSharing, sharedLinkHealth } from "./share.js";
|
|
24
24
|
import { detectShell, shellenvScript } from "./shellenv.js";
|
|
25
25
|
import { runDoctor } from "./doctor.js";
|
|
26
|
+
import { TOOL_IDS, checkTooling, formatToolingLines, runToolAction } from "./tooling.js";
|
|
27
|
+
import { readHistory } from "./history.js";
|
|
26
28
|
import { launchGui } from "./gui-launch.js";
|
|
27
29
|
import { checkForUpdate, selfUpdate } from "./updates.js";
|
|
28
30
|
import { mappingRows, pruneMappings, removeMapping, resolveMapping, setMapping, } from "./mappings.js";
|
|
@@ -1623,6 +1625,60 @@ function cmdOpen(appId, name) {
|
|
|
1623
1625
|
function cmdShellenv(shellArg) {
|
|
1624
1626
|
console.log(shellenvScript(detectShell(shellArg)));
|
|
1625
1627
|
}
|
|
1628
|
+
/** `agent-switch tooling` — ecosystem tool readout (agent-config, rtk, provider
|
|
1629
|
+
* CLIs). `--json` is the GUI IPC contract: the GUI renders this readout and
|
|
1630
|
+
* never shells out to detect on its own. */
|
|
1631
|
+
function cmdTooling(json = false) {
|
|
1632
|
+
const tools = checkTooling();
|
|
1633
|
+
if (json) {
|
|
1634
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
console.log(`agent-switch tooling — platform ${process.platform}\n`);
|
|
1638
|
+
for (const l of formatToolingLines(tools))
|
|
1639
|
+
console.log(l);
|
|
1640
|
+
}
|
|
1641
|
+
/** `agent-switch tooling install|upgrade <id>` — run the tool's per-platform
|
|
1642
|
+
* install/upgrade command as a visible child process (the GUI runs this inside
|
|
1643
|
+
* its embedded terminal). Exit code = the child's exit code. */
|
|
1644
|
+
function cmdToolingAction(action, id) {
|
|
1645
|
+
if (!id || !TOOL_IDS.includes(id)) {
|
|
1646
|
+
die(`usage: agent-switch tooling ${action} <${TOOL_IDS.join("|")}>`);
|
|
1647
|
+
}
|
|
1648
|
+
process.exit(runToolAction(id, action));
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* `agent-switch usage history [--provider claude] [--profile <name>] [--json]`
|
|
1652
|
+
* — the daemon-recorded usage-history ring per profile (same file the daemon
|
|
1653
|
+
* appends: `<profileDir>/usage-history.json`). `--json` is the GUI contract:
|
|
1654
|
+
* `[{ profile, samples: [{ at, windows }] }]`. Own-profile data only.
|
|
1655
|
+
*/
|
|
1656
|
+
function cmdUsageHistory(providerId, sub, flags = {}) {
|
|
1657
|
+
if (sub !== "history")
|
|
1658
|
+
die("usage: agent-switch usage history [--provider P] [--profile <name>] [--json]");
|
|
1659
|
+
const profiles = typeof flags.profile === "string"
|
|
1660
|
+
? [requireProfile(providerId, flags.profile, "usage history --profile")]
|
|
1661
|
+
: listProfiles(providerId);
|
|
1662
|
+
if (profiles.length === 0)
|
|
1663
|
+
die(`no ${providerId} profiles`);
|
|
1664
|
+
const rows = profiles.map((p) => ({
|
|
1665
|
+
profile: p,
|
|
1666
|
+
samples: readHistory(path.join(profileDir(providerId, p), "usage-history.json")),
|
|
1667
|
+
}));
|
|
1668
|
+
if (flags.json) {
|
|
1669
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
for (const r of rows) {
|
|
1673
|
+
if (r.samples.length === 0) {
|
|
1674
|
+
console.log(`${providerId}/${r.profile}: no usage history yet (the daemon records samples — agent-switch service start)`);
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
const first = r.samples[0].at;
|
|
1678
|
+
const last = r.samples[r.samples.length - 1].at;
|
|
1679
|
+
console.log(`${providerId}/${r.profile}: ${r.samples.length} sample${r.samples.length === 1 ? "" : "s"} (${first} → ${last})`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1626
1682
|
function usage() {
|
|
1627
1683
|
console.log(`agent-switch — switch accounts for Claude Code, Codex, and Antigravity (macOS · Linux · Windows)
|
|
1628
1684
|
|
|
@@ -1665,6 +1721,9 @@ Provider defaults to claude; pass --provider codex|antigravity for the others.
|
|
|
1665
1721
|
agent-switch notify --kind K --title T --message M [--json] record a notification event
|
|
1666
1722
|
agent-switch os-notify [on|off|status] [--json] daemon-side OS desktop notifications (default off)
|
|
1667
1723
|
agent-switch uninstall [--force] remove all agent-switch data + daemon
|
|
1724
|
+
agent-switch usage history [--provider P] [--profile <name>] [--json] daemon-recorded usage samples per profile
|
|
1725
|
+
agent-switch tooling [--json] ecosystem tool readout (agent-config, rtk, provider CLIs)
|
|
1726
|
+
agent-switch tooling install|upgrade <tool> install/upgrade an ecosystem tool (agent-config, rtk, claude, codex)
|
|
1668
1727
|
agent-switch doctor per-OS, per-provider self-check`);
|
|
1669
1728
|
}
|
|
1670
1729
|
// ---------- main -------------------------------------------------------------
|
|
@@ -1743,6 +1802,15 @@ async function main() {
|
|
|
1743
1802
|
case "rm": return cmdRemove(providerId, positional[0], !!flags.force);
|
|
1744
1803
|
case "shellenv": return cmdShellenv(flags.shell ?? positional[0]);
|
|
1745
1804
|
case "service": return cmdService(positional[0]);
|
|
1805
|
+
case "usage": return cmdUsageHistory(providerId, positional[0], flags);
|
|
1806
|
+
case "tooling":
|
|
1807
|
+
if (positional[0] === "install" || positional[0] === "upgrade") {
|
|
1808
|
+
return cmdToolingAction(positional[0], positional[1]);
|
|
1809
|
+
}
|
|
1810
|
+
if (positional[0] !== undefined) {
|
|
1811
|
+
die(`usage: agent-switch tooling [install|upgrade <${TOOL_IDS.join("|")}>] [--json]`);
|
|
1812
|
+
}
|
|
1813
|
+
return cmdTooling(!!flags.json);
|
|
1746
1814
|
case "doctor": return process.exit(await runDoctor());
|
|
1747
1815
|
case "help":
|
|
1748
1816
|
case "--help":
|
package/dist/tooling.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ecosystem tooling detection — the single source behind `agent-switch tooling`
|
|
3
|
+
* and the agent-config/rtk rows in `agent-switch doctor` (one implementation,
|
|
4
|
+
* two renderers; the GUI consumes `tooling --json` and never shells out).
|
|
5
|
+
*
|
|
6
|
+
* Covers agent-config, rtk, and the provider CLIs (claude, codex, agy).
|
|
7
|
+
* Detection is read-only: it runs local binaries with a short timeout and
|
|
8
|
+
* discards their output after classification; no network calls.
|
|
9
|
+
*
|
|
10
|
+
* rtk is special — upstream documents a hard name collision with an unrelated
|
|
11
|
+
* `rtk` (Rust Type Kit) and names `rtk gain` as the discriminator. Identity is
|
|
12
|
+
* judged on the probe's OUTPUT SIGNATURE, not its exit code (upstream defines
|
|
13
|
+
* no exit-code contract for `rtk gain`): a broken right tool is not the wrong
|
|
14
|
+
* tool, so a timed-out/crashed/ambiguous probe is `unverified`, never
|
|
15
|
+
* `unknown-rtk`.
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { readBinaryPath } from "./profiles.js";
|
|
21
|
+
import { npmSearchPath } from "./updates.js";
|
|
22
|
+
export const TOOL_IDS = ["agent-config", "rtk", "claude", "codex", "agy"];
|
|
23
|
+
/** Which provider a probed binary belongs to (for `providers link` hints and
|
|
24
|
+
* the user-linked path lookup). */
|
|
25
|
+
const BINARY_PROVIDER = {
|
|
26
|
+
claude: "claude",
|
|
27
|
+
codex: "codex",
|
|
28
|
+
agy: "antigravity",
|
|
29
|
+
};
|
|
30
|
+
/** Probe timeouts: `rtk gain` is the identity discriminator and measured at
|
|
31
|
+
* ~666 ms warm (S0.2), so 1500 ms is headroom, not hope; `--version` probes
|
|
32
|
+
* get a little more because a cold Node CLI start can exceed 1.5 s. */
|
|
33
|
+
export const RTK_PROBE_TIMEOUT_MS = 1500;
|
|
34
|
+
const VERSION_PROBE_TIMEOUT_MS = 3000;
|
|
35
|
+
/** spawnSync wrapper. Runs with {@link toolingSearchPath} so probes work when
|
|
36
|
+
* this CLI was spawned from the GUI with a stripped PATH (Finder/menu-bar
|
|
37
|
+
* launches); `shell` on Windows for `.cmd` shims (same as selfUpdate). */
|
|
38
|
+
export function defaultRunner(cmd, args, timeoutMs) {
|
|
39
|
+
const r = spawnSync(cmd, args, {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
timeout: timeoutMs,
|
|
42
|
+
env: { ...process.env, PATH: toolingSearchPath() },
|
|
43
|
+
shell: process.platform === "win32",
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
const err = r.error;
|
|
47
|
+
const timedOut = err?.code === "ETIMEDOUT" || r.signal === "SIGTERM";
|
|
48
|
+
return {
|
|
49
|
+
stdout: r.stdout ?? "",
|
|
50
|
+
stderr: r.stderr ?? "",
|
|
51
|
+
status: r.status,
|
|
52
|
+
failedToStart: !!err && !timedOut,
|
|
53
|
+
timedOut,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ---------- PATH resolution ---------------------------------------------------
|
|
57
|
+
/** The PATH the tooling sweep searches: {@link npmSearchPath} already covers
|
|
58
|
+
* the stripped-GUI-PATH case (node's own bin dir first, then the inherited
|
|
59
|
+
* PATH, homebrew, /usr/local/bin, ~/.npm-global/bin, and ~/.local/bin — where
|
|
60
|
+
* rtk's upstream install.sh and agy's installer land). */
|
|
61
|
+
export function toolingSearchPath() {
|
|
62
|
+
return npmSearchPath();
|
|
63
|
+
}
|
|
64
|
+
/** Locate a binary on a search PATH without spawning it. Pure fs, injectable
|
|
65
|
+
* platform — on Windows, tries the executable extensions; on POSIX, requires
|
|
66
|
+
* the executable bit so a stray data file named `rtk` doesn't count. */
|
|
67
|
+
export function findOnPath(binary, searchPath = toolingSearchPath(), platform = process.platform) {
|
|
68
|
+
const exts = platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
69
|
+
for (const dir of searchPath.split(path.delimiter)) {
|
|
70
|
+
if (!dir)
|
|
71
|
+
continue;
|
|
72
|
+
for (const ext of exts) {
|
|
73
|
+
const candidate = path.join(dir, binary + ext);
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.statSync(candidate).isFile())
|
|
76
|
+
continue;
|
|
77
|
+
if (platform !== "win32")
|
|
78
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
79
|
+
return candidate;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* keep looking */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** First version-looking token in a probe's output (`rtk 1.4.2` → `1.4.2`,
|
|
89
|
+
* `v2.0.1-beta.3` → `2.0.1-beta.3`); null when there is none. */
|
|
90
|
+
export function parseVersionToken(s) {
|
|
91
|
+
return /\bv?(\d+\.\d+\.\d+[-+.\w]*)/.exec(s)?.[1] ?? null;
|
|
92
|
+
}
|
|
93
|
+
// ---------- rtk identity probe ------------------------------------------------
|
|
94
|
+
/** `rtk gain` output header of the real Token Killer (S0.2 measured signature). */
|
|
95
|
+
const RTK_SIGNATURE = /RTK Token Savings|Token Savings/;
|
|
96
|
+
/**
|
|
97
|
+
* The rtk identity probe — THE seam for the planned delegation: when
|
|
98
|
+
* agent-config ships its rtk detection contract (AC-side
|
|
99
|
+
* road-to-rtk-onboarding-correctness Phase 3), delegation replaces this
|
|
100
|
+
* function's body with a mapping over that readout; everything else in this
|
|
101
|
+
* module stays. Today's implementation is the documented fallback probe.
|
|
102
|
+
*
|
|
103
|
+
* Semantics (output signature, never exit code):
|
|
104
|
+
* - stdout carries the Token Savings header → `token-killer` (+ version via
|
|
105
|
+
* `rtk --version`, which is not identity-bearing);
|
|
106
|
+
* - the probe ran and produced other output (unknown-subcommand error etc.)
|
|
107
|
+
* → `unknown-rtk` (the name-collision case);
|
|
108
|
+
* - timeout, crash-on-start, or a silent run → `unverified` (a broken right
|
|
109
|
+
* tool is not the wrong tool).
|
|
110
|
+
*/
|
|
111
|
+
export function probeRtkIdentity(rtkCmd, run = defaultRunner) {
|
|
112
|
+
const gain = run(rtkCmd, ["gain"], RTK_PROBE_TIMEOUT_MS);
|
|
113
|
+
if (gain.timedOut || gain.failedToStart)
|
|
114
|
+
return { identity: "unverified", version: null };
|
|
115
|
+
if (RTK_SIGNATURE.test(gain.stdout)) {
|
|
116
|
+
const v = run(rtkCmd, ["--version"], VERSION_PROBE_TIMEOUT_MS);
|
|
117
|
+
return { identity: "token-killer", version: parseVersionToken(v.stdout) };
|
|
118
|
+
}
|
|
119
|
+
if (`${gain.stdout}${gain.stderr}`.trim().length > 0)
|
|
120
|
+
return { identity: "unknown-rtk", version: null };
|
|
121
|
+
return { identity: "unverified", version: null }; // ran silent — ambiguous
|
|
122
|
+
}
|
|
123
|
+
// ---------- hints ---------------------------------------------------------------
|
|
124
|
+
function installHint(id, platform) {
|
|
125
|
+
switch (id) {
|
|
126
|
+
case "agent-config":
|
|
127
|
+
return "not installed — install: `npm install -g @event4u/agent-config`";
|
|
128
|
+
case "rtk":
|
|
129
|
+
if (platform === "darwin")
|
|
130
|
+
return "not installed — install: `brew install rtk`";
|
|
131
|
+
if (platform === "win32")
|
|
132
|
+
return "not installed — install: `winget install rtk-ai.rtk`";
|
|
133
|
+
return "not installed — install: `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh`";
|
|
134
|
+
default:
|
|
135
|
+
return `not installed — install the ${id} CLI, or link it: \`agent-switch providers link --provider ${BINARY_PROVIDER[id]} --path <path-to-binary>\``;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function rtkUnhealthyHint(identity, platform) {
|
|
139
|
+
if (identity === "unknown-rtk") {
|
|
140
|
+
// installHint's tail carries the right per-OS install command.
|
|
141
|
+
return `the \`rtk\` on PATH is not Token Killer (name collision) — ${installHint("rtk", platform).replace("not installed — ", "")}`;
|
|
142
|
+
}
|
|
143
|
+
return "could not verify `rtk` (probe timed out or crashed) — run `rtk gain` manually to check";
|
|
144
|
+
}
|
|
145
|
+
/** One status entry for a tool that is not on the search PATH. Absence carries
|
|
146
|
+
* no `identity` — there was no binary to probe, so there is nothing to judge. */
|
|
147
|
+
function absent(id, platform) {
|
|
148
|
+
return { id, present: false, version: null, path: null, healthy: false, hint: installHint(id, platform) };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Detect every requested tool. Absence is encoded as `present: false`;
|
|
152
|
+
* `identity` appears only for rtk; `healthy` for rtk is true only for
|
|
153
|
+
* identity `token-killer`, for everything else when its `--version` probe ran.
|
|
154
|
+
*/
|
|
155
|
+
export function checkTooling(opts = {}) {
|
|
156
|
+
const ids = opts.ids ?? TOOL_IDS;
|
|
157
|
+
const run = opts.run ?? defaultRunner;
|
|
158
|
+
const searchPath = opts.searchPath ?? toolingSearchPath();
|
|
159
|
+
const platform = opts.platform ?? process.platform;
|
|
160
|
+
const linked = opts.linked ?? readBinaryPath;
|
|
161
|
+
return ids.map((id) => {
|
|
162
|
+
// Provider binaries honor a user-linked path first (same precedence as
|
|
163
|
+
// resolveBinary: linked > PATH, with ~/.local/bin inside the search PATH).
|
|
164
|
+
const providerId = BINARY_PROVIDER[id];
|
|
165
|
+
const linkedPath = providerId ? linked(providerId) : null;
|
|
166
|
+
const binPath = linkedPath && fs.existsSync(linkedPath) ? linkedPath : findOnPath(id, searchPath, platform);
|
|
167
|
+
if (!binPath)
|
|
168
|
+
return absent(id, platform);
|
|
169
|
+
if (id === "rtk") {
|
|
170
|
+
const { identity, version } = probeRtkIdentity(binPath, run);
|
|
171
|
+
return {
|
|
172
|
+
id,
|
|
173
|
+
present: true,
|
|
174
|
+
version,
|
|
175
|
+
path: binPath,
|
|
176
|
+
healthy: identity === "token-killer",
|
|
177
|
+
identity,
|
|
178
|
+
hint: identity === "token-killer" ? "" : rtkUnhealthyHint(identity, platform),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const probe = run(binPath, ["--version"], VERSION_PROBE_TIMEOUT_MS);
|
|
182
|
+
const healthy = !probe.failedToStart && !probe.timedOut;
|
|
183
|
+
return {
|
|
184
|
+
id,
|
|
185
|
+
present: true,
|
|
186
|
+
version: healthy ? parseVersionToken(probe.stdout) : null,
|
|
187
|
+
path: binPath,
|
|
188
|
+
healthy,
|
|
189
|
+
hint: healthy ? "" : `\`${id} --version\` ${probe.timedOut ? "timed out" : "failed to run"} — reinstall it (${installHint(id, platform).replace("not installed — ", "")})`,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** npm packages behind the npm-installed tools (same commands the hints cite). */
|
|
194
|
+
const NPM_PACKAGES = {
|
|
195
|
+
"agent-config": "@event4u/agent-config",
|
|
196
|
+
claude: "@anthropic-ai/claude-code",
|
|
197
|
+
codex: "@openai/codex",
|
|
198
|
+
};
|
|
199
|
+
/** rtk's upstream installer (linux — used for install AND upgrade there).
|
|
200
|
+
* NEVER `cargo install rtk`: that builds the unrelated Rust Type Kit. */
|
|
201
|
+
const RTK_INSTALL_SH = "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh";
|
|
202
|
+
function npmCommand(pkg, latest) {
|
|
203
|
+
const spec = latest ? `${pkg}@latest` : pkg;
|
|
204
|
+
return { cmd: "npm", args: ["install", "-g", spec], display: `npm install -g ${spec}` };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The exact command for `tooling install|upgrade <id>` — pure, per-platform.
|
|
208
|
+
* A `refusal` result means there is no honest command to run (today: agy —
|
|
209
|
+
* the binary ships with the Antigravity app; this repo documents no standalone
|
|
210
|
+
* installer, and inventing one would be worse than saying so).
|
|
211
|
+
*
|
|
212
|
+
* `claudePath` (resolved linked-or-PATH binary) selects the claude upgrade
|
|
213
|
+
* shape: the CLI's own `claude update` when the binary is present, else the
|
|
214
|
+
* npm `@latest` reinstall.
|
|
215
|
+
*/
|
|
216
|
+
export function planToolAction(id, action, opts = {}) {
|
|
217
|
+
const platform = opts.platform ?? process.platform;
|
|
218
|
+
switch (id) {
|
|
219
|
+
case "agent-config":
|
|
220
|
+
return { command: npmCommand(NPM_PACKAGES["agent-config"], action === "upgrade") };
|
|
221
|
+
case "codex":
|
|
222
|
+
return { command: npmCommand(NPM_PACKAGES.codex, action === "upgrade") };
|
|
223
|
+
case "claude":
|
|
224
|
+
if (action === "upgrade" && opts.claudePath) {
|
|
225
|
+
return { command: { cmd: opts.claudePath, args: ["update"], display: "claude update" } };
|
|
226
|
+
}
|
|
227
|
+
return { command: npmCommand(NPM_PACKAGES.claude, action === "upgrade") };
|
|
228
|
+
case "rtk": {
|
|
229
|
+
if (platform === "darwin") {
|
|
230
|
+
const verb = action === "upgrade" ? "upgrade" : "install";
|
|
231
|
+
return { command: { cmd: "brew", args: [verb, "rtk"], display: `brew ${verb} rtk` } };
|
|
232
|
+
}
|
|
233
|
+
if (platform === "win32") {
|
|
234
|
+
return { command: { cmd: "winget", args: [action, "rtk-ai.rtk"], display: `winget ${action} rtk-ai.rtk` } };
|
|
235
|
+
}
|
|
236
|
+
// linux (and anything else POSIX): the upstream installer covers both actions.
|
|
237
|
+
return { command: { cmd: "sh", args: ["-c", RTK_INSTALL_SH], display: RTK_INSTALL_SH } };
|
|
238
|
+
}
|
|
239
|
+
case "agy":
|
|
240
|
+
return {
|
|
241
|
+
refusal: "`agy` has no standalone installer — the Antigravity CLI ships with the Antigravity app.\n" +
|
|
242
|
+
"Install the app, then (if `agy` is not on PATH) link the binary:\n" +
|
|
243
|
+
" agent-switch providers link --provider antigravity --path <path-to-agy>",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** The real runner: inherits stdio (the GUI runs this CLI inside its embedded
|
|
248
|
+
* terminal, so the user watches npm/brew output live), searches
|
|
249
|
+
* {@link toolingSearchPath} so a GUI-spawned run finds npm/brew on a stripped
|
|
250
|
+
* PATH, and uses `shell` on Windows for `.cmd` shims (same as selfUpdate). */
|
|
251
|
+
export function defaultInstallRunner(cmd, args) {
|
|
252
|
+
const r = spawnSync(cmd, args, {
|
|
253
|
+
stdio: "inherit",
|
|
254
|
+
env: { ...process.env, PATH: toolingSearchPath() },
|
|
255
|
+
shell: process.platform === "win32",
|
|
256
|
+
});
|
|
257
|
+
if (r.error?.code === "ENOENT") {
|
|
258
|
+
console.error(`❌ \`${cmd}\` not found on PATH — install it first, then retry.`);
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
return r.status ?? 1;
|
|
262
|
+
}
|
|
263
|
+
/** The claude binary `tooling upgrade claude` prefers: linked path first (same
|
|
264
|
+
* precedence as resolveBinary), else PATH. Null → npm fallback. */
|
|
265
|
+
function resolveClaudeForUpdate() {
|
|
266
|
+
const linked = readBinaryPath("claude");
|
|
267
|
+
if (linked && fs.existsSync(linked))
|
|
268
|
+
return linked;
|
|
269
|
+
return findOnPath("claude");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `agent-switch tooling install|upgrade <id>` — plan the per-platform command,
|
|
273
|
+
* print it (transparency: the user sees exactly what runs), run it with
|
|
274
|
+
* inherited stdio, and return the child's exit code. A refusal (agy) prints
|
|
275
|
+
* the explanation and returns 1 without running anything.
|
|
276
|
+
*/
|
|
277
|
+
export function runToolAction(id, action, opts = {}) {
|
|
278
|
+
const log = opts.log ?? console.log;
|
|
279
|
+
const error = opts.error ?? console.error;
|
|
280
|
+
const claudePath = opts.claudePath !== undefined
|
|
281
|
+
? opts.claudePath
|
|
282
|
+
: id === "claude" && action === "upgrade"
|
|
283
|
+
? resolveClaudeForUpdate()
|
|
284
|
+
: null;
|
|
285
|
+
const plan = planToolAction(id, action, { platform: opts.platform, claudePath });
|
|
286
|
+
if ("refusal" in plan) {
|
|
287
|
+
error(plan.refusal);
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
log(`→ ${plan.command.display}`);
|
|
291
|
+
const status = (opts.run ?? defaultInstallRunner)(plan.command.cmd, plan.command.args);
|
|
292
|
+
if (status === 0) {
|
|
293
|
+
log(`✅ ${id} ${action === "upgrade" ? "upgraded" : "installed"} — verify with \`agent-switch tooling\`.`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
error(`❌ \`${plan.command.display}\` exited with ${status} (its output is above).`);
|
|
297
|
+
}
|
|
298
|
+
return status;
|
|
299
|
+
}
|
|
300
|
+
// ---------- renderers -----------------------------------------------------------
|
|
301
|
+
export function statusGlyph(t) {
|
|
302
|
+
if (t.healthy)
|
|
303
|
+
return "✅";
|
|
304
|
+
return t.present ? "⚠️" : "❌";
|
|
305
|
+
}
|
|
306
|
+
/** Aligned human rows for `agent-switch tooling` (glyph, id, version, detail). */
|
|
307
|
+
export function formatToolingLines(tools) {
|
|
308
|
+
const idW = Math.max(...tools.map((t) => t.id.length));
|
|
309
|
+
const verW = Math.max(...tools.map((t) => (t.version ?? "—").length));
|
|
310
|
+
return tools.map((t) => {
|
|
311
|
+
const detail = t.healthy
|
|
312
|
+
? `${t.path}${t.identity === "token-killer" ? " (Token Killer verified)" : ""}`
|
|
313
|
+
: t.hint;
|
|
314
|
+
return `${statusGlyph(t)} ${t.id.padEnd(idW)} ${(t.version ?? "—").padEnd(verW)} ${detail}`.trimEnd();
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/** One doctor-style message per tool (doctor prefixes its own OK/WARN mark).
|
|
318
|
+
* Never a hard error — agent-config and rtk are optional ecosystem tools. */
|
|
319
|
+
export function doctorToolingLine(t) {
|
|
320
|
+
if (t.healthy) {
|
|
321
|
+
return `\`${t.id}\`${t.version ? ` ${t.version}` : ""} is installed${t.identity === "token-killer" ? " (Token Killer verified)" : ""}.`;
|
|
322
|
+
}
|
|
323
|
+
return `\`${t.id}\`: ${t.hint}`;
|
|
324
|
+
}
|
package/package.json
CHANGED