@event4u/agent-switch 1.5.1 → 1.6.1
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/dist/index.js +29 -10
- package/dist/profiles.js +31 -4
- package/dist/providers.js +16 -10
- package/dist/updates.js +30 -2
- package/package.json +1 -1
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/dist/updates.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
14
15
|
import * as path from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
/** The published npm package + the repo whose GitHub Releases are the update source. */
|
|
@@ -102,9 +103,36 @@ export async function checkForUpdate(repo = UPDATE_REPO) {
|
|
|
102
103
|
return { kind: "error", current, message: e instanceof Error ? e.message : String(e) };
|
|
103
104
|
}
|
|
104
105
|
}
|
|
106
|
+
/** Build a PATH that can locate `npm` even when this CLI was spawned from the
|
|
107
|
+
* GUI with a minimal environment PATH (`/usr/bin:/bin:/usr/sbin:/sbin`) — the
|
|
108
|
+
* case a macOS/Linux app launched from Finder or the menu bar inherits. `npm`
|
|
109
|
+
* always ships in the same bin directory as the running `node`
|
|
110
|
+
* (`process.execPath`), so that dir goes first; the current PATH and the common
|
|
111
|
+
* global-install locations follow as fallbacks. Mirrors the GUI's Rust PATH
|
|
112
|
+
* recovery so the deepest spawn (`npm`) resolves regardless of launch context.
|
|
113
|
+
* Pure — every source is injectable for tests. */
|
|
114
|
+
export function npmSearchPath(nodeDir = path.dirname(process.execPath), currentPath = process.env.PATH ?? "", home = os.homedir()) {
|
|
115
|
+
return [
|
|
116
|
+
nodeDir,
|
|
117
|
+
currentPath,
|
|
118
|
+
"/opt/homebrew/bin",
|
|
119
|
+
"/usr/local/bin",
|
|
120
|
+
path.join(home, ".npm-global", "bin"),
|
|
121
|
+
path.join(home, ".local", "bin"),
|
|
122
|
+
]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join(path.delimiter);
|
|
125
|
+
}
|
|
105
126
|
/** Self-update: reinstall the npm package at @latest (global). Streams npm's
|
|
106
|
-
* own output; returns its exit code (0 = success).
|
|
127
|
+
* own output; returns its exit code (0 = success). The spawn runs with a
|
|
128
|
+
* PATH that can always find `npm` ({@link npmSearchPath}), so a GUI-triggered
|
|
129
|
+
* update works from Finder/menu-bar launches with a stripped PATH; `shell` is
|
|
130
|
+
* required on Windows, where `npm` is a `.cmd` shim. */
|
|
107
131
|
export function selfUpdate() {
|
|
108
|
-
const r = spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
132
|
+
const r = spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
133
|
+
stdio: "inherit",
|
|
134
|
+
shell: process.platform === "win32",
|
|
135
|
+
env: { ...process.env, PATH: npmSearchPath() },
|
|
136
|
+
});
|
|
109
137
|
return r.status ?? 1;
|
|
110
138
|
}
|
package/package.json
CHANGED