@event4u/agent-switch 2.0.0 → 2.2.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 +170 -68
- package/dist/activity.js +97 -0
- package/dist/args.js +23 -3
- package/dist/auth-status.js +149 -0
- package/dist/balance.js +52 -0
- package/dist/browsers.js +90 -0
- package/dist/config-transfer.js +12 -2
- package/dist/daemon.js +89 -38
- package/dist/desktop-sessions.js +115 -0
- package/dist/dir-resolve.js +151 -0
- package/dist/doctor.js +209 -52
- package/dist/gui-settings.js +67 -0
- package/dist/hooks.js +21 -0
- package/dist/index.js +921 -71
- package/dist/mappings.js +48 -9
- package/dist/pools.js +144 -0
- package/dist/profiles.js +84 -5
- package/dist/protection.js +93 -0
- package/dist/providers.js +3 -0
- package/dist/rebind.js +108 -11
- package/dist/release-notes.js +154 -0
- package/dist/service.js +24 -9
- package/dist/sessions.js +101 -13
- package/dist/share.js +92 -13
- package/dist/shellenv.js +131 -18
- package/dist/single-load.js +176 -0
- package/dist/tooling.js +73 -0
- package/dist/updates.js +37 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,47 +15,102 @@ import * as fs from "node:fs";
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import * as readline from "node:readline";
|
|
17
17
|
import { spawn, spawnSync } from "node:child_process";
|
|
18
|
-
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, readOsNotifications, setOsNotifications, readProviders, setProviderSurface, readBinaryPath, readBinaryPaths, setBinaryPath, ROOT, resetRebindKillSwitch, } from "./profiles.js";
|
|
18
|
+
import { HOME, browserDir, configDir, die, ensureRoot, identity, listAllProfiles, listProfiles, migrateLegacyLayout, profileDir, profileExists, readJson, requireProfile, activeFor, setActive, labelFor, setLabel, clearLabel, colorOf, isDisabled, setDisabled, setColor, isProfileColor, PROFILE_COLORS, renameProfile, isProfileLabel, PROFILE_LABELS, readAutoSwitch, readAutoSwitchAll, setAutoSwitch, readOsNotifications, setOsNotifications, readProviders, setProviderSurface, readBinaryPath, readGuard, setGuard, readBinaryPaths, setBinaryPath, ROOT, resetRebindKillSwitch, } from "./profiles.js";
|
|
19
|
+
import { browserWrapperScript, defaultBrowserDeps, listBrowsers, locateBrowser } from "./browsers.js";
|
|
19
20
|
import { PROVIDER_IDS, provider, isProviderInstalled, resolveBinary } from "./providers.js";
|
|
20
|
-
import { rebind, restoreRebind } from "./rebind.js";
|
|
21
|
+
import { borrowerOf, discardBinding, readBinding, rebind, releaseBorrowedAccount, restoreRebind } from "./rebind.js";
|
|
21
22
|
import { buildRebindDialog, resolveRebindChoice, selectableRows } from "./rebind-dialog.js";
|
|
22
23
|
import { parseArgs, parseRun, resolveProviderValue } from "./args.js";
|
|
23
24
|
import { extractBrief, writeBrief, sweepBriefs, seedPrompt } from "./handoff.js";
|
|
24
25
|
import { credentialStore } from "./credentials.js";
|
|
25
26
|
import { withProperLock } from "./locks.js";
|
|
26
|
-
import { applySharing, removeSharing, syncSharing, sharedLinkHealth } from "./share.js";
|
|
27
|
+
import { applySharing, removeSharing, syncSharing, sharedLinkHealth, PROMPT_HISTORY } from "./share.js";
|
|
27
28
|
import { assertNotFull, exportConfig, importConfig } from "./config-transfer.js";
|
|
28
|
-
import { detectShell, shellenvScript } from "./shellenv.js";
|
|
29
|
-
import { runDoctor } from "./doctor.js";
|
|
30
|
-
import {
|
|
29
|
+
import { detectShell, installShellIntegration, shellenvScript } from "./shellenv.js";
|
|
30
|
+
import { runDoctor, shellIntegrationCheck } from "./doctor.js";
|
|
31
|
+
import { appendActivity, readActivity, ACTIVITY_MAX_LINES } from "./activity.js";
|
|
32
|
+
import { readGuiSettings, writeGuiSettings, applyGuiSetting } from "./gui-settings.js";
|
|
33
|
+
import { readProtection, setProtection, renameProtection, forgetProtection } from "./protection.js";
|
|
34
|
+
import { POOL_PREFIX, addPoolMember, createPool, deletePool, findPool, isPoolRef, parseMember, poolIdOf, readPools, removePoolMember, renamePool, renamePoolMember, suggestPoolsFromLabels, } from "./pools.js";
|
|
35
|
+
import { resolveForDir } from "./dir-resolve.js";
|
|
36
|
+
import { antigravityAuthStatus, claudeAuthStatus, claudeCredentialFile, codexAuthStatus, fileMtimeMs, readText, } from "./auth-status.js";
|
|
37
|
+
import { TOOL_IDS, checkTooling, formatToolNotes, formatToolingLines, runToolAction, toolNotesRefusal, toolReleaseRepo, } from "./tooling.js";
|
|
38
|
+
import { releaseNoteBlocks } from "./release-notes.js";
|
|
31
39
|
import { readHistory, recordHistorySample } from "./history.js";
|
|
32
40
|
import { launchGui } from "./gui-launch.js";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
41
|
+
import { GUARD_POLICIES, GUARD_REFUSED_EXIT, OVERRIDE_ENV, guardVerdict, inspectLoad, isGuardPolicy, overrideFromEnv, renderGuardMessage, } from "./single-load.js";
|
|
42
|
+
import { checkForUpdate, fetchReleases, releasesNewerThan, selfUpdate } from "./updates.js";
|
|
43
|
+
import { mappingRows, pruneMappings, removeMapping, resolveMapping, setMapping, setPaused, normalizePath, } from "./mappings.js";
|
|
35
44
|
import { checkAuth, fetchProfile, fetchUsage, liveSessionPids, } from "./api.js";
|
|
36
45
|
import { defaultFreshenDeps, freshAccessToken } from "./token-freshen.js";
|
|
37
46
|
import { formatSnapshot, parseUsage } from "./usage.js";
|
|
38
47
|
import { readCodexUsage } from "./codex-usage.js";
|
|
39
48
|
import { redeemResetCredit } from "./codex-reset.js";
|
|
40
|
-
import { assertValidSessionId, cleanupForkVehicle, codexSessionCommand, deleteSession, listSessions, listCodexSessions, locateSession, locateCodexSession, markLive, restoreSession, sharedHistory, sweepTrash, transferSession, transferCodexSession, trashedSessionExists, } from "./sessions.js";
|
|
49
|
+
import { assertValidSessionId, cleanupForkVehicle, codexSessionCommand, deleteSession, detectRunningClaudeProfile, listSessions, listCodexSessions, locateSession, locateCodexSession, markLive, openQuestion, restoreSession, sharedHistory, sweepTrash, transferSession, transferCodexSession, trashedSessionExists, } from "./sessions.js";
|
|
41
50
|
import { isFresh, readDaemonState, readPid, PIDFILE, processAlive } from "./daemon.js";
|
|
42
|
-
import { cmdService, serviceUninstall } from "./service.js";
|
|
51
|
+
import { cmdService, serviceInstall, serviceTarget, serviceUninstall } from "./service.js";
|
|
52
|
+
import { fileURLToPath } from "node:url";
|
|
43
53
|
import { readContext, turnInFlight } from "./telemetry.js";
|
|
44
54
|
import { readPreview } from "./session-preview.js";
|
|
45
|
-
import { installHooks, uninstallHooks, hooksInstalled, readSettings, appendEvent, eventFile, profileFromConfigDir, } from "./hooks.js";
|
|
55
|
+
import { installHooks, uninstallHooks, hooksInstalled, readSettings, appendEvent, eventFile, profileFromConfigDir, readEvents, waitingSessions, } from "./hooks.js";
|
|
46
56
|
import { readTelemetryConfig, writeTelemetryConfig } from "./notify.js";
|
|
47
57
|
import { appendNotification, readNotifications, clearNotifications } from "./notifications.js";
|
|
48
58
|
import { APPS, buildLaunch, findApp, guiDataDir, isInstalled } from "./apps.js";
|
|
59
|
+
import { scanDesktop } from "./desktop-sessions.js";
|
|
49
60
|
import { ensureAgyKeychain } from "./agy-keychain.js";
|
|
50
61
|
import { currentManagedSession, currentTmuxSessionName, newSessionArgs, readTmuxRegistry, recordManagedSession, respawnPaneArgs, sendKeysArgs, tmuxAvailable, tmuxSessionName, } from "./tmux.js";
|
|
51
62
|
// Per-OS credential store (keychain-then-file on darwin, file-only elsewhere).
|
|
52
63
|
const credentials = credentialStore();
|
|
53
64
|
const CLAUDE_HOME = provider("claude").defaultConfigDir(); // ~/.claude
|
|
54
65
|
const CLAUDE_JSON = path.join(HOME, ".claude.json");
|
|
66
|
+
/** Record one of agent-switch's own actions in the activity log (never a token). */
|
|
67
|
+
function logActivity(kind, target, action, reason, ok = true) {
|
|
68
|
+
appendActivity({ kind, target, action, reason, ok });
|
|
69
|
+
}
|
|
70
|
+
/** Same-provider profiles that carry no live session — the concrete
|
|
71
|
+
* alternatives a refusal names, so the user can act on it without first
|
|
72
|
+
* running `list` themselves. */
|
|
73
|
+
function freeProfiles(providerId, exclude) {
|
|
74
|
+
return listProfiles(providerId).filter((n) => n !== exclude && liveSessionPids(configDir(providerId, n)).length === 0);
|
|
75
|
+
}
|
|
76
|
+
function checkSingleLoad(providerId, name, opts = {}) {
|
|
77
|
+
const p = provider(providerId);
|
|
78
|
+
// Early-out before touching the filesystem: `dir --guard` runs on every
|
|
79
|
+
// interactive start, so a disabled guard (or a provider with no live-session
|
|
80
|
+
// signal at all) must cost a state read and nothing more.
|
|
81
|
+
const policy = readGuard();
|
|
82
|
+
if (policy === "off" || !p.hasLiveSessionSignal) {
|
|
83
|
+
return { verdict: "allow", inspection: { sessions: [], loaded: false, nested: false }, message: null };
|
|
84
|
+
}
|
|
85
|
+
const cfg = configDir(providerId, name);
|
|
86
|
+
const inspection = inspectLoad(cfg, p.envVar);
|
|
87
|
+
const verdict = guardVerdict({
|
|
88
|
+
policy,
|
|
89
|
+
inspection,
|
|
90
|
+
overridden: opts.forced === true || overrideFromEnv(),
|
|
91
|
+
detectable: p.hasLiveSessionSignal,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
verdict,
|
|
95
|
+
inspection,
|
|
96
|
+
message: verdict === "allow"
|
|
97
|
+
? null
|
|
98
|
+
: renderGuardMessage({
|
|
99
|
+
profile: name,
|
|
100
|
+
binary: p.binary,
|
|
101
|
+
verdict,
|
|
102
|
+
inspection,
|
|
103
|
+
offerForceFlag: opts.offerForceFlag === true,
|
|
104
|
+
context: opts.context ?? "start",
|
|
105
|
+
// Only worth computing on a message we are actually going to print.
|
|
106
|
+
alternatives: verdict === "refuse" ? freeProfiles(providerId, name) : [],
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
55
110
|
// ---------- launcher ---------------------------------------------------------
|
|
56
|
-
function launch(p, name, args) {
|
|
111
|
+
function launch(p, name, args, extraEnv = {}) {
|
|
57
112
|
const home = configDir(p.id, name);
|
|
58
|
-
const env = { ...process.env, [p.envVar]: home };
|
|
113
|
+
const env = { ...process.env, ...extraEnv, [p.envVar]: home };
|
|
59
114
|
// Antigravity's agy CLI stores its token in the macOS keychain (fixed key) —
|
|
60
115
|
// seed a per-profile keychain under this HOME so the token is isolated per
|
|
61
116
|
// account (and macOS doesn't pop its "no keychain found" dialog). No-op off mac.
|
|
@@ -73,20 +128,111 @@ function launch(p, name, args) {
|
|
|
73
128
|
return res.status ?? 1;
|
|
74
129
|
}
|
|
75
130
|
// ---------- commands ---------------------------------------------------------
|
|
76
|
-
function cmdAdd(providerId, name) {
|
|
131
|
+
function cmdAdd(providerId, name, browserId) {
|
|
77
132
|
if (!name)
|
|
78
|
-
die(`usage: agent-switch add [--provider ${providerId}] <profile
|
|
133
|
+
die(`usage: agent-switch add [--provider ${providerId}] <profile> [--browser B]`);
|
|
79
134
|
if (profileExists(providerId, name))
|
|
80
135
|
die(`profile "${name}" already exists for ${providerId}`);
|
|
81
136
|
const p = provider(providerId);
|
|
137
|
+
const env = browserId ? browserEnv(browserId, providerId) : {};
|
|
82
138
|
ensureRoot();
|
|
83
|
-
|
|
139
|
+
const dir = configDir(providerId, name);
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
141
|
+
// A bare `claude` in a fresh config dir opens Claude Code's setup screens, not
|
|
142
|
+
// the sign-in; skip them the way `import` does and run the sign-in itself.
|
|
143
|
+
if (providerId === "claude")
|
|
144
|
+
seedClaudeOnboarding(dir);
|
|
84
145
|
console.log(`Created ${providerId} profile "${name}".`);
|
|
85
|
-
|
|
86
|
-
|
|
146
|
+
const loginArgs = LOGIN_ARGS[providerId];
|
|
147
|
+
console.log(loginArgs.length
|
|
148
|
+
? `Signing it in with \`${[p.binary, ...loginArgs].join(" ")}\` (finish it in the browser if one opens)...`
|
|
149
|
+
: `Launching ${p.binary} for first login (log in inside if prompted)...`);
|
|
150
|
+
const code = launch(p, name, loginArgs, env);
|
|
87
151
|
const id = identity(providerId, name);
|
|
88
|
-
|
|
89
|
-
|
|
152
|
+
const after = authStatusFor(providerId, name);
|
|
153
|
+
const ok = !!id || after.state === "connected" || after.state === "expiring";
|
|
154
|
+
logActivity("login", `${providerId}/${name}`, ok ? "signed in (new profile)" : "created, sign-in not confirmed", "user ran `agent-switch add`", ok);
|
|
155
|
+
const flag = providerId === "claude" ? "" : ` --provider ${providerId}`;
|
|
156
|
+
if (!ok && loginArgs.length) {
|
|
157
|
+
console.error(`Profile "${name}" was created but is not signed in yet${code !== 0 ? ` (${p.binary} exited with ${code})` : ""}. Finish it with: agent-switch login ${name}${flag}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
console.log(id ? `Profile "${name}" is signed in as ${id}.` : ok ? `Profile "${name}" is signed in.` : `Profile "${name}" created.`);
|
|
161
|
+
console.log(`Activate it with: agent-switch use ${name}${flag}`);
|
|
162
|
+
}
|
|
163
|
+
/** Mark Claude Code's first-run setup as done in a new profile (theme taken
|
|
164
|
+
* from the default install), so its first start goes straight to work. */
|
|
165
|
+
function seedClaudeOnboarding(dir) {
|
|
166
|
+
const file = path.join(dir, ".claude.json");
|
|
167
|
+
const config = (fs.existsSync(file) && readJson(file)) || {};
|
|
168
|
+
const theme = (fs.existsSync(CLAUDE_JSON) && readJson(CLAUDE_JSON)?.theme) || "dark";
|
|
169
|
+
config.hasCompletedOnboarding = true;
|
|
170
|
+
config.theme = config.theme || theme;
|
|
171
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* `$BROWSER` for a sign-in in one specific browser. Only Claude Code is verified
|
|
175
|
+
* to follow it; codex on macOS opens the system default regardless, so it is
|
|
176
|
+
* told rather than silently ignored.
|
|
177
|
+
*/
|
|
178
|
+
function browserEnv(browserId, providerId) {
|
|
179
|
+
if (process.platform === "win32")
|
|
180
|
+
die("--browser is not supported on Windows yet.");
|
|
181
|
+
const deps = defaultBrowserDeps(HOME);
|
|
182
|
+
const located = locateBrowser(browserId, deps);
|
|
183
|
+
if (!located) {
|
|
184
|
+
const installed = listBrowsers(deps).map((b) => b.id).join(", ") || "(none found)";
|
|
185
|
+
die(`browser "${browserId}" is not installed. Installed: ${installed}`);
|
|
186
|
+
}
|
|
187
|
+
if (providerId !== "claude") {
|
|
188
|
+
console.log(`Note: ${provider(providerId).binary} opens your default browser itself. If that is the wrong one, open the link it prints in ${browserId} — the sign-in works from any browser.`);
|
|
189
|
+
}
|
|
190
|
+
if (process.platform !== "darwin")
|
|
191
|
+
return { BROWSER: located };
|
|
192
|
+
const wrapper = path.join(ROOT, "browsers", browserId);
|
|
193
|
+
fs.mkdirSync(path.dirname(wrapper), { recursive: true, mode: 0o700 });
|
|
194
|
+
fs.writeFileSync(wrapper, browserWrapperScript(located), { mode: 0o700 });
|
|
195
|
+
fs.chmodSync(wrapper, 0o700);
|
|
196
|
+
return { BROWSER: wrapper };
|
|
197
|
+
}
|
|
198
|
+
function cmdBrowsers(json) {
|
|
199
|
+
const list = listBrowsers(defaultBrowserDeps(HOME));
|
|
200
|
+
if (json)
|
|
201
|
+
return void console.log(JSON.stringify(list, null, 2));
|
|
202
|
+
if (list.length === 0)
|
|
203
|
+
return void console.log("No supported browsers found.");
|
|
204
|
+
for (const b of list)
|
|
205
|
+
console.log(`${b.id.padEnd(16)} ${b.name}`);
|
|
206
|
+
}
|
|
207
|
+
/** The provider's own sign-in command for an existing profile. Antigravity's
|
|
208
|
+
* agy has no separate login command; it prompts when it starts signed out. */
|
|
209
|
+
const LOGIN_ARGS = {
|
|
210
|
+
claude: ["auth", "login"],
|
|
211
|
+
codex: ["login"],
|
|
212
|
+
antigravity: [],
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Sign an existing profile in again — `add` refuses a name that exists, and a
|
|
216
|
+
* login the provider cleared (signed out, refresh window ended) needs exactly
|
|
217
|
+
* this. Runs the provider's own sign-in under the profile's config dir, then
|
|
218
|
+
* reads the stored login back: the identity in the config outlives a sign-out,
|
|
219
|
+
* so only the credential proves the sign-in happened.
|
|
220
|
+
*/
|
|
221
|
+
function cmdLogin(providerId, name, browserId) {
|
|
222
|
+
const n = requireProfile(providerId, name, "login");
|
|
223
|
+
const p = provider(providerId);
|
|
224
|
+
const env = browserId ? browserEnv(browserId, providerId) : {};
|
|
225
|
+
console.log(`Signing ${providerId}/${n} in with \`${[p.binary, ...LOGIN_ARGS[providerId]].join(" ")}\` (finish it in the browser if one opens)...`);
|
|
226
|
+
const code = launch(p, n, LOGIN_ARGS[providerId], env);
|
|
227
|
+
const after = authStatusFor(providerId, n);
|
|
228
|
+
const ok = after.state === "connected" || after.state === "expiring";
|
|
229
|
+
logActivity("login", `${providerId}/${n}`, ok ? "signed in again" : "sign-in not confirmed", "user ran `agent-switch login`", ok);
|
|
230
|
+
if (!ok) {
|
|
231
|
+
console.error(`${providerId}/${n} is still not signed in (${after.detail})${code !== 0 ? ` — ${p.binary} exited with ${code}` : ""}.`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
const id = identity(providerId, n);
|
|
235
|
+
console.log(`${providerId}/${n} is signed in${id ? ` as ${id}` : ""}.`);
|
|
90
236
|
}
|
|
91
237
|
/**
|
|
92
238
|
* Login-free import of the default Claude install into a profile. Takes Claude
|
|
@@ -161,10 +307,21 @@ async function cmdImport(providerId, name) {
|
|
|
161
307
|
}
|
|
162
308
|
function cmdUse(providerId, name) {
|
|
163
309
|
const n = requireProfile(providerId, name, "use");
|
|
310
|
+
if (isDisabled(providerId, n))
|
|
311
|
+
die(`${n} is disabled — turn it back on first: agent-switch enable ${n}${providerId === "claude" ? "" : ` --provider ${providerId}`}`);
|
|
164
312
|
setActive(providerId, n);
|
|
313
|
+
logActivity("switch", `${providerId}/${n}`, "set as active profile", "user ran `agent-switch use`");
|
|
165
314
|
const id = identity(providerId, n);
|
|
166
315
|
console.log(`Active ${providerId} profile: ${n}${id ? ` (${id})` : ""}`);
|
|
167
316
|
console.log(`New \`${provider(providerId).binary}\` sessions use this profile. Running sessions are unaffected.`);
|
|
317
|
+
// `use` loads nothing — it states an intent — so it NEVER blocks. But it is
|
|
318
|
+
// the cheapest possible moment to tell the user that the profile they just
|
|
319
|
+
// pointed at is already busy, before they type the command that gets refused.
|
|
320
|
+
const check = checkSingleLoad(providerId, n, { context: "use" });
|
|
321
|
+
if (check.message) {
|
|
322
|
+
console.log("");
|
|
323
|
+
console.warn(check.message);
|
|
324
|
+
}
|
|
168
325
|
}
|
|
169
326
|
function cmdDeactivate(providerId) {
|
|
170
327
|
const active = activeFor(providerId);
|
|
@@ -173,15 +330,30 @@ function cmdDeactivate(providerId) {
|
|
|
173
330
|
return;
|
|
174
331
|
}
|
|
175
332
|
setActive(providerId, null);
|
|
333
|
+
logActivity("switch", `${providerId}/${active}`, "deactivated", "user ran `agent-switch deactivate`");
|
|
176
334
|
console.log(`Deactivated ${providerId} profile "${active}". No ${providerId} profile is active.`);
|
|
177
335
|
console.log(`New \`${provider(providerId).binary}\` sessions fall back to the default install until you \`use\` one.`);
|
|
178
336
|
}
|
|
179
|
-
function cmdRun(providerId, name, args) {
|
|
337
|
+
function cmdRun(providerId, name, args, cwd) {
|
|
180
338
|
const n = requireProfile(providerId, name, "run");
|
|
339
|
+
if (cwd !== undefined) {
|
|
340
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory())
|
|
341
|
+
die(`no such folder: ${cwd}`);
|
|
342
|
+
process.chdir(cwd);
|
|
343
|
+
}
|
|
181
344
|
const p = provider(providerId);
|
|
182
|
-
// `--tmux`
|
|
345
|
+
// `--tmux` and `--force` are agent-switch flags, not passthrough flags — strip
|
|
346
|
+
// them before the rest reaches the provider binary.
|
|
183
347
|
const wantTmux = args.includes("--tmux");
|
|
184
|
-
const
|
|
348
|
+
const forced = args.includes("--force");
|
|
349
|
+
const passthrough = args.filter((a) => a !== "--tmux" && a !== "--force");
|
|
350
|
+
// agent-switch spawns the binary itself here, so this is the strongest
|
|
351
|
+
// enforcement point: refuse before anything is launched.
|
|
352
|
+
const guard = checkSingleLoad(providerId, n, { forced, offerForceFlag: true });
|
|
353
|
+
if (guard.message)
|
|
354
|
+
console.error(guard.verdict === "refuse" ? `error: ${guard.message}` : guard.message);
|
|
355
|
+
if (guard.verdict === "refuse")
|
|
356
|
+
process.exit(GUARD_REFUSED_EXIT);
|
|
185
357
|
if (wantTmux) {
|
|
186
358
|
if (!tmuxAvailable()) {
|
|
187
359
|
console.log("(--tmux unavailable — tmux not found or Windows; running normally.)");
|
|
@@ -204,8 +376,12 @@ function printProfileLine(providerId, name, showLive) {
|
|
|
204
376
|
let live = "";
|
|
205
377
|
if (showLive && providerId === "claude") {
|
|
206
378
|
const pids = liveSessionPids(configDir("claude", name));
|
|
207
|
-
|
|
208
|
-
|
|
379
|
+
// More than one session on one account is the state the single-load guard
|
|
380
|
+
// exists to prevent, so it is marked rather than blending into the count.
|
|
381
|
+
if (pids.length > 1)
|
|
382
|
+
live = ` ⚠️ [loaded ${pids.length}× — ${pids.length} live sessions]`;
|
|
383
|
+
else if (pids.length === 1)
|
|
384
|
+
live = " [1 live session]";
|
|
209
385
|
}
|
|
210
386
|
console.log(`${mark} ${name.padEnd(16)} ${id}${live}`);
|
|
211
387
|
}
|
|
@@ -224,6 +400,8 @@ function cmdList(providerId, json = false) {
|
|
|
224
400
|
name: n,
|
|
225
401
|
identity: identity(pid, n),
|
|
226
402
|
label: labelFor(pid, n),
|
|
403
|
+
color: colorOf(pid, n),
|
|
404
|
+
disabled: isDisabled(pid, n),
|
|
227
405
|
active: activeFor(pid) === n,
|
|
228
406
|
liveSessions: pid === "claude" ? liveSessionPids(configDir("claude", n)).length : 0,
|
|
229
407
|
})));
|
|
@@ -340,7 +518,10 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
340
518
|
}
|
|
341
519
|
const token = freshAccessToken(configDir("claude", n), defaultFreshenDeps(readBinaryPath("claude")));
|
|
342
520
|
if (!token) {
|
|
343
|
-
|
|
521
|
+
const auth = authStatusFor("claude", n);
|
|
522
|
+
console.log(auth.state === "sign-in-needed"
|
|
523
|
+
? ` (signed out — sign in again: agent-switch login ${n})`
|
|
524
|
+
: " (credential not readable — profile may not have run yet)");
|
|
344
525
|
continue;
|
|
345
526
|
}
|
|
346
527
|
const [profileInfo, raw] = await Promise.all([fetchProfile(token), fetchUsage(token)]);
|
|
@@ -362,20 +543,107 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
362
543
|
}
|
|
363
544
|
}
|
|
364
545
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
546
|
+
/** Cached usage for one profile: the daemon's snapshot or the newest history
|
|
547
|
+
* sample, whichever is newer. Local reads only — `dir` runs on every start. */
|
|
548
|
+
function cachedUsage(providerId, name) {
|
|
549
|
+
const daemonSnap = readDaemonState()?.profiles?.[`${providerId}/${name}`] ?? null;
|
|
550
|
+
const daemonMs = daemonSnap ? Date.parse(daemonSnap.capturedAt) : NaN;
|
|
551
|
+
const hist = readHistory(path.join(profileDir(providerId, name), "usage-history.json"));
|
|
552
|
+
const last = hist.length ? hist[hist.length - 1] : null;
|
|
553
|
+
const histMs = last ? Date.parse(last.at) : NaN;
|
|
554
|
+
if (last && !isNaN(histMs) && (isNaN(daemonMs) || histMs > daemonMs)) {
|
|
555
|
+
// History samples keep no reset time; the daemon's last one still holds
|
|
556
|
+
// while it lies ahead (a window's reset only moves once it has passed).
|
|
557
|
+
const resetOf = (key) => {
|
|
558
|
+
const r = daemonSnap?.windows.find((d) => d.key === key)?.resetsAt ?? null;
|
|
559
|
+
return r && Date.parse(r) > histMs ? r : null;
|
|
560
|
+
};
|
|
561
|
+
const windows = last.windows.map((w) => ({ key: w.key, label: w.key, utilization: w.utilization, resetsAt: resetOf(w.key) }));
|
|
562
|
+
return { snapshot: { windows, routines: null, capturedAt: last.at }, capturedMs: histMs };
|
|
563
|
+
}
|
|
564
|
+
return { snapshot: daemonSnap, capturedMs: isNaN(daemonMs) ? null : daemonMs };
|
|
565
|
+
}
|
|
566
|
+
function tildePath(p) {
|
|
567
|
+
return p === HOME || p.startsWith(HOME + path.sep) ? "~" + p.slice(HOME.length) : p;
|
|
568
|
+
}
|
|
569
|
+
/** Profile names are validated by `profileDir`, which exits on a bad name —
|
|
570
|
+
* a pool ref or a stale mapping must never kill the shell wrapper's hot path. */
|
|
571
|
+
function profileExistsSafe(providerId, name) {
|
|
572
|
+
return /^[A-Za-z0-9_-]+$/.test(name) && profileExists(providerId, name);
|
|
573
|
+
}
|
|
574
|
+
function resolveDir(providerId, plain = false) {
|
|
575
|
+
return resolveForDir({
|
|
576
|
+
providerId,
|
|
577
|
+
mapping: plain ? null : resolveMapping(process.cwd(), providerId),
|
|
578
|
+
names: listProfiles(providerId),
|
|
579
|
+
threshold: (n) => readProtection(providerId, n).thresholdPct,
|
|
580
|
+
active: activeFor(providerId),
|
|
581
|
+
exists: (n) => profileExistsSafe(providerId, n),
|
|
582
|
+
signedIn: (n) => identity(providerId, n) !== null,
|
|
583
|
+
disabled: (n) => isDisabled(providerId, n),
|
|
584
|
+
pool: (id) => findPool(id),
|
|
585
|
+
usage: (n) => cachedUsage(providerId, n),
|
|
586
|
+
// Only providers that write a live-session signal can report one; the
|
|
587
|
+
// check runs for pool members only.
|
|
588
|
+
live: (n) => provider(providerId).hasLiveSessionSignal && liveSessionPids(configDir(providerId, n)).length > 0,
|
|
589
|
+
guard: readGuard(),
|
|
590
|
+
now: Date.now(),
|
|
591
|
+
displayPath: tildePath,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Resolve which profile a shell wrapper should load: directory mapping (nearest
|
|
596
|
+
* ancestor of CWD) > active-for-provider. Empty output → the wrapper falls back
|
|
597
|
+
* to the default config dir.
|
|
598
|
+
*
|
|
599
|
+
* With `--guard`, stdout is unchanged (the wrapper still reads the path from
|
|
600
|
+
* it) and the guard speaks on STDERR, exiting {@link GUARD_REFUSED_EXIT} on a
|
|
601
|
+
* refusal. Splitting the streams is what lets one command both feed the wrapper
|
|
602
|
+
* and talk to the human; a distinct exit code is what lets the wrapper block on
|
|
603
|
+
* a refusal without ever blocking on a missing or older `agent-switch`.
|
|
604
|
+
*/
|
|
605
|
+
function cmdDir(providerId, guarded = false, json = false, plain = false) {
|
|
606
|
+
const r = resolveDir(providerId, plain);
|
|
607
|
+
if (json) {
|
|
608
|
+
console.log(JSON.stringify({ profile: r.name ? `${providerId}/${r.name}` : null, configDir: r.name ? configDir(providerId, r.name) : null, why: r.why }, null, 2));
|
|
370
609
|
return;
|
|
371
610
|
}
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
611
|
+
const name = r.name;
|
|
612
|
+
if (!name)
|
|
613
|
+
return;
|
|
614
|
+
console.log(configDir(providerId, name));
|
|
615
|
+
if (!guarded)
|
|
616
|
+
return;
|
|
617
|
+
// A wrapper cannot pass `--force` (it would reach the provider binary), so
|
|
618
|
+
// only the env override is offered here.
|
|
619
|
+
const guard = checkSingleLoad(providerId, name, { offerForceFlag: false });
|
|
620
|
+
if (guard.message)
|
|
621
|
+
console.error(`agent-switch: ${guard.message}`);
|
|
622
|
+
if (guard.verdict === "refuse")
|
|
623
|
+
process.exit(GUARD_REFUSED_EXIT);
|
|
376
624
|
}
|
|
377
|
-
function cmdMap(providerId, name, dir) {
|
|
378
|
-
|
|
625
|
+
function cmdMap(providerId, name, dir, flags = {}) {
|
|
626
|
+
if (flags.pause || flags.resume) {
|
|
627
|
+
// `map --pause [dir]`: the positional is the folder, there is no profile.
|
|
628
|
+
const target = name ?? process.cwd();
|
|
629
|
+
const on = !!flags.pause;
|
|
630
|
+
if (!setPaused(target, providerId, on))
|
|
631
|
+
die(`no mapping for ${target} (${providerId})`);
|
|
632
|
+
console.log(on
|
|
633
|
+
? `Paused the ${providerId} rule for ${normalizePath(target)} — it stays listed but no longer picks the account. Resume: agent-switch map --resume ${target}`
|
|
634
|
+
: `Resumed the ${providerId} rule for ${normalizePath(target)}.`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (name && isPoolRef(name)) {
|
|
638
|
+
const pool = findPool(poolIdOf(name));
|
|
639
|
+
if (!pool)
|
|
640
|
+
die(`no pool "${poolIdOf(name)}" — create it with: agent-switch pool create <name>`);
|
|
641
|
+
const key = setMapping(dir ?? process.cwd(), providerId, `${POOL_PREFIX}${pool.id}`);
|
|
642
|
+
console.log(`Mapped ${key} -> pool ${pool.id} (${providerId})`);
|
|
643
|
+
console.log(`\`${provider(providerId).binary}\` here starts on the ${providerId} member of "${pool.name}" with the most fresh headroom. See why: agent-switch dir --json`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const n = requireProfile(providerId, name, `map [--provider ${providerId}] <profile|pool:<id>> [dir]`);
|
|
379
647
|
const key = setMapping(dir ?? process.cwd(), providerId, n);
|
|
380
648
|
console.log(`Mapped ${key} -> ${providerId}/${n}`);
|
|
381
649
|
console.log(`\`${provider(providerId).binary}\` in this directory (and subdirectories) now uses this profile.`);
|
|
@@ -387,14 +655,20 @@ function cmdUnmap(providerId, dir) {
|
|
|
387
655
|
else
|
|
388
656
|
die(`no mapping for ${target}${providerId ? ` (${providerId})` : ""}`);
|
|
389
657
|
}
|
|
390
|
-
function cmdMappings() {
|
|
658
|
+
function cmdMappings(json = false) {
|
|
391
659
|
const rows = mappingRows();
|
|
660
|
+
if (json) {
|
|
661
|
+
console.log(JSON.stringify(rows.map((r) => ({ path: r.path, provider: r.provider, target: r.name, paused: r.paused }))));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
392
664
|
if (rows.length === 0) {
|
|
393
665
|
console.log("No directory mappings. Create one with: agent-switch map <profile> [dir]");
|
|
394
666
|
return;
|
|
395
667
|
}
|
|
396
|
-
for (const r of rows)
|
|
397
|
-
|
|
668
|
+
for (const r of rows) {
|
|
669
|
+
const line = isPoolRef(r.name) ? `${r.path} -> ${r.name} (${r.provider})` : `${r.path} -> ${r.provider}/${r.name}`;
|
|
670
|
+
console.log(r.paused ? `${line} [paused]` : line);
|
|
671
|
+
}
|
|
398
672
|
}
|
|
399
673
|
// share + web are Claude-config concepts and stay Claude-scoped.
|
|
400
674
|
function cmdShare(mode, flags = []) {
|
|
@@ -420,7 +694,9 @@ function cmdShare(mode, flags = []) {
|
|
|
420
694
|
const actions = applySharing(source, configDir("claude", p), withHistory);
|
|
421
695
|
console.log(` ${p}: ${actions.length > 0 ? actions.join(", ") : "up to date"}`);
|
|
422
696
|
}
|
|
423
|
-
console.log("
|
|
697
|
+
console.log("Prompt history (history.jsonl — what ↑ and Ctrl+R search) is shared too; a profile's own\n" +
|
|
698
|
+
"file was merged in and kept as history.jsonl.pre-share.\n" +
|
|
699
|
+
"Shared directories (skills/, commands/, agents/) write through the link. Shared files\n" +
|
|
424
700
|
"(settings.json, ...) fork on an in-profile /config edit — run `agent-switch share sync`\n" +
|
|
425
701
|
"to push a fork back to the source and re-link.");
|
|
426
702
|
}
|
|
@@ -446,7 +722,11 @@ function cmdShare(mode, flags = []) {
|
|
|
446
722
|
// flag: sharing is active when any profile carries agent-switch-managed links.
|
|
447
723
|
const rows = profiles
|
|
448
724
|
.filter((p) => configDir("claude", p) !== source)
|
|
449
|
-
.map((p) =>
|
|
725
|
+
.map((p) => {
|
|
726
|
+
const links = sharedLinkHealth(configDir("claude", p));
|
|
727
|
+
const prompts = links.some((l) => l.name === PROMPT_HISTORY && l.state === "linked");
|
|
728
|
+
return { name: p, shared: links.length > 0, prompts };
|
|
729
|
+
});
|
|
450
730
|
const active = rows.some((r) => r.shared);
|
|
451
731
|
if (flags.includes("--json")) {
|
|
452
732
|
console.log(JSON.stringify({ active, source: sourceName ?? "default", profiles: rows }));
|
|
@@ -454,7 +734,7 @@ function cmdShare(mode, flags = []) {
|
|
|
454
734
|
}
|
|
455
735
|
console.log(`share: ${active ? "on" : "off"} (source ${sourceName ?? "default"})`);
|
|
456
736
|
for (const r of rows)
|
|
457
|
-
console.log(` ${r.name}: ${r.shared ? "shared" : "own"}`);
|
|
737
|
+
console.log(` ${r.name}: ${r.shared ? "shared" : "own"}${r.shared && !r.prompts ? " (prompt history not linked — run share on)" : ""}`);
|
|
458
738
|
}
|
|
459
739
|
else {
|
|
460
740
|
die("usage: agent-switch share on|sync|off|status [--history] [--source <profile|default>]");
|
|
@@ -739,6 +1019,40 @@ function cmdHookEvent() {
|
|
|
739
1019
|
* GUI contract: a flat array, metadata only — transcript content never leaves
|
|
740
1020
|
* the profile dir (only the guarded first-line header is read).
|
|
741
1021
|
*/
|
|
1022
|
+
/** The regular Desktop app's data dir, then every profile's own instance. */
|
|
1023
|
+
function desktopInstances() {
|
|
1024
|
+
const app = findApp("claude-desktop");
|
|
1025
|
+
const regular = process.platform === "darwin"
|
|
1026
|
+
? path.join(HOME, "Library", "Application Support", "Claude")
|
|
1027
|
+
: process.platform === "win32"
|
|
1028
|
+
? path.join(process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming"), "Claude")
|
|
1029
|
+
: path.join(HOME, ".config", "Claude");
|
|
1030
|
+
return [{ profile: null, dir: regular }, ...(app ? listProfiles("claude").map((p) => ({ profile: p, dir: guiDataDir(app, p) })) : [])];
|
|
1031
|
+
}
|
|
1032
|
+
function desktopScan() {
|
|
1033
|
+
const byAccount = new Map();
|
|
1034
|
+
for (const p of listProfiles("claude")) {
|
|
1035
|
+
const uuid = readJson(path.join(configDir("claude", p), ".claude.json"))?.oauthAccount?.accountUuid;
|
|
1036
|
+
if (typeof uuid === "string")
|
|
1037
|
+
byAccount.set(uuid, p);
|
|
1038
|
+
}
|
|
1039
|
+
return scanDesktop({ instances: desktopInstances(), claudeHome: CLAUDE_HOME, profileOf: (u) => byAccount.get(u) ?? null, now: Date.now() });
|
|
1040
|
+
}
|
|
1041
|
+
/** Which accounts ran sessions in each Claude Desktop instance; `mismatch` =
|
|
1042
|
+
* a profile's own instance signed in with another account. */
|
|
1043
|
+
function cmdDesktop(json) {
|
|
1044
|
+
const { instances } = desktopScan();
|
|
1045
|
+
const shown = instances.filter((i) => i.accounts.length > 0);
|
|
1046
|
+
if (json)
|
|
1047
|
+
return void console.log(JSON.stringify(shown, null, 2));
|
|
1048
|
+
if (!shown.length)
|
|
1049
|
+
return void console.log("No Claude Desktop sessions found.");
|
|
1050
|
+
for (const i of shown) {
|
|
1051
|
+
console.log(`${i.profile ? `${i.profile}'s instance` : "Claude Desktop"} (${tildePath(i.dir)})`);
|
|
1052
|
+
for (const a of i.accounts)
|
|
1053
|
+
console.log(` ${a.profile ?? "an account without a profile"} (${a.accountUuid.slice(0, 8)})${i.profile && a.profile !== i.profile ? " <- not this profile's account" : ""}`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
742
1056
|
function cmdSessions(providerId, providerExplicit, name, flags = {}) {
|
|
743
1057
|
if (providerExplicit && providerId !== "claude" && providerId !== "codex") {
|
|
744
1058
|
die(`sessions supports claude and codex for now (${providerId} parity is gated on its Phase-0 spike — see scripts/spikes/)`);
|
|
@@ -751,6 +1065,8 @@ function cmdSessions(providerId, providerExplicit, name, flags = {}) {
|
|
|
751
1065
|
if (profiles.length === 0)
|
|
752
1066
|
die(`no ${providerId} profiles`);
|
|
753
1067
|
const all = [];
|
|
1068
|
+
// Claude Desktop's own sessions, filed by account (see desktop-sessions.ts).
|
|
1069
|
+
const desktop = providerId === "claude" ? desktopScan().rows : [];
|
|
754
1070
|
for (const p of profiles) {
|
|
755
1071
|
const cfg = configDir(providerId, p);
|
|
756
1072
|
// Codex stores date-partitioned rollout files (no encoded-cwd dir); its live
|
|
@@ -759,7 +1075,24 @@ function cmdSessions(providerId, providerExplicit, name, flags = {}) {
|
|
|
759
1075
|
const rows = providerId === "codex" ? listCodexSessions(cfg, limit) : listSessions(cfg, limit);
|
|
760
1076
|
if (providerId === "claude")
|
|
761
1077
|
markLive(cfg, rows); // POSIX: pid→cwd→dir match; win32: recent-only
|
|
762
|
-
|
|
1078
|
+
if (providerId === "claude") {
|
|
1079
|
+
const mtime = new Map(rows.map((r) => [r.sessionId, r.mtimeMs]));
|
|
1080
|
+
const waiting = waitingSessions(readEvents(eventFile(ROOT, providerId, p)), (id) => mtime.get(id) ?? null);
|
|
1081
|
+
// Hooks report a prompt; an open question also shows in the transcript itself.
|
|
1082
|
+
for (const r of rows) {
|
|
1083
|
+
const why = (r.file ? openQuestion(r.file) : null) ?? waiting.get(r.sessionId) ?? null;
|
|
1084
|
+
if (why)
|
|
1085
|
+
Object.assign(r, { waiting: true, waitingFor: why });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const own = new Set(rows.map((r) => r.sessionId));
|
|
1089
|
+
for (const d of desktop)
|
|
1090
|
+
if (d.profile === p && !own.has(d.sessionId)) {
|
|
1091
|
+
const q = d.file ? openQuestion(d.file) : null;
|
|
1092
|
+
rows.push(q ? { ...d, waiting: true, waitingFor: q } : d);
|
|
1093
|
+
}
|
|
1094
|
+
rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1095
|
+
all.push(...rows.slice(0, limit).map((r) => ({ ...r, profile: p })));
|
|
763
1096
|
}
|
|
764
1097
|
if (flags.json) {
|
|
765
1098
|
console.log(JSON.stringify(all.map((r) => {
|
|
@@ -1236,7 +1569,25 @@ function takeoverCodex(sessionId, target, opts) {
|
|
|
1236
1569
|
if (!printOnly && process.stdin.isTTY)
|
|
1237
1570
|
process.exit(launch(provider("codex"), target, resumeArgs));
|
|
1238
1571
|
}
|
|
1239
|
-
|
|
1572
|
+
/**
|
|
1573
|
+
* Ask before a destructive or scarce-credit action. `--yes` skips the question.
|
|
1574
|
+
* Off a terminal there is nobody to ask, so the action proceeds as it always
|
|
1575
|
+
* did (scripts and the GUI, which confirms in its own UI, keep working) and a
|
|
1576
|
+
* note on stderr says how to make that explicit.
|
|
1577
|
+
*/
|
|
1578
|
+
async function confirmAction(question, yes) {
|
|
1579
|
+
if (yes)
|
|
1580
|
+
return true;
|
|
1581
|
+
if (!process.stdin.isTTY) {
|
|
1582
|
+
console.error("agent-switch: not a terminal — proceeding without confirmation (pass --yes to make this explicit).");
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1586
|
+
const answer = await new Promise((resolve) => rl.question(`${question} [y/N] `, resolve));
|
|
1587
|
+
rl.close();
|
|
1588
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1589
|
+
}
|
|
1590
|
+
async function cmdRemove(providerId, name, force = false, yes = false) {
|
|
1240
1591
|
const n = requireProfile(providerId, name, "remove");
|
|
1241
1592
|
if (activeFor(providerId) === n && !force) {
|
|
1242
1593
|
die(`"${n}" is the active ${providerId} profile. Switch away first or use --force.`);
|
|
@@ -1247,15 +1598,53 @@ function cmdRemove(providerId, name, force = false) {
|
|
|
1247
1598
|
die(`profile "${n}" has live Claude Code sessions (PIDs ${pids.join(", ")}). Close them or use --force.`);
|
|
1248
1599
|
}
|
|
1249
1600
|
}
|
|
1601
|
+
// An active rebind keeps both original credentials in two Keychain stash
|
|
1602
|
+
// entries. Deleting the profile would orphan them — restore first.
|
|
1603
|
+
const binding = providerId === "claude" ? readBinding(configDir("claude", n)) : null;
|
|
1604
|
+
if (binding && !force) {
|
|
1605
|
+
die(`profile "${n}" is rebound to "${binding.boundToProfile}"'s account. Restore it first: ` +
|
|
1606
|
+
`agent-switch rebind --restore --profile ${n} — or use --force to drop the rebind (its stashed credentials are deleted).`);
|
|
1607
|
+
}
|
|
1608
|
+
// The reverse case: another profile is serving THIS profile's account. Its
|
|
1609
|
+
// restore would write into the store of a profile that no longer exists.
|
|
1610
|
+
const borrower = providerId === "claude" ? borrowerOf(n) : null;
|
|
1611
|
+
if (borrower && !force) {
|
|
1612
|
+
die(`profile "${n}"'s account is currently borrowed by "${borrower}" (rebind). Restore it first: ` +
|
|
1613
|
+
`agent-switch rebind --restore --profile ${borrower} — or use --force to restore "${borrower}" and then remove "${n}".`);
|
|
1614
|
+
}
|
|
1615
|
+
const who = identity(providerId, n);
|
|
1616
|
+
if (!(await confirmAction(`Remove ${providerId} profile "${n}"${who ? ` (${who})` : ""} and its login? This cannot be undone.`, yes))) {
|
|
1617
|
+
console.log("Cancelled — nothing removed.");
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1250
1620
|
// Keychain entries exist only for claude (darwin); codex/antigravity are file-based,
|
|
1251
1621
|
// so computing a Claude hash for them would be a conceptual no-op — skip it.
|
|
1622
|
+
let restoredBorrower = null;
|
|
1623
|
+
if (borrower) {
|
|
1624
|
+
try {
|
|
1625
|
+
restoredBorrower = await releaseBorrowedAccount(n);
|
|
1626
|
+
}
|
|
1627
|
+
catch (e) {
|
|
1628
|
+
die(`could not restore "${borrower}" first, so "${n}" was not removed: ${e.message}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (restoredBorrower)
|
|
1631
|
+
logActivity("rebind", `claude/${restoredBorrower}`, `restored own account (was on ${n})`, "user ran `agent-switch remove --force`");
|
|
1632
|
+
}
|
|
1633
|
+
const droppedRebind = binding ? discardBinding(n) : false;
|
|
1252
1634
|
const removedEntry = providerId === "claude" ? credentials.removeEntry(configDir(providerId, n)) : false;
|
|
1253
1635
|
fs.rmSync(profileDir(providerId, n), { recursive: true, force: true });
|
|
1254
1636
|
if (activeFor(providerId) === n)
|
|
1255
1637
|
setActive(providerId, null);
|
|
1256
1638
|
clearLabel(providerId, n); // drop its label so it can't linger as an orphan
|
|
1257
1639
|
const pruned = pruneMappings(providerId, n);
|
|
1640
|
+
forgetProtection(providerId, n);
|
|
1641
|
+
renamePoolMember(`${providerId}/${n}`, null);
|
|
1642
|
+
logActivity("remove", `${providerId}/${n}`, "profile removed", yes ? "user ran `agent-switch remove --yes`" : "user ran `agent-switch remove`");
|
|
1258
1643
|
console.log(`Removed ${providerId} profile "${n}".`);
|
|
1644
|
+
if (restoredBorrower)
|
|
1645
|
+
console.log(`Restored "${restoredBorrower}" to its own account first (it was borrowing "${n}").`);
|
|
1646
|
+
if (droppedRebind)
|
|
1647
|
+
console.log(`Dropped its active rebind (stashed credentials deleted; "${binding.boundToProfile}" keeps its own login).`);
|
|
1259
1648
|
if (removedEntry)
|
|
1260
1649
|
console.log("Removed its keychain credential entry.");
|
|
1261
1650
|
if (pruned.length > 0)
|
|
@@ -1280,6 +1669,55 @@ function cmdLabel(providerId, name, label) {
|
|
|
1280
1669
|
setLabel(providerId, name, label);
|
|
1281
1670
|
console.log(`Labeled ${providerId}/${name} as ${label}.`);
|
|
1282
1671
|
}
|
|
1672
|
+
/** Disable keeps a profile (login, history, settings) but takes it out of every
|
|
1673
|
+
* pick: no active slot, no pool or folder rule, no background polling. */
|
|
1674
|
+
function cmdDisable(providerId, name, disabled) {
|
|
1675
|
+
const n = requireProfile(providerId, name, disabled ? "disable" : "enable");
|
|
1676
|
+
const wasActive = activeFor(providerId) === n;
|
|
1677
|
+
setDisabled(providerId, n, disabled);
|
|
1678
|
+
if (!disabled)
|
|
1679
|
+
return void console.log(`Turned ${providerId}/${n} back on.`);
|
|
1680
|
+
console.log(`Disabled ${providerId}/${n}. Nothing was deleted; \`agent-switch enable ${n}\` turns it back on.`);
|
|
1681
|
+
if (wasActive)
|
|
1682
|
+
console.log(`It was the active ${providerId} profile — new sessions now use the default login until you pick another.`);
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Make this CLI the one terminals and the background service use. Each app runs
|
|
1686
|
+
* it on start, so a dev build and an installed app each bring their own CLI:
|
|
1687
|
+
* the shell wrapper reads <home>/active-cli, and a service installed from the
|
|
1688
|
+
* other CLI is re-installed onto this one. An uninstalled service stays so.
|
|
1689
|
+
*/
|
|
1690
|
+
function cmdActivate(json) {
|
|
1691
|
+
const cli = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
1692
|
+
ensureRoot();
|
|
1693
|
+
fs.writeFileSync(path.join(ROOT, "active-cli"), `${cli}\n`, { mode: 0o600 });
|
|
1694
|
+
const before = serviceTarget();
|
|
1695
|
+
if (before === "other-cli")
|
|
1696
|
+
serviceInstall(() => { });
|
|
1697
|
+
const service = before === "other-cli" ? "repointed" : before === "this-cli" ? "unchanged" : "not-installed";
|
|
1698
|
+
if (json)
|
|
1699
|
+
return void console.log(JSON.stringify({ cli, service }));
|
|
1700
|
+
console.log(`Terminals now use ${cli}.`);
|
|
1701
|
+
if (service === "repointed")
|
|
1702
|
+
console.log("The background service was moved onto it.");
|
|
1703
|
+
if (service === "not-installed")
|
|
1704
|
+
console.log("The background service isn't installed (agent-switch service install).");
|
|
1705
|
+
}
|
|
1706
|
+
function cmdColor(providerId, name, color) {
|
|
1707
|
+
// Like a label, independent state keyed by provider/name: the GUI picks the
|
|
1708
|
+
// colour before `add` has created the profile.
|
|
1709
|
+
if (!name)
|
|
1710
|
+
die(`usage: agent-switch color <profile> [${PROFILE_COLORS.join("|")}|none] [--provider P]`);
|
|
1711
|
+
if (color === undefined || color === "none" || color === "clear") {
|
|
1712
|
+
setColor(providerId, name, null);
|
|
1713
|
+
console.log(`Cleared the colour of ${providerId}/${name}.`);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if (!isProfileColor(color))
|
|
1717
|
+
die(`invalid colour "${color}" (choose: ${PROFILE_COLORS.join(", ")}, or "none" to clear)`);
|
|
1718
|
+
setColor(providerId, name, color);
|
|
1719
|
+
console.log(`Coloured ${providerId}/${name} ${color}.`);
|
|
1720
|
+
}
|
|
1283
1721
|
/** Enable/disable opt-in near-limit notifications (notify + suggest, never an
|
|
1284
1722
|
* automatic switch), or show the current setting. */
|
|
1285
1723
|
/** Rename a profile (move its config dir + carry active/label/mappings). */
|
|
@@ -1309,15 +1747,22 @@ function cmdRename(providerId, from, to) {
|
|
|
1309
1747
|
if (m.provider === providerId && m.name === n)
|
|
1310
1748
|
setMapping(m.path, providerId, to);
|
|
1311
1749
|
}
|
|
1750
|
+
renameProtection(providerId, n, to);
|
|
1751
|
+
renamePoolMember(`${providerId}/${n}`, `${providerId}/${to}`);
|
|
1312
1752
|
console.log(`Renamed ${providerId} profile "${n}" → "${to}".`);
|
|
1313
1753
|
}
|
|
1314
1754
|
/** Manually redeem one banked rate-limit reset for a Codex profile. Consumes a
|
|
1315
1755
|
* real, scarce credit — the GUI gates this behind a confirmation. */
|
|
1316
|
-
async function cmdReset(providerId, name) {
|
|
1756
|
+
async function cmdReset(providerId, name, yes = false) {
|
|
1317
1757
|
if (providerId !== "codex")
|
|
1318
1758
|
die("reset is only available for codex (banked rate-limit resets).");
|
|
1319
1759
|
const n = requireProfile("codex", name, "reset");
|
|
1760
|
+
if (!(await confirmAction(`Redeem one banked rate-limit reset for codex/${n}? Credits are scarce and cannot be returned.`, yes))) {
|
|
1761
|
+
console.log("Cancelled — nothing redeemed.");
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1320
1764
|
const r = await redeemResetCredit(configDir("codex", n));
|
|
1765
|
+
logActivity("redeem", `codex/${n}`, r.ok ? "redeemed one banked reset" : `redeem failed: ${r.reason ?? "unknown"}`, "user ran `agent-switch reset`", r.ok);
|
|
1321
1766
|
if (r.ok) {
|
|
1322
1767
|
console.log(`Redeemed a reset for codex/${n} (windows_reset=${r.windowsReset ?? "?"}).`);
|
|
1323
1768
|
}
|
|
@@ -1358,7 +1803,7 @@ function cmdAutoswitch(providerId, providerExplicit, mode, flags = {}) {
|
|
|
1358
1803
|
if (cfg.enabled) {
|
|
1359
1804
|
console.log(`Once the active ${providerId} profile hits the threshold, the daemon NOTIFIES you and SUGGESTS the\n` +
|
|
1360
1805
|
"account with the most headroom — it never switches automatically; switching stays a manual action.\n" +
|
|
1361
|
-
"On Codex it
|
|
1806
|
+
"On Codex it also tells you when a banked reset is available — redeeming one stays your call (`agent-switch reset`).\n" +
|
|
1362
1807
|
"Run `agent-switch service start` so the daemon is watching.");
|
|
1363
1808
|
}
|
|
1364
1809
|
return;
|
|
@@ -1547,7 +1992,9 @@ function cmdNotify(flags) {
|
|
|
1547
1992
|
}
|
|
1548
1993
|
if (!title && !message)
|
|
1549
1994
|
die("agent-switch notify needs at least --title or --message");
|
|
1550
|
-
|
|
1995
|
+
// --repeat: each occurrence is its own moment (a session waiting again), so
|
|
1996
|
+
// an identical notice is not collapsed into the last one.
|
|
1997
|
+
const created = appendNotification({ kind: kind, title, message }, undefined, undefined, !flags.repeat);
|
|
1551
1998
|
if (flags.json)
|
|
1552
1999
|
console.log(JSON.stringify(created));
|
|
1553
2000
|
}
|
|
@@ -1634,7 +2081,9 @@ async function cmdUpdate() {
|
|
|
1634
2081
|
console.log(`Updating ${c.current} → ${c.release.tag} via npm…`);
|
|
1635
2082
|
else
|
|
1636
2083
|
console.log(`Update check failed (${c.message}); attempting \`npm install\` anyway…`);
|
|
1637
|
-
|
|
2084
|
+
const code = selfUpdate();
|
|
2085
|
+
logActivity("update", null, code === 0 ? "self-update installed" : `self-update failed (exit ${code})`, "user ran `agent-switch update`", code === 0);
|
|
2086
|
+
process.exit(code);
|
|
1638
2087
|
}
|
|
1639
2088
|
function cmdOpen(appId, name) {
|
|
1640
2089
|
if (!appId)
|
|
@@ -1664,8 +2113,75 @@ function cmdOpen(appId, name) {
|
|
|
1664
2113
|
console.log("is saved to this profile. Quit other windows of this app before signing in.");
|
|
1665
2114
|
}
|
|
1666
2115
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
2116
|
+
/**
|
|
2117
|
+
* `agent-switch guard [block|warn|off]` — read or set the single-load policy.
|
|
2118
|
+
* The bare form also reports what is loaded RIGHT NOW, because "what is my
|
|
2119
|
+
* policy" and "is anything currently doubled" are the same question in practice.
|
|
2120
|
+
*/
|
|
2121
|
+
function cmdGuard(value, json = false) {
|
|
2122
|
+
if (value !== undefined) {
|
|
2123
|
+
if (!isGuardPolicy(value)) {
|
|
2124
|
+
die(`unknown guard policy "${value}" (choose: ${GUARD_POLICIES.join(", ")})`);
|
|
2125
|
+
}
|
|
2126
|
+
setGuard(value);
|
|
2127
|
+
logActivity("guard", null, `set ${value}`, "user ran `agent-switch guard`");
|
|
2128
|
+
}
|
|
2129
|
+
const policy = readGuard();
|
|
2130
|
+
// Every profile of every provider that can report a live session.
|
|
2131
|
+
const rows = [];
|
|
2132
|
+
for (const pid of PROVIDER_IDS) {
|
|
2133
|
+
if (!provider(pid).hasLiveSessionSignal)
|
|
2134
|
+
continue;
|
|
2135
|
+
for (const n of listProfiles(pid)) {
|
|
2136
|
+
const count = liveSessionPids(configDir(pid, n)).length;
|
|
2137
|
+
if (count > 0)
|
|
2138
|
+
rows.push({ provider: pid, name: n, sessions: count });
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
if (json) {
|
|
2142
|
+
console.log(JSON.stringify({ policy, loaded: rows }, null, 2));
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
const explain = {
|
|
2146
|
+
block: "a second session on a profile that is already loaded is refused",
|
|
2147
|
+
warn: "a second session is reported, then allowed",
|
|
2148
|
+
off: "no check at all",
|
|
2149
|
+
};
|
|
2150
|
+
console.log(`Single-load guard: ${policy} — ${explain[policy]}.`);
|
|
2151
|
+
if (rows.length === 0) {
|
|
2152
|
+
console.log("No profile is currently loaded by a live session.");
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
console.log("");
|
|
2156
|
+
for (const r of rows) {
|
|
2157
|
+
const doubled = r.sessions > 1;
|
|
2158
|
+
const mark = doubled ? "⚠️ " : " ";
|
|
2159
|
+
const detail = doubled ? `loaded ${r.sessions}× — this is what the guard exists to prevent` : "1 live session";
|
|
2160
|
+
console.log(`${mark} ${`${r.provider}/${r.name}`.padEnd(24)} ${detail}`);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const inert = PROVIDER_IDS.filter((pid) => !provider(pid).hasLiveSessionSignal && listProfiles(pid).length > 0);
|
|
2164
|
+
if (inert.length > 0) {
|
|
2165
|
+
console.log("");
|
|
2166
|
+
console.log(`Not covered: ${inert.join(", ")} — these CLIs do not register a running session on disk,`);
|
|
2167
|
+
console.log("so agent-switch has no way to tell a second load from a first.");
|
|
2168
|
+
}
|
|
2169
|
+
if (policy !== "off") {
|
|
2170
|
+
console.log("");
|
|
2171
|
+
console.log(`Override once: ${OVERRIDE_ENV}=1 <binary> · Change: agent-switch guard <${GUARD_POLICIES.join("|")}>`);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
function cmdShellenv(shellArg, sub, json = false) {
|
|
2175
|
+
if (sub !== "install")
|
|
2176
|
+
return void console.log(shellenvScript(detectShell(shellArg)));
|
|
2177
|
+
const shell = detectShell(shellArg);
|
|
2178
|
+
const present = shellIntegrationCheck(HOME).status === "ok";
|
|
2179
|
+
const r = installShellIntegration(shell, HOME, present);
|
|
2180
|
+
if (json)
|
|
2181
|
+
return void console.log(JSON.stringify(r));
|
|
2182
|
+
console.log(r.alreadyInstalled
|
|
2183
|
+
? "The shell integration is already installed."
|
|
2184
|
+
: `Added the shell integration to ${r.file}. Open a new terminal (or run: source ${r.file}) and \`${shell === "powershell" ? "claude" : "type claude"}\` uses agent-switch.`);
|
|
1669
2185
|
}
|
|
1670
2186
|
/** `agent-switch tooling` — ecosystem tool readout (agent-config, rtk, provider
|
|
1671
2187
|
* CLIs). `--json` is the GUI IPC contract: the GUI renders this readout and
|
|
@@ -1679,6 +2195,61 @@ function cmdTooling(json = false) {
|
|
|
1679
2195
|
console.log(`agent-switch tooling — platform ${process.platform}\n`);
|
|
1680
2196
|
for (const l of formatToolingLines(tools))
|
|
1681
2197
|
console.log(l);
|
|
2198
|
+
// This readout stays offline by contract; the notes command is where the
|
|
2199
|
+
// network lives, so point at it rather than fetching here.
|
|
2200
|
+
console.log("\nWhat changed in a release: `agent-switch tooling notes [<tool>]`");
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* `agent-switch tooling notes [<tool>] [--latest] [--json]` — read what an
|
|
2204
|
+
* ecosystem update actually contains, in the terminal.
|
|
2205
|
+
*
|
|
2206
|
+
* Deliberately a SEPARATE subcommand: `tooling` (no args) is a local, offline
|
|
2207
|
+
* readout the GUI consumes over `--json`, and that contract stays. This one
|
|
2208
|
+
* fetches, and says so in the help.
|
|
2209
|
+
*
|
|
2210
|
+
* Default tool is `agent-config` (the one agent-switch announces updates for).
|
|
2211
|
+
* Shown: every release newer than the installed version, newest first — so
|
|
2212
|
+
* someone several versions behind reads all of them, not just the last. Up to
|
|
2213
|
+
* date or not installed → the newest release, so the command always answers.
|
|
2214
|
+
* A tool with no verified release source gets a named refusal, never an
|
|
2215
|
+
* invented source.
|
|
2216
|
+
*/
|
|
2217
|
+
async function cmdToolingNotes(id, flags) {
|
|
2218
|
+
const toolId = (id ?? "agent-config");
|
|
2219
|
+
if (!TOOL_IDS.includes(toolId)) {
|
|
2220
|
+
die(`usage: agent-switch tooling notes [<${TOOL_IDS.join("|")}>] [--latest] [--json]`);
|
|
2221
|
+
}
|
|
2222
|
+
const repo = toolReleaseRepo(toolId);
|
|
2223
|
+
if (!repo)
|
|
2224
|
+
die(toolNotesRefusal(toolId));
|
|
2225
|
+
const installed = checkTooling({ ids: [toolId] })[0]?.version ?? null;
|
|
2226
|
+
let releases = [];
|
|
2227
|
+
let failure = null;
|
|
2228
|
+
try {
|
|
2229
|
+
releases = await fetchReleases(repo);
|
|
2230
|
+
}
|
|
2231
|
+
catch (e) {
|
|
2232
|
+
failure = e instanceof Error ? e.message : String(e);
|
|
2233
|
+
}
|
|
2234
|
+
const latest = releases[0]?.tag ?? null;
|
|
2235
|
+
// Unread first; when there is nothing unread (up to date, not installed, or
|
|
2236
|
+
// the list is empty) fall back to the newest release so the command answers.
|
|
2237
|
+
const unread = installed ? releasesNewerThan(releases, installed) : [];
|
|
2238
|
+
const selected = unread.length > 0 ? unread : releases.slice(0, 1);
|
|
2239
|
+
const view = { id: toolId, installed, latest, releases: flags.latest ? selected.slice(0, 1) : selected };
|
|
2240
|
+
if (flags.json) {
|
|
2241
|
+
console.log(JSON.stringify({
|
|
2242
|
+
...view,
|
|
2243
|
+
repo,
|
|
2244
|
+
error: failure,
|
|
2245
|
+
releases: view.releases.map((r) => ({ ...r, blocks: releaseNoteBlocks(r.notes) })),
|
|
2246
|
+
}, null, 2));
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
for (const line of formatToolNotes(view))
|
|
2250
|
+
console.log(line);
|
|
2251
|
+
if (failure)
|
|
2252
|
+
console.log(`\nCould not reach the release source (${failure}) — showing what was available.`);
|
|
1682
2253
|
}
|
|
1683
2254
|
/** `agent-switch tooling install|upgrade <id>` — run the tool's per-platform
|
|
1684
2255
|
* install/upgrade command as a visible child process (the GUI runs this inside
|
|
@@ -1727,7 +2298,9 @@ function usage() {
|
|
|
1727
2298
|
Provider defaults to claude; pass --provider codex|antigravity for the others.
|
|
1728
2299
|
Run with no command to launch the tray/menubar GUI (single-instance).
|
|
1729
2300
|
|
|
1730
|
-
agent-switch add [--provider P] <name>
|
|
2301
|
+
agent-switch add [--provider P] <name> [--browser B] create a profile and sign it in
|
|
2302
|
+
agent-switch login [--provider P] <name> [--browser <id>] sign an existing profile in again (after a sign-out / expired login); --browser opens the Claude sign-in in that browser
|
|
2303
|
+
agent-switch browsers [--json] browsers --browser can open (macOS / Linux)
|
|
1731
2304
|
agent-switch import [--provider P] <name> migrate the default install (no re-login)
|
|
1732
2305
|
agent-switch use [--provider P] <name> set the active profile for a provider
|
|
1733
2306
|
agent-switch deactivate [--provider P] clear the active profile for a provider
|
|
@@ -1736,8 +2309,10 @@ Run with no command to launch the tray/menubar GUI (single-instance).
|
|
|
1736
2309
|
agent-switch status [--provider P] [name] [--json] identity (+ Claude usage); --json = active only
|
|
1737
2310
|
agent-switch current [--provider P] show the active profile(s)
|
|
1738
2311
|
agent-switch whoami [--provider P] [name] show a profile's account identity
|
|
1739
|
-
agent-switch dir [--provider P]
|
|
1740
|
-
agent-switch
|
|
2312
|
+
agent-switch dir [--provider P] [--guard] [--json] [--plain] resolve profile for CWD (rule > balanced pick > active); --json adds why
|
|
2313
|
+
agent-switch guard [block|warn|off] one profile, one live session (default warn on new installs)
|
|
2314
|
+
agent-switch map [--provider P] <name|pool:<id>> [dir] map a directory to a profile or a pool
|
|
2315
|
+
agent-switch map --pause|--resume [--provider P] [dir] pause a folder rule (kept, but skipped) or turn it back on
|
|
1741
2316
|
agent-switch unmap [--provider P] [dir] remove a directory mapping
|
|
1742
2317
|
agent-switch mappings list directory mappings
|
|
1743
2318
|
agent-switch share on|sync|off|status [--history] [--source <profile|default>] [--json] (Claude)
|
|
@@ -1749,10 +2324,18 @@ Run with no command to launch the tray/menubar GUI (single-instance).
|
|
|
1749
2324
|
agent-switch takeover <id> --to <profile> [--from <profile>] [--keep-source] [--in-place] [--print-only] [--force] move a session to another profile and resume it
|
|
1750
2325
|
agent-switch rebind [<account>] [--profile <p>] | rebind --restore [--profile <p>] | rebind --reset switch a RUNNING profile's account (macOS; no <account> = interactive picker; --restore reverses; --reset re-enables after a circuit-breaker trip)
|
|
1751
2326
|
agent-switch web <name> claude.ai in a persistent browser (Claude)
|
|
1752
|
-
agent-switch remove [--provider P] <name> [--force] delete a profile
|
|
2327
|
+
agent-switch remove [--provider P] <name> [--force] [--yes] delete a profile (asks on a terminal; --yes skips)
|
|
1753
2328
|
agent-switch label [--provider P] <name> [Work|Personal|Other|none] tag a profile
|
|
2329
|
+
agent-switch desktop [--json] which accounts ran sessions in each Claude Desktop instance
|
|
2330
|
+
agent-switch activate [--json] make this CLI the one terminals and the background service use
|
|
2331
|
+
agent-switch disable|enable [--provider P] <name> keep a profile but stop using it / turn it back on
|
|
2332
|
+
agent-switch color [--provider P] <name> [orange|red|yellow|green|teal|blue|purple|pink|none] pick a profile's colour
|
|
1754
2333
|
agent-switch autoswitch on|off|status [--provider P] [--threshold <1-100>] notify + suggest near a usage limit; never switches automatically (default OFF)
|
|
1755
|
-
agent-switch reset <profile> --provider codex
|
|
2334
|
+
agent-switch reset <profile> --provider codex [--yes] redeem one banked Codex rate-limit reset (asks on a terminal)
|
|
2335
|
+
agent-switch protection [--json] | protection set <provider/name> [--on|--off] [--threshold N] per-profile limit warning (default on, 98%)
|
|
2336
|
+
agent-switch pool [list|create|rename|delete|add|remove|suggest] [--json] optional groups a folder rule can point at
|
|
2337
|
+
agent-switch auth-status [--provider P] [--json] per-profile login state from the provider's store (no tokens)
|
|
2338
|
+
agent-switch activity [--limit N] [--json] what agent-switch itself did, and why
|
|
1756
2339
|
agent-switch rename <old> <new> [--provider P] rename a profile (name & keep its tag)
|
|
1757
2340
|
agent-switch providers enable|disable|status [--provider P] [--surface cli|ui] enable/disable a provider (default: Claude + Codex)
|
|
1758
2341
|
agent-switch [gui] launch the tray/menubar GUI (the default with no command; won't start a second if one is running)
|
|
@@ -1761,6 +2344,7 @@ Run with no command to launch the tray/menubar GUI (single-instance).
|
|
|
1761
2344
|
agent-switch apps list launchable GUI apps (macOS)
|
|
1762
2345
|
agent-switch open <app> [profile] launch a GUI app on a profile, isolated (macOS)
|
|
1763
2346
|
agent-switch shellenv [--shell zsh|bash|fish|powershell] shell integration
|
|
2347
|
+
agent-switch shellenv install [--shell S] [--json] add it to your shell's startup file
|
|
1764
2348
|
agent-switch service run|start|stop|status|install|uninstall background usage daemon
|
|
1765
2349
|
agent-switch notifications [clear] [--json] recent notifications (usage-limit suggestions, fetch failures)
|
|
1766
2350
|
agent-switch notify --kind K --title T --message M [--json] record a notification event
|
|
@@ -1769,7 +2353,8 @@ Run with no command to launch the tray/menubar GUI (single-instance).
|
|
|
1769
2353
|
agent-switch usage history [--provider P] [--profile <name>] [--json] daemon-recorded usage samples per profile
|
|
1770
2354
|
agent-switch tooling [--json] ecosystem tool readout (agent-config, rtk, provider CLIs)
|
|
1771
2355
|
agent-switch tooling install|upgrade <tool> install/upgrade an ecosystem tool (agent-config, rtk, claude, codex)
|
|
1772
|
-
agent-switch
|
|
2356
|
+
agent-switch tooling notes [<tool>] [--latest] [--json] read an ecosystem tool's release notes (default agent-config; fetches from GitHub)
|
|
2357
|
+
agent-switch doctor [--json] per-OS, per-provider self-check`);
|
|
1773
2358
|
}
|
|
1774
2359
|
/**
|
|
1775
2360
|
* The interactive limit dialog (Phase 4) — `agent-switch rebind` with no account
|
|
@@ -1842,6 +2427,7 @@ async function rebindDialog(running) {
|
|
|
1842
2427
|
}
|
|
1843
2428
|
try {
|
|
1844
2429
|
const r = await rebind({ account: choice.profile, profile: running });
|
|
2430
|
+
logActivity("rebind", `claude/${running}`, `now serves ${choice.profile}'s account`, "user picked it in the rebind dialog");
|
|
1845
2431
|
console.log(r.uxNote);
|
|
1846
2432
|
}
|
|
1847
2433
|
catch (e) {
|
|
@@ -1858,7 +2444,26 @@ async function rebindDialog(running) {
|
|
|
1858
2444
|
async function cmdRebind(providerId, providerExplicit, account, flags) {
|
|
1859
2445
|
if (providerExplicit && providerId !== "claude")
|
|
1860
2446
|
die(`rebind supports claude only (not ${providerId})`);
|
|
1861
|
-
|
|
2447
|
+
// Resolve which running profile rebind targets: explicit --profile wins; else
|
|
2448
|
+
// prefer the profile that actually has a LIVE session (rebind's whole point is
|
|
2449
|
+
// the running session, which may not be the "active" default-for-new-processes
|
|
2450
|
+
// profile). Only auto-target when exactly one profile is running — a credential
|
|
2451
|
+
// write must never guess across ambiguity — otherwise keep the active fallback.
|
|
2452
|
+
const explicitProfile = typeof flags.profile === "string" ? flags.profile : undefined;
|
|
2453
|
+
let profile = explicitProfile ?? activeFor("claude") ?? undefined;
|
|
2454
|
+
if (!explicitProfile) {
|
|
2455
|
+
const running = detectRunningClaudeProfile(listProfiles("claude"), (p) => configDir("claude", p));
|
|
2456
|
+
if (running.kind === "single") {
|
|
2457
|
+
if (running.profile !== profile) {
|
|
2458
|
+
console.log(`Auto-detected the running claude profile "${running.profile}" (its session is live) — targeting it. Pass --profile to override.`);
|
|
2459
|
+
}
|
|
2460
|
+
profile = running.profile;
|
|
2461
|
+
}
|
|
2462
|
+
else if (running.kind === "ambiguous") {
|
|
2463
|
+
console.log(`Multiple running claude profiles (${running.profiles.join(", ")}) — ` +
|
|
2464
|
+
`${profile ? `using the active profile "${profile}"` : "no active default"}; pass --profile <name> to target a specific one.`);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
1862
2467
|
if (flags.reset) {
|
|
1863
2468
|
// Kill-switch reset (ADR-003): clear `disabled` + the failure counter so a
|
|
1864
2469
|
// circuit-breaker trip (or a manual disable) no longer refuses rebind.
|
|
@@ -1872,6 +2477,7 @@ async function cmdRebind(providerId, providerExplicit, account, flags) {
|
|
|
1872
2477
|
requireProfile("claude", profile, "rebind --restore --profile");
|
|
1873
2478
|
try {
|
|
1874
2479
|
const r = await restoreRebind({ profile });
|
|
2480
|
+
logActivity("rebind", `claude/${profile}`, `restored own account (was on ${r.wasBoundTo})`, "user ran `agent-switch rebind --restore`");
|
|
1875
2481
|
console.log(`Restored "${r.restoredProfile}" to its own account (was rebound to "${r.wasBoundTo}").`);
|
|
1876
2482
|
}
|
|
1877
2483
|
catch (e) {
|
|
@@ -1894,12 +2500,228 @@ async function cmdRebind(providerId, providerExplicit, account, flags) {
|
|
|
1894
2500
|
requireProfile("claude", profile, "rebind --profile");
|
|
1895
2501
|
try {
|
|
1896
2502
|
const r = await rebind({ account, profile });
|
|
2503
|
+
logActivity("rebind", `claude/${profile}`, `now serves ${account}'s account`, "user ran `agent-switch rebind`");
|
|
1897
2504
|
console.log(r.uxNote);
|
|
1898
2505
|
}
|
|
1899
2506
|
catch (e) {
|
|
1900
2507
|
die(e.message);
|
|
1901
2508
|
}
|
|
1902
2509
|
}
|
|
2510
|
+
// ---------- gui-settings / activity / auth-status / protection / pool ----------
|
|
2511
|
+
/** `gui-settings [--json]` · `gui-settings set pet-welcomed` ·
|
|
2512
|
+
* `gui-settings set autostart enabled|disabled --by first-run|user` ·
|
|
2513
|
+
* `gui-settings set autostart-degraded --reason <text>|none`. The GUI's
|
|
2514
|
+
* file-backed preferences (see gui-settings.ts); prints the result as JSON. */
|
|
2515
|
+
function cmdGuiSettings(sub, args, flags) {
|
|
2516
|
+
const usageLine = "usage: agent-switch gui-settings [--json] | gui-settings set pet-welcomed | set autostart enabled|disabled --by first-run|user | set autostart-degraded --reason <text>|none";
|
|
2517
|
+
if (sub === undefined || sub === "get" || sub === "status") {
|
|
2518
|
+
console.log(JSON.stringify(readGuiSettings(), null, 2));
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
if (sub !== "set")
|
|
2522
|
+
die(usageLine);
|
|
2523
|
+
const [key, value] = args;
|
|
2524
|
+
let u;
|
|
2525
|
+
if (key === "pet-welcomed")
|
|
2526
|
+
u = { key };
|
|
2527
|
+
else if (key === "autostart" && (value === "enabled" || value === "disabled")) {
|
|
2528
|
+
const by = flags.by === "first-run" || flags.by === "user" ? flags.by : die(usageLine);
|
|
2529
|
+
u = { key, state: value, by };
|
|
2530
|
+
}
|
|
2531
|
+
else if (key === "autostart-degraded") {
|
|
2532
|
+
const reason = typeof flags.reason === "string" ? flags.reason : die(usageLine);
|
|
2533
|
+
u = { key, reason: reason === "none" ? null : reason };
|
|
2534
|
+
}
|
|
2535
|
+
else
|
|
2536
|
+
die(usageLine);
|
|
2537
|
+
const before = readGuiSettings();
|
|
2538
|
+
let after;
|
|
2539
|
+
try {
|
|
2540
|
+
after = applyGuiSetting(before, u);
|
|
2541
|
+
}
|
|
2542
|
+
catch (e) {
|
|
2543
|
+
die(e.message);
|
|
2544
|
+
}
|
|
2545
|
+
writeGuiSettings(after);
|
|
2546
|
+
if (u.key === "autostart" && JSON.stringify(after.autostart) !== JSON.stringify(before.autostart)) {
|
|
2547
|
+
logActivity("autostart", null, `start at login ${u.state}`, u.by === "user" ? "user changed it in the desktop app" : "first-run default, confirmed with the OS");
|
|
2548
|
+
}
|
|
2549
|
+
if (u.key === "autostart-degraded" && u.reason) {
|
|
2550
|
+
logActivity("autostart", null, "start at login could not be confirmed", "first-run default; retried next launch", false);
|
|
2551
|
+
}
|
|
2552
|
+
console.log(JSON.stringify(after, null, 2));
|
|
2553
|
+
}
|
|
2554
|
+
function cmdActivity(flags) {
|
|
2555
|
+
const limit = typeof flags.limit === "string" ? Number(flags.limit) : undefined;
|
|
2556
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1))
|
|
2557
|
+
die("--limit must be a whole number of at least 1");
|
|
2558
|
+
const entries = readActivity(limit ?? (flags.json ? undefined : 50));
|
|
2559
|
+
if (flags.json) {
|
|
2560
|
+
console.log(JSON.stringify({ entries, capacity: ACTIVITY_MAX_LINES }, null, 2));
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (entries.length === 0) {
|
|
2564
|
+
console.log("No activity recorded yet.");
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
for (const e of entries) {
|
|
2568
|
+
console.log(`${e.at} ${e.ok ? " " : "!"} ${e.kind.padEnd(10)} ${e.target ?? "-"} ${e.action} — ${e.reason}`);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
function authStatusFor(providerId, name) {
|
|
2572
|
+
const now = Date.now();
|
|
2573
|
+
const cfg = provider(providerId).configDirFor(configDir(providerId, name));
|
|
2574
|
+
if (providerId === "claude") {
|
|
2575
|
+
const file = claudeCredentialFile(cfg);
|
|
2576
|
+
const fromFile = readText(file);
|
|
2577
|
+
return claudeAuthStatus(name, credentials.read(cfg), fromFile !== null ? fileMtimeMs(file) : null, now);
|
|
2578
|
+
}
|
|
2579
|
+
if (providerId === "codex")
|
|
2580
|
+
return codexAuthStatus(name, readText(provider("codex").credentialPath(cfg)), now);
|
|
2581
|
+
return antigravityAuthStatus(name, fs.existsSync(provider("antigravity").credentialPath(cfg)));
|
|
2582
|
+
}
|
|
2583
|
+
function cmdAuthStatus(providerId, json) {
|
|
2584
|
+
const rows = (providerId ? listProfiles(providerId).map((n) => ({ provider: providerId, name: n })) : listAllProfiles()).map((r) => authStatusFor(r.provider, r.name));
|
|
2585
|
+
if (json) {
|
|
2586
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
if (rows.length === 0)
|
|
2590
|
+
return void console.log("No profiles.");
|
|
2591
|
+
for (const r of rows) {
|
|
2592
|
+
const exp = r.expiresAt ? `, ends ${r.expiresAt}` : "";
|
|
2593
|
+
console.log(`${`${r.provider}/${r.name}`.padEnd(24)} ${r.state}${exp} — ${r.detail}`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
function parseMemberArg(v, usageLine) {
|
|
2597
|
+
const m = v ? parseMember(v) : null;
|
|
2598
|
+
if (!m)
|
|
2599
|
+
die(usageLine);
|
|
2600
|
+
return m;
|
|
2601
|
+
}
|
|
2602
|
+
function cmdProtection(sub, args, flags) {
|
|
2603
|
+
const usageLine = "usage: agent-switch protection [--json] | protection set <provider/name> [--on|--off] [--threshold <1-100>]";
|
|
2604
|
+
if (sub === undefined || sub === "status" || sub === "list") {
|
|
2605
|
+
const rows = listAllProfiles().map((p) => ({ profile: `${p.provider}/${p.name}`, ...readProtection(p.provider, p.name) }));
|
|
2606
|
+
if (flags.json)
|
|
2607
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
2608
|
+
if (rows.length === 0)
|
|
2609
|
+
return void console.log("No profiles.");
|
|
2610
|
+
for (const r of rows)
|
|
2611
|
+
console.log(`${r.profile.padEnd(24)} ${r.enabled ? "on " : "off"} at ${r.thresholdPct}%`);
|
|
2612
|
+
console.log("\nA warning when a profile reaches its threshold — nothing is paused or switched.");
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
if (sub !== "set")
|
|
2616
|
+
die(usageLine);
|
|
2617
|
+
const m = parseMemberArg(args[0], usageLine);
|
|
2618
|
+
requireProfile(m.provider, m.name, "protection set");
|
|
2619
|
+
if (flags.on && flags.off)
|
|
2620
|
+
die("pass either --on or --off, not both");
|
|
2621
|
+
const threshold = typeof flags.threshold === "string" ? Number(flags.threshold) : undefined;
|
|
2622
|
+
if (threshold !== undefined && !(Number.isInteger(threshold) && threshold >= 1 && threshold <= 100))
|
|
2623
|
+
die("--threshold must be a whole number between 1 and 100");
|
|
2624
|
+
if (!flags.on && !flags.off && threshold === undefined)
|
|
2625
|
+
die(usageLine);
|
|
2626
|
+
const next = setProtection(m.provider, m.name, {
|
|
2627
|
+
...(flags.on ? { enabled: true } : {}),
|
|
2628
|
+
...(flags.off ? { enabled: false } : {}),
|
|
2629
|
+
...(threshold !== undefined ? { thresholdPct: threshold } : {}),
|
|
2630
|
+
});
|
|
2631
|
+
logActivity("protection", `${m.provider}/${m.name}`, `${next.enabled ? "on" : "off"} at ${next.thresholdPct}%`, "user ran `agent-switch protection set`");
|
|
2632
|
+
if (flags.json)
|
|
2633
|
+
return void console.log(JSON.stringify({ profile: `${m.provider}/${m.name}`, ...next }, null, 2));
|
|
2634
|
+
console.log(`Session protection for ${m.provider}/${m.name}: ${next.enabled ? "on" : "off"} at ${next.thresholdPct}%.`);
|
|
2635
|
+
}
|
|
2636
|
+
function poolMappings(id) {
|
|
2637
|
+
return mappingRows().filter((r) => r.name === `${POOL_PREFIX}${id}`).map((r) => `${r.path} (${r.provider})`);
|
|
2638
|
+
}
|
|
2639
|
+
function cmdPool(sub, args, flags) {
|
|
2640
|
+
const usageLine = "usage: agent-switch pool [list] [--json] | pool create <name> | pool rename <id> <name> | pool delete <id> | pool add <id> <provider/name> | pool remove <id> <provider/name> | pool suggest [--confirm] [--json]";
|
|
2641
|
+
const json = !!flags.json;
|
|
2642
|
+
const out = (v, text) => console.log(json ? JSON.stringify(v, null, 2) : text);
|
|
2643
|
+
try {
|
|
2644
|
+
switch (sub) {
|
|
2645
|
+
case undefined:
|
|
2646
|
+
case "list": {
|
|
2647
|
+
const pools = readPools().map((p) => ({ ...p, rules: poolMappings(p.id) }));
|
|
2648
|
+
if (json)
|
|
2649
|
+
return void console.log(JSON.stringify(pools, null, 2));
|
|
2650
|
+
if (pools.length === 0)
|
|
2651
|
+
return void console.log("No pools. Pools are optional — create one with: agent-switch pool create <name>");
|
|
2652
|
+
for (const p of pools) {
|
|
2653
|
+
console.log(`${p.id} "${p.name}" ${p.members.length ? p.members.join(", ") : "(no members)"}${p.rules.length ? ` · folders: ${p.rules.join(", ")}` : ""}`);
|
|
2654
|
+
}
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
case "create": {
|
|
2658
|
+
if (!args[0])
|
|
2659
|
+
die(usageLine);
|
|
2660
|
+
const p = createPool(args.join(" "));
|
|
2661
|
+
logActivity("pool", p.id, "created", "user ran `agent-switch pool create`");
|
|
2662
|
+
return out(p, `Created pool ${p.id} ("${p.name}"). Add members: agent-switch pool add ${p.id} <provider/name>`);
|
|
2663
|
+
}
|
|
2664
|
+
case "rename": {
|
|
2665
|
+
if (!args[0] || !args[1])
|
|
2666
|
+
die(usageLine);
|
|
2667
|
+
const p = renamePool(args[0], args.slice(1).join(" "));
|
|
2668
|
+
logActivity("pool", p.id, `renamed to "${p.name}"`, "user ran `agent-switch pool rename`");
|
|
2669
|
+
return out(p, `Pool ${p.id} is now called "${p.name}" (its id and folder rules are unchanged).`);
|
|
2670
|
+
}
|
|
2671
|
+
case "delete": {
|
|
2672
|
+
if (!args[0])
|
|
2673
|
+
die(usageLine);
|
|
2674
|
+
const rules = poolMappings(args[0]);
|
|
2675
|
+
if (rules.length > 0)
|
|
2676
|
+
die(`pool "${args[0]}" is used by folder rules: ${rules.join(", ")}. Remove them first (agent-switch unmap <dir>).`);
|
|
2677
|
+
deletePool(args[0]);
|
|
2678
|
+
logActivity("pool", args[0], "deleted", "user ran `agent-switch pool delete`");
|
|
2679
|
+
return out({ deleted: args[0] }, `Deleted pool ${args[0]}. Its profiles are untouched.`);
|
|
2680
|
+
}
|
|
2681
|
+
case "add":
|
|
2682
|
+
case "remove": {
|
|
2683
|
+
if (!args[0])
|
|
2684
|
+
die(usageLine);
|
|
2685
|
+
const m = parseMemberArg(args[1], usageLine);
|
|
2686
|
+
if (sub === "add")
|
|
2687
|
+
requireProfile(m.provider, m.name, "pool add");
|
|
2688
|
+
const key = `${m.provider}/${m.name}`;
|
|
2689
|
+
const p = sub === "add" ? addPoolMember(args[0], key) : removePoolMember(args[0], key);
|
|
2690
|
+
logActivity("pool", p.id, `${sub === "add" ? "added" : "removed"} ${key}`, `user ran \`agent-switch pool ${sub}\``);
|
|
2691
|
+
return out(p, `${sub === "add" ? "Added" : "Removed"} ${key} ${sub === "add" ? "to" : "from"} pool ${p.id}.`);
|
|
2692
|
+
}
|
|
2693
|
+
case "suggest": {
|
|
2694
|
+
const proposals = suggestPoolsFromLabels();
|
|
2695
|
+
if (!flags.confirm) {
|
|
2696
|
+
if (json)
|
|
2697
|
+
return void console.log(JSON.stringify({ preview: true, proposals }, null, 2));
|
|
2698
|
+
if (proposals.length === 0)
|
|
2699
|
+
return void console.log("No labelled profiles — nothing to suggest.");
|
|
2700
|
+
console.log("Preview — nothing is created until you run `agent-switch pool suggest --confirm`:");
|
|
2701
|
+
for (const p of proposals)
|
|
2702
|
+
console.log(` ${p.id} ${p.members.join(", ")}${p.exists ? " (exists — skipped)" : ""}`);
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
const created = [];
|
|
2706
|
+
for (const prop of proposals) {
|
|
2707
|
+
if (prop.exists)
|
|
2708
|
+
continue;
|
|
2709
|
+
const pool = createPool(prop.name);
|
|
2710
|
+
for (const m of prop.members)
|
|
2711
|
+
addPoolMember(pool.id, m);
|
|
2712
|
+
created.push(pool.id);
|
|
2713
|
+
logActivity("pool", pool.id, `created from label ${prop.name}`, "user confirmed `agent-switch pool suggest`");
|
|
2714
|
+
}
|
|
2715
|
+
return out({ created }, created.length ? `Created pools: ${created.join(", ")}.` : "Nothing to create.");
|
|
2716
|
+
}
|
|
2717
|
+
default:
|
|
2718
|
+
die(usageLine);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
catch (e) {
|
|
2722
|
+
die(e.message);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
1903
2725
|
// ---------- main -------------------------------------------------------------
|
|
1904
2726
|
async function main() {
|
|
1905
2727
|
const argv = process.argv.slice(2);
|
|
@@ -1920,19 +2742,29 @@ async function main() {
|
|
|
1920
2742
|
const moved = migrateLegacyLayout();
|
|
1921
2743
|
if (moved.length > 0) {
|
|
1922
2744
|
// stderr, not stdout: `dir` is machine-consumed by the shell wrapper
|
|
1923
|
-
// (`dir="$(agent-switch dir
|
|
1924
|
-
// would pollute the resolved config path.
|
|
2745
|
+
// (`dir="$(agent-switch dir --provider … --guard)"`), so a status line on
|
|
2746
|
+
// stdout would pollute the resolved config path. Since the single-load
|
|
2747
|
+
// guard needs to speak, the wrapper no longer discards stderr — this
|
|
2748
|
+
// one-time migration note is now visible to the user, which is right.
|
|
1925
2749
|
console.error(`Migrated ${moved.length} Claude profile(s) to the new layout: ${moved.join(", ")}.`);
|
|
1926
2750
|
}
|
|
1927
2751
|
}
|
|
1928
2752
|
switch (cmd) {
|
|
1929
|
-
case "add": return cmdAdd(providerId, positional[0]);
|
|
2753
|
+
case "add": return cmdAdd(providerId, positional[0], typeof flags.browser === "string" ? flags.browser : undefined);
|
|
2754
|
+
case "login": return cmdLogin(providerId, positional[0], typeof flags.browser === "string" ? flags.browser : undefined);
|
|
2755
|
+
case "browsers": return cmdBrowsers(!!flags.json);
|
|
1930
2756
|
case "import": return cmdImport(providerId, positional[0]);
|
|
1931
2757
|
case "use": return cmdUse(providerId, positional[0]);
|
|
1932
2758
|
case "deactivate": return cmdDeactivate(providerId);
|
|
1933
2759
|
case "label": return cmdLabel(providerId, positional[0], positional[1]);
|
|
2760
|
+
case "color":
|
|
2761
|
+
case "colour": return cmdColor(providerId, positional[0], positional[1]);
|
|
2762
|
+
case "activate": return cmdActivate(!!flags.json);
|
|
2763
|
+
case "desktop": return cmdDesktop(!!flags.json);
|
|
2764
|
+
case "disable": return cmdDisable(providerId, positional[0], true);
|
|
2765
|
+
case "enable": return cmdDisable(providerId, positional[0], false);
|
|
1934
2766
|
case "autoswitch": return cmdAutoswitch(providerId, providerExplicit, positional[0], flags);
|
|
1935
|
-
case "reset": return cmdReset(providerId, positional[0]);
|
|
2767
|
+
case "reset": return cmdReset(providerId, positional[0], !!flags.yes);
|
|
1936
2768
|
case "rename": return cmdRename(providerId, positional[0], positional[1]);
|
|
1937
2769
|
case "providers": return cmdProviders(providerId, providerExplicit, positional[0], flags);
|
|
1938
2770
|
case "gui": return cmdGui();
|
|
@@ -1947,8 +2779,14 @@ async function main() {
|
|
|
1947
2779
|
case "os-notify": return cmdOsNotify(positional[0], !!flags.json);
|
|
1948
2780
|
case "uninstall": return cmdUninstall(flags);
|
|
1949
2781
|
case "run": {
|
|
1950
|
-
|
|
1951
|
-
|
|
2782
|
+
let r;
|
|
2783
|
+
try {
|
|
2784
|
+
r = parseRun(rest);
|
|
2785
|
+
}
|
|
2786
|
+
catch (e) {
|
|
2787
|
+
die(e.message);
|
|
2788
|
+
}
|
|
2789
|
+
return cmdRun(r.providerId, r.name, r.args, r.cwd);
|
|
1952
2790
|
}
|
|
1953
2791
|
case "handoff": return cmdHandoff(providerId, positional[0], positional.slice(1), flags);
|
|
1954
2792
|
case "list":
|
|
@@ -1956,10 +2794,11 @@ async function main() {
|
|
|
1956
2794
|
case "status": return cmdStatus(providerExplicit ? providerId : undefined, positional[0], !!flags.json);
|
|
1957
2795
|
case "current": return cmdCurrent(providerExplicit ? providerId : undefined);
|
|
1958
2796
|
case "whoami": return cmdWhoami(providerId, positional[0]);
|
|
1959
|
-
case "dir": return cmdDir(providerId);
|
|
1960
|
-
case "
|
|
2797
|
+
case "dir": return cmdDir(providerId, !!flags.guard, !!flags.json, !!flags.plain);
|
|
2798
|
+
case "guard": return cmdGuard(positional[0], !!flags.json);
|
|
2799
|
+
case "map": return cmdMap(providerId, positional[0], positional[1], flags);
|
|
1961
2800
|
case "unmap": return cmdUnmap(providerExplicit ? providerId : undefined, positional[0]);
|
|
1962
|
-
case "mappings": return cmdMappings();
|
|
2801
|
+
case "mappings": return cmdMappings(rest.includes("--json"));
|
|
1963
2802
|
case "share": return cmdShare(positional[0], rest.slice(1));
|
|
1964
2803
|
case "config": return cmdConfig(positional[0], positional[1], flags);
|
|
1965
2804
|
case "sessions":
|
|
@@ -1977,19 +2816,30 @@ async function main() {
|
|
|
1977
2816
|
case "rebind": return cmdRebind(providerId, providerExplicit, positional[0], flags);
|
|
1978
2817
|
case "web": return cmdWeb(positional[0]);
|
|
1979
2818
|
case "remove":
|
|
1980
|
-
case "rm": return cmdRemove(providerId, positional[0], !!flags.force);
|
|
1981
|
-
case "shellenv":
|
|
2819
|
+
case "rm": return cmdRemove(providerId, positional[0], !!flags.force, !!flags.yes);
|
|
2820
|
+
case "shellenv":
|
|
2821
|
+
return positional[0] === "install"
|
|
2822
|
+
? cmdShellenv(flags.shell, "install", !!flags.json)
|
|
2823
|
+
: cmdShellenv(flags.shell ?? positional[0]);
|
|
1982
2824
|
case "service": return cmdService(positional[0]);
|
|
1983
2825
|
case "usage": return cmdUsageHistory(providerId, positional[0], flags);
|
|
1984
2826
|
case "tooling":
|
|
1985
2827
|
if (positional[0] === "install" || positional[0] === "upgrade") {
|
|
1986
2828
|
return cmdToolingAction(positional[0], positional[1]);
|
|
1987
2829
|
}
|
|
2830
|
+
if (positional[0] === "notes")
|
|
2831
|
+
return cmdToolingNotes(positional[1], flags);
|
|
1988
2832
|
if (positional[0] !== undefined) {
|
|
1989
|
-
die(`usage: agent-switch tooling [install|upgrade <${TOOL_IDS.join("|")}>] [--json]`);
|
|
2833
|
+
die(`usage: agent-switch tooling [install|upgrade <${TOOL_IDS.join("|")}>] [notes [<tool>]] [--json]`);
|
|
1990
2834
|
}
|
|
1991
2835
|
return cmdTooling(!!flags.json);
|
|
1992
|
-
case "doctor": return process.exit(await runDoctor());
|
|
2836
|
+
case "doctor": return process.exit(await runDoctor(!!flags.json));
|
|
2837
|
+
case "auth-status": return cmdAuthStatus(providerExplicit ? providerId : undefined, !!flags.json);
|
|
2838
|
+
case "activity": return cmdActivity(flags);
|
|
2839
|
+
case "protection": return cmdProtection(positional[0], positional.slice(1), flags);
|
|
2840
|
+
case "pool":
|
|
2841
|
+
case "pools": return cmdPool(positional[0], positional.slice(1), flags);
|
|
2842
|
+
case "gui-settings": return cmdGuiSettings(positional[0], positional.slice(1), flags);
|
|
1993
2843
|
case "help":
|
|
1994
2844
|
case "--help":
|
|
1995
2845
|
case "-h": return usage();
|