@event4u/agent-switch 1.5.0 → 1.6.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 +11 -1
- package/dist/index.js +29 -10
- package/dist/profiles.js +31 -4
- package/dist/providers.js +16 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
<p align="center"><a href="https://event4u.app"><img alt="agent-switch" src=".github/assets/banner.png"></a></p>
|
|
2
|
+
|
|
1
3
|
# agent-switch
|
|
2
4
|
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@event4u/agent-switch"><img alt="npm" src="https://img.shields.io/npm/v/@event4u/agent-switch?style=flat-square&color=orange"></a>
|
|
7
|
+
<a href="https://event4u-app.github.io/agent-switch/"><img alt="Documentation" src="https://img.shields.io/badge/docs-event4u--app.github.io-orange?style=flat-square"></a>
|
|
8
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-orange?style=flat-square"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
3
11
|
Switch between multiple Claude Code accounts on macOS, Linux, and Windows with a single shell command — no repeated login/logout, no browser round-trips after the initial setup.
|
|
4
12
|
|
|
13
|
+
📖 **[Read the documentation →](https://event4u-app.github.io/agent-switch/)**
|
|
14
|
+
|
|
5
15
|
```
|
|
6
16
|
$ asw work # switch active account
|
|
7
17
|
$ claude # runs on the "work" account
|
|
@@ -19,7 +29,7 @@ Most naive switchers snapshot the macOS Keychain entry (`Claude Code-credentials
|
|
|
19
29
|
|
|
20
30
|
## Install
|
|
21
31
|
|
|
22
|
-
**npm / npx** (any OS — needs Node ≥
|
|
32
|
+
**npm / npx** (any OS — needs Node ≥ 20). The package is published as
|
|
23
33
|
`@event4u/agent-switch` (the bare name `agent-switch` on npm is an unrelated
|
|
24
34
|
project):
|
|
25
35
|
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
17
|
-
import { HOME, browserDir, configDir, die, ensureRoot, identity, listAllProfiles, listProfiles, migrateLegacyLayout, profileDir, profileExists, readJson, requireProfile, activeFor, setActive, labelFor, setLabel, clearLabel, renameProfile, isProfileLabel, PROFILE_LABELS, readAutoSwitch, readAutoSwitchAll, setAutoSwitch, readSwitchStrategy, setSwitchStrategy, readOsNotifications, setOsNotifications, readProviders, setProviderSurface, ROOT, } from "./profiles.js";
|
|
17
|
+
import { HOME, browserDir, configDir, die, ensureRoot, identity, listAllProfiles, listProfiles, migrateLegacyLayout, profileDir, profileExists, readJson, requireProfile, activeFor, setActive, labelFor, setLabel, clearLabel, renameProfile, isProfileLabel, PROFILE_LABELS, readAutoSwitch, readAutoSwitchAll, setAutoSwitch, readSwitchStrategy, setSwitchStrategy, readOsNotifications, setOsNotifications, readProviders, setProviderSurface, readBinaryPath, readBinaryPaths, setBinaryPath, ROOT, } from "./profiles.js";
|
|
18
18
|
import { PROVIDER_IDS, provider, isProviderInstalled, resolveBinary } from "./providers.js";
|
|
19
19
|
import { parseArgs, parseRun, resolveProviderValue } from "./args.js";
|
|
20
20
|
import { extractBrief, writeBrief, sweepBriefs, seedPrompt } from "./handoff.js";
|
|
@@ -59,9 +59,9 @@ function launch(p, name, args) {
|
|
|
59
59
|
ensureAgyKeychain(home);
|
|
60
60
|
env.CFFIXED_USER_HOME = home;
|
|
61
61
|
}
|
|
62
|
-
const res = spawnSync(resolveBinary(p.binary), args, { env, stdio: "inherit" });
|
|
62
|
+
const res = spawnSync(resolveBinary(p.binary, readBinaryPath(p.id)), args, { env, stdio: "inherit" });
|
|
63
63
|
if (res.error && res.error.code === "ENOENT") {
|
|
64
|
-
die(`\`${p.binary}\`
|
|
64
|
+
die(`\`${p.binary}\` not found. Install ${p.id}, or link its binary: agent-switch providers link --provider ${p.id} --path <path> (Providers tab in the GUI).`);
|
|
65
65
|
}
|
|
66
66
|
return res.status ?? 1;
|
|
67
67
|
}
|
|
@@ -952,7 +952,7 @@ function sessionsRmCodex(sessionId, flags, purge, json) {
|
|
|
952
952
|
const action = purge ? "delete" : "archive";
|
|
953
953
|
const p = provider("codex");
|
|
954
954
|
const env = { ...process.env, [p.envVar]: cfg };
|
|
955
|
-
const r = spawnSync(resolveBinary(p.binary), codexSessionCommand(action, sessionId), { env, encoding: "utf8" });
|
|
955
|
+
const r = spawnSync(resolveBinary(p.binary, readBinaryPath(p.id)), codexSessionCommand(action, sessionId), { env, encoding: "utf8" });
|
|
956
956
|
const out = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim();
|
|
957
957
|
if (/no .*session.*found/i.test(out))
|
|
958
958
|
die(`codex could not find session ${sessionId} in "${profile}": ${out}`);
|
|
@@ -990,7 +990,7 @@ function cmdSessionsRestore(providerId, providerExplicit, handle, flags = {}) {
|
|
|
990
990
|
const profile = requireProfile("codex", flags.from, "sessions restore --from");
|
|
991
991
|
const p = provider("codex");
|
|
992
992
|
const env = { ...process.env, [p.envVar]: configDir("codex", profile) };
|
|
993
|
-
const r = spawnSync(resolveBinary(p.binary), codexSessionCommand("unarchive", handle), { env, encoding: "utf8" });
|
|
993
|
+
const r = spawnSync(resolveBinary(p.binary, readBinaryPath(p.id)), codexSessionCommand("unarchive", handle), { env, encoding: "utf8" });
|
|
994
994
|
const out = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim();
|
|
995
995
|
if (/no .*session.*found/i.test(out))
|
|
996
996
|
die(`codex could not find an archived session ${handle} in "${profile}": ${out}`);
|
|
@@ -1351,22 +1351,41 @@ function cmdProviders(providerId, providerExplicit, mode, flags = {}) {
|
|
|
1351
1351
|
console.log(`${providerId}: cli ${cfg.cli ? "on" : "off"}, ui ${cfg.ui ? "on" : "off"}.`);
|
|
1352
1352
|
return;
|
|
1353
1353
|
}
|
|
1354
|
+
// Link the provider's CLI binary when it isn't on PATH — the user points us at
|
|
1355
|
+
// it (from the GUI Providers tab or here). `unlink` reverts to PATH resolution.
|
|
1356
|
+
if (mode === "link") {
|
|
1357
|
+
const binPath = typeof flags.path === "string" ? flags.path : undefined;
|
|
1358
|
+
if (!binPath)
|
|
1359
|
+
die(`usage: agent-switch providers link --provider ${providerId} --path <path-to-binary>`);
|
|
1360
|
+
if (!fs.existsSync(binPath))
|
|
1361
|
+
die(`no file at "${binPath}" — pass the full path to the ${provider(providerId).binary} binary.`);
|
|
1362
|
+
setBinaryPath(providerId, binPath);
|
|
1363
|
+
console.log(`Linked ${providerId} binary → ${binPath}`);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (mode === "unlink") {
|
|
1367
|
+
setBinaryPath(providerId, null);
|
|
1368
|
+
console.log(`Unlinked ${providerId} binary (back to PATH resolution).`);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1354
1371
|
if (mode === undefined || mode === "status") {
|
|
1355
1372
|
const all = readProviders();
|
|
1373
|
+
const links = readBinaryPaths();
|
|
1356
1374
|
if (flags.json) {
|
|
1357
|
-
// The GUI needs every provider's enabled surfaces
|
|
1358
|
-
//
|
|
1359
|
-
|
|
1375
|
+
// The GUI needs every provider's enabled surfaces, whether its binary is
|
|
1376
|
+
// usable (installed on PATH or linked), and any linked path — so it can
|
|
1377
|
+
// show, link, and enable a provider that isn't on PATH — all at once.
|
|
1378
|
+
const enriched = Object.fromEntries(PROVIDER_IDS.map((p) => [p, { ...all[p], installed: isProviderInstalled(p, links[p]), binaryPath: links[p] ?? null }]));
|
|
1360
1379
|
console.log(JSON.stringify(enriched));
|
|
1361
1380
|
return;
|
|
1362
1381
|
}
|
|
1363
1382
|
for (const p of providerExplicit ? [providerId] : PROVIDER_IDS) {
|
|
1364
|
-
const inst = isProviderInstalled(p) ? "installed" : "not installed";
|
|
1383
|
+
const inst = isProviderInstalled(p, links[p]) ? (links[p] ? `linked: ${links[p]}` : "installed") : "not installed";
|
|
1365
1384
|
console.log(`${p}: cli ${all[p].cli ? "on" : "off"}, ui ${all[p].ui ? "on" : "off"} (${inst}).`);
|
|
1366
1385
|
}
|
|
1367
1386
|
return;
|
|
1368
1387
|
}
|
|
1369
|
-
die("usage: agent-switch providers enable|disable|status [--provider P] [--surface cli|ui] [--json]");
|
|
1388
|
+
die("usage: agent-switch providers enable|disable|link|unlink|status [--provider P] [--surface cli|ui] [--path <bin>] [--json]");
|
|
1370
1389
|
}
|
|
1371
1390
|
/**
|
|
1372
1391
|
* Remove agent-switch's own footprint. Deletes every profile (incl. Claude
|
package/dist/profiles.js
CHANGED
|
@@ -147,27 +147,36 @@ function normalizeProvidersMap(raw) {
|
|
|
147
147
|
out[p] = normalizeSurfaces(r[p], p);
|
|
148
148
|
return out;
|
|
149
149
|
}
|
|
150
|
+
function normalizeBinaryPaths(raw) {
|
|
151
|
+
const r = (raw ?? {});
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const p of PROVIDER_IDS)
|
|
154
|
+
if (typeof r[p] === "string" && r[p])
|
|
155
|
+
out[p] = r[p];
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
150
158
|
export function readState() {
|
|
151
159
|
try {
|
|
152
160
|
const raw = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
153
161
|
const labels = normalizeLabels(raw?.labels);
|
|
154
162
|
const autoSwitch = normalizeAutoSwitchMap(raw?.autoSwitch);
|
|
155
163
|
const providers = normalizeProvidersMap(raw?.providers);
|
|
164
|
+
const binaryPaths = normalizeBinaryPaths(raw?.binaryPaths);
|
|
156
165
|
const switchStrategy = raw?.switchStrategy === "rotation-first" ? "rotation-first" : DEFAULT_SWITCH_STRATEGY;
|
|
157
166
|
const osNotifications = raw?.osNotifications === true;
|
|
158
167
|
// v1: { active: "<name>" } — a single Claude profile.
|
|
159
168
|
if (typeof raw?.active === "string") {
|
|
160
|
-
return { active: { ...emptyActive(), claude: raw.active }, labels, autoSwitch, providers, switchStrategy, osNotifications };
|
|
169
|
+
return { active: { ...emptyActive(), claude: raw.active }, labels, autoSwitch, providers, binaryPaths, switchStrategy, osNotifications };
|
|
161
170
|
}
|
|
162
171
|
if (raw?.active && typeof raw.active === "object") {
|
|
163
|
-
return { active: { ...emptyActive(), ...raw.active }, labels, autoSwitch, providers, switchStrategy, osNotifications };
|
|
172
|
+
return { active: { ...emptyActive(), ...raw.active }, labels, autoSwitch, providers, binaryPaths, switchStrategy, osNotifications };
|
|
164
173
|
}
|
|
165
|
-
return { active: emptyActive(), labels, autoSwitch, providers, switchStrategy, osNotifications };
|
|
174
|
+
return { active: emptyActive(), labels, autoSwitch, providers, binaryPaths, switchStrategy, osNotifications };
|
|
166
175
|
}
|
|
167
176
|
catch {
|
|
168
177
|
/* absent / unparsable → default */
|
|
169
178
|
}
|
|
170
|
-
return { active: emptyActive(), labels: {}, autoSwitch: emptyAutoSwitch(), providers: emptyProviders(), switchStrategy: DEFAULT_SWITCH_STRATEGY, osNotifications: false };
|
|
179
|
+
return { active: emptyActive(), labels: {}, autoSwitch: emptyAutoSwitch(), providers: emptyProviders(), binaryPaths: {}, switchStrategy: DEFAULT_SWITCH_STRATEGY, osNotifications: false };
|
|
171
180
|
}
|
|
172
181
|
export function readSwitchStrategy() {
|
|
173
182
|
return readState().switchStrategy;
|
|
@@ -292,6 +301,24 @@ export function setProviderSurface(providerId, surface, enabled) {
|
|
|
292
301
|
writeState(state);
|
|
293
302
|
return state.providers[providerId];
|
|
294
303
|
}
|
|
304
|
+
// ---------- linked binary paths (provider CLI not on PATH) ----------
|
|
305
|
+
/** The user-linked absolute path to a provider's CLI binary, or null. */
|
|
306
|
+
export function readBinaryPath(providerId) {
|
|
307
|
+
return readState().binaryPaths[providerId] ?? null;
|
|
308
|
+
}
|
|
309
|
+
/** All linked binary paths (for `providers status` / the GUI tab). */
|
|
310
|
+
export function readBinaryPaths() {
|
|
311
|
+
return readState().binaryPaths;
|
|
312
|
+
}
|
|
313
|
+
/** Link (set) or unlink (null) a provider's CLI binary path. */
|
|
314
|
+
export function setBinaryPath(providerId, binPath) {
|
|
315
|
+
const state = readState();
|
|
316
|
+
if (binPath)
|
|
317
|
+
state.binaryPaths[providerId] = binPath;
|
|
318
|
+
else
|
|
319
|
+
delete state.binaryPaths[providerId];
|
|
320
|
+
writeState(state);
|
|
321
|
+
}
|
|
295
322
|
// ---------- layout migration: v1 <ROOT>/<name> → <ROOT>/claude/<name> ----------
|
|
296
323
|
/**
|
|
297
324
|
* One-time migration of v1 Claude profiles (`<ROOT>/<name>/config`) into the
|
package/dist/providers.js
CHANGED
|
@@ -143,12 +143,15 @@ function localBinary(binary) {
|
|
|
143
143
|
return path.join(os.homedir(), ".local", "bin", binary);
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
|
-
* Resolve a CLI binary to an invocable command:
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
146
|
+
* Resolve a CLI binary to an invocable command: a user-linked path when set and
|
|
147
|
+
* present (the CLI is not on PATH but the user pointed us at it), otherwise its
|
|
148
|
+
* bare name when it is on PATH, otherwise its ~/.local/bin path when the binary
|
|
149
|
+
* lives there (GUI apps often run with a minimal PATH that omits it), otherwise
|
|
150
|
+
* the bare name so the caller surfaces a clean "not found" error.
|
|
150
151
|
*/
|
|
151
|
-
export function resolveBinary(binary) {
|
|
152
|
+
export function resolveBinary(binary, linked) {
|
|
153
|
+
if (linked && fs.existsSync(linked))
|
|
154
|
+
return linked;
|
|
152
155
|
const probe = spawnSync(binary, ["--version"], { stdio: "ignore", timeout: 5000 });
|
|
153
156
|
if (probe.error?.code !== "ENOENT")
|
|
154
157
|
return binary;
|
|
@@ -156,12 +159,15 @@ export function resolveBinary(binary) {
|
|
|
156
159
|
return fs.existsSync(local) ? local : binary;
|
|
157
160
|
}
|
|
158
161
|
/**
|
|
159
|
-
* Best-effort: is this provider's CLI binary
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
162
|
+
* Best-effort: is this provider's CLI binary usable? True when a user-linked
|
|
163
|
+
* path is set and present, else when `<binary> --version` resolves on PATH
|
|
164
|
+
* (only ENOENT counts as not-installed — a non-zero exit or a timeout still
|
|
165
|
+
* means the binary exists), else when it lives in ~/.local/bin. Used to gate
|
|
166
|
+
* enabling a provider the user hasn't installed (or hasn't linked).
|
|
163
167
|
*/
|
|
164
|
-
export function isProviderInstalled(id) {
|
|
168
|
+
export function isProviderInstalled(id, linked) {
|
|
169
|
+
if (linked && fs.existsSync(linked))
|
|
170
|
+
return true;
|
|
165
171
|
const bin = PROVIDERS[id].binary;
|
|
166
172
|
const probe = spawnSync(bin, ["--version"], { stdio: "ignore", timeout: 5000 });
|
|
167
173
|
if (probe.error?.code !== "ENOENT")
|
package/package.json
CHANGED