@event4u/agent-switch 2.1.0 → 2.3.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 +166 -73
- package/dist/activity.js +99 -0
- package/dist/args.js +23 -3
- package/dist/auth-status.js +159 -0
- package/dist/balance.js +55 -0
- package/dist/browsers.js +90 -0
- package/dist/daemon.js +95 -42
- package/dist/desktop-sessions.js +115 -0
- package/dist/desktop-settings.js +90 -0
- package/dist/dir-resolve.js +178 -0
- package/dist/doctor.js +239 -69
- package/dist/gui-settings.js +67 -0
- package/dist/history.js +20 -0
- package/dist/hooks.js +21 -0
- package/dist/index.js +879 -68
- package/dist/launchers.js +228 -0
- package/dist/locks.js +61 -0
- package/dist/mappings.js +48 -9
- package/dist/pools.js +157 -0
- package/dist/profiles.js +181 -67
- package/dist/protection.js +100 -0
- package/dist/rebind.js +115 -12
- package/dist/service.js +40 -9
- package/dist/sessions.js +82 -13
- package/dist/share.js +92 -13
- package/dist/shellenv.js +122 -21
- package/dist/single-load.js +5 -0
- package/dist/token-freshen.js +11 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,45 +15,60 @@ 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, readGuard, setGuard, 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, setSharesDefaultLogin, sharesDefaultLogin, 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";
|
|
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, keptFreshByFor, } from "./auth-status.js";
|
|
30
37
|
import { TOOL_IDS, checkTooling, formatToolNotes, formatToolingLines, runToolAction, toolNotesRefusal, toolReleaseRepo, } from "./tooling.js";
|
|
31
38
|
import { releaseNoteBlocks } from "./release-notes.js";
|
|
32
|
-
import { readHistory, recordHistorySample } from "./history.js";
|
|
39
|
+
import { LATEST_FILE, readHistory, readLatestSnapshot, recordHistorySample, writeLatestSnapshot } from "./history.js";
|
|
33
40
|
import { launchGui } from "./gui-launch.js";
|
|
34
41
|
import { GUARD_POLICIES, GUARD_REFUSED_EXIT, OVERRIDE_ENV, guardVerdict, inspectLoad, isGuardPolicy, overrideFromEnv, renderGuardMessage, } from "./single-load.js";
|
|
35
42
|
import { checkForUpdate, fetchReleases, releasesNewerThan, selfUpdate } from "./updates.js";
|
|
36
|
-
import { mappingRows, pruneMappings, removeMapping, resolveMapping, setMapping, } from "./mappings.js";
|
|
43
|
+
import { mappingRows, pruneMappings, removeMapping, resolveMapping, setMapping, setPaused, normalizePath, } from "./mappings.js";
|
|
37
44
|
import { checkAuth, fetchProfile, fetchUsage, liveSessionPids, } from "./api.js";
|
|
38
|
-
import { defaultFreshenDeps,
|
|
45
|
+
import { defaultFreshenDeps, usageToken } from "./token-freshen.js";
|
|
39
46
|
import { formatSnapshot, parseUsage } from "./usage.js";
|
|
40
47
|
import { readCodexUsage } from "./codex-usage.js";
|
|
41
48
|
import { redeemResetCredit } from "./codex-reset.js";
|
|
42
|
-
import { assertValidSessionId, cleanupForkVehicle, codexSessionCommand, deleteSession, detectRunningClaudeProfile, 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";
|
|
43
50
|
import { isFresh, readDaemonState, readPid, PIDFILE, processAlive } from "./daemon.js";
|
|
44
|
-
import { cmdService, serviceUninstall } from "./service.js";
|
|
51
|
+
import { cmdService, serviceAfterUpdate, serviceInstall, serviceInstallFromDisk, serviceTarget, serviceUninstall } from "./service.js";
|
|
52
|
+
import { fileURLToPath } from "node:url";
|
|
45
53
|
import { readContext, turnInFlight } from "./telemetry.js";
|
|
46
54
|
import { readPreview } from "./session-preview.js";
|
|
47
|
-
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";
|
|
48
56
|
import { readTelemetryConfig, writeTelemetryConfig } from "./notify.js";
|
|
49
57
|
import { appendNotification, readNotifications, clearNotifications } from "./notifications.js";
|
|
50
58
|
import { APPS, buildLaunch, findApp, guiDataDir, isInstalled } from "./apps.js";
|
|
59
|
+
import { copyDesktopSettings, DEFAULT_DATA_DIR, hasSettingsLayout } from "./desktop-settings.js";
|
|
60
|
+
import { createLauncher, findRunning, focusRunning, readLaunchers, removeAllLaunchers, removeLauncher, removeLaunchersFor, renameLaunchersFor, repairLaunchers, } from "./launchers.js";
|
|
61
|
+
import { scanDesktop } from "./desktop-sessions.js";
|
|
51
62
|
import { ensureAgyKeychain } from "./agy-keychain.js";
|
|
52
63
|
import { currentManagedSession, currentTmuxSessionName, newSessionArgs, readTmuxRegistry, recordManagedSession, respawnPaneArgs, sendKeysArgs, tmuxAvailable, tmuxSessionName, } from "./tmux.js";
|
|
53
64
|
// Per-OS credential store (keychain-then-file on darwin, file-only elsewhere).
|
|
54
65
|
const credentials = credentialStore();
|
|
55
66
|
const CLAUDE_HOME = provider("claude").defaultConfigDir(); // ~/.claude
|
|
56
67
|
const CLAUDE_JSON = path.join(HOME, ".claude.json");
|
|
68
|
+
/** Record one of agent-switch's own actions in the activity log (never a token). */
|
|
69
|
+
function logActivity(kind, target, action, reason, ok = true) {
|
|
70
|
+
appendActivity({ kind, target, action, reason, ok });
|
|
71
|
+
}
|
|
57
72
|
/** Same-provider profiles that carry no live session — the concrete
|
|
58
73
|
* alternatives a refusal names, so the user can act on it without first
|
|
59
74
|
* running `list` themselves. */
|
|
@@ -95,9 +110,9 @@ function checkSingleLoad(providerId, name, opts = {}) {
|
|
|
95
110
|
};
|
|
96
111
|
}
|
|
97
112
|
// ---------- launcher ---------------------------------------------------------
|
|
98
|
-
function launch(p, name, args) {
|
|
113
|
+
function launch(p, name, args, extraEnv = {}) {
|
|
99
114
|
const home = configDir(p.id, name);
|
|
100
|
-
const env = { ...process.env, [p.envVar]: home };
|
|
115
|
+
const env = { ...process.env, ...extraEnv, [p.envVar]: home };
|
|
101
116
|
// Antigravity's agy CLI stores its token in the macOS keychain (fixed key) —
|
|
102
117
|
// seed a per-profile keychain under this HOME so the token is isolated per
|
|
103
118
|
// account (and macOS doesn't pop its "no keychain found" dialog). No-op off mac.
|
|
@@ -115,20 +130,114 @@ function launch(p, name, args) {
|
|
|
115
130
|
return res.status ?? 1;
|
|
116
131
|
}
|
|
117
132
|
// ---------- commands ---------------------------------------------------------
|
|
118
|
-
function cmdAdd(providerId, name) {
|
|
133
|
+
function cmdAdd(providerId, name, browserId) {
|
|
119
134
|
if (!name)
|
|
120
|
-
die(`usage: agent-switch add [--provider ${providerId}] <profile
|
|
135
|
+
die(`usage: agent-switch add [--provider ${providerId}] <profile> [--browser B]`);
|
|
121
136
|
if (profileExists(providerId, name))
|
|
122
137
|
die(`profile "${name}" already exists for ${providerId}`);
|
|
123
138
|
const p = provider(providerId);
|
|
139
|
+
const env = browserId ? browserEnv(browserId, providerId) : {};
|
|
124
140
|
ensureRoot();
|
|
125
|
-
|
|
141
|
+
const dir = configDir(providerId, name);
|
|
142
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
143
|
+
// A bare `claude` in a fresh config dir opens Claude Code's setup screens, not
|
|
144
|
+
// the sign-in; skip them the way `import` does and run the sign-in itself.
|
|
145
|
+
if (providerId === "claude")
|
|
146
|
+
seedClaudeOnboarding(dir);
|
|
126
147
|
console.log(`Created ${providerId} profile "${name}".`);
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
const loginArgs = LOGIN_ARGS[providerId];
|
|
149
|
+
console.log(loginArgs.length
|
|
150
|
+
? `Signing it in with \`${[p.binary, ...loginArgs].join(" ")}\` (finish it in the browser if one opens)...`
|
|
151
|
+
: `Launching ${p.binary} for first login (log in inside if prompted)...`);
|
|
152
|
+
const code = launch(p, name, loginArgs, env);
|
|
129
153
|
const id = identity(providerId, name);
|
|
130
|
-
|
|
131
|
-
|
|
154
|
+
const after = authStatusFor(providerId, name);
|
|
155
|
+
const ok = !!id || after.state === "connected" || after.state === "expiring";
|
|
156
|
+
logActivity("login", `${providerId}/${name}`, ok ? "signed in (new profile)" : "created, sign-in not confirmed", "user ran `agent-switch add`", ok);
|
|
157
|
+
const flag = providerId === "claude" ? "" : ` --provider ${providerId}`;
|
|
158
|
+
if (!ok && loginArgs.length) {
|
|
159
|
+
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}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
console.log(id ? `Profile "${name}" is signed in as ${id}.` : ok ? `Profile "${name}" is signed in.` : `Profile "${name}" created.`);
|
|
163
|
+
console.log(`Activate it with: agent-switch use ${name}${flag}`);
|
|
164
|
+
}
|
|
165
|
+
/** Mark Claude Code's first-run setup as done in a new profile (theme taken
|
|
166
|
+
* from the default install), so its first start goes straight to work. */
|
|
167
|
+
function seedClaudeOnboarding(dir) {
|
|
168
|
+
const file = path.join(dir, ".claude.json");
|
|
169
|
+
const config = (fs.existsSync(file) && readJson(file)) || {};
|
|
170
|
+
const theme = (fs.existsSync(CLAUDE_JSON) && readJson(CLAUDE_JSON)?.theme) || "dark";
|
|
171
|
+
config.hasCompletedOnboarding = true;
|
|
172
|
+
config.theme = config.theme || theme;
|
|
173
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* `$BROWSER` for a sign-in in one specific browser. Only Claude Code is verified
|
|
177
|
+
* to follow it; codex on macOS opens the system default regardless, so it is
|
|
178
|
+
* told rather than silently ignored.
|
|
179
|
+
*/
|
|
180
|
+
function browserEnv(browserId, providerId) {
|
|
181
|
+
if (process.platform === "win32")
|
|
182
|
+
die("--browser is not supported on Windows yet.");
|
|
183
|
+
const deps = defaultBrowserDeps(HOME);
|
|
184
|
+
const located = locateBrowser(browserId, deps);
|
|
185
|
+
if (!located) {
|
|
186
|
+
const installed = listBrowsers(deps).map((b) => b.id).join(", ") || "(none found)";
|
|
187
|
+
die(`browser "${browserId}" is not installed. Installed: ${installed}`);
|
|
188
|
+
}
|
|
189
|
+
if (providerId !== "claude") {
|
|
190
|
+
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.`);
|
|
191
|
+
}
|
|
192
|
+
if (process.platform !== "darwin")
|
|
193
|
+
return { BROWSER: located };
|
|
194
|
+
const wrapper = path.join(ROOT, "browsers", browserId);
|
|
195
|
+
fs.mkdirSync(path.dirname(wrapper), { recursive: true, mode: 0o700 });
|
|
196
|
+
fs.writeFileSync(wrapper, browserWrapperScript(located), { mode: 0o700 });
|
|
197
|
+
fs.chmodSync(wrapper, 0o700);
|
|
198
|
+
return { BROWSER: wrapper };
|
|
199
|
+
}
|
|
200
|
+
function cmdBrowsers(json) {
|
|
201
|
+
const list = listBrowsers(defaultBrowserDeps(HOME));
|
|
202
|
+
if (json)
|
|
203
|
+
return void console.log(JSON.stringify(list, null, 2));
|
|
204
|
+
if (list.length === 0)
|
|
205
|
+
return void console.log("No supported browsers found.");
|
|
206
|
+
for (const b of list)
|
|
207
|
+
console.log(`${b.id.padEnd(16)} ${b.name}`);
|
|
208
|
+
}
|
|
209
|
+
/** The provider's own sign-in command for an existing profile. Antigravity's
|
|
210
|
+
* agy has no separate login command; it prompts when it starts signed out. */
|
|
211
|
+
const LOGIN_ARGS = {
|
|
212
|
+
claude: ["auth", "login"],
|
|
213
|
+
codex: ["login"],
|
|
214
|
+
antigravity: [],
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Sign an existing profile in again — `add` refuses a name that exists, and a
|
|
218
|
+
* login the provider cleared (signed out, refresh window ended) needs exactly
|
|
219
|
+
* this. Runs the provider's own sign-in under the profile's config dir, then
|
|
220
|
+
* reads the stored login back: the identity in the config outlives a sign-out,
|
|
221
|
+
* so only the credential proves the sign-in happened.
|
|
222
|
+
*/
|
|
223
|
+
function cmdLogin(providerId, name, browserId) {
|
|
224
|
+
const n = requireProfile(providerId, name, "login");
|
|
225
|
+
const p = provider(providerId);
|
|
226
|
+
const env = browserId ? browserEnv(browserId, providerId) : {};
|
|
227
|
+
console.log(`Signing ${providerId}/${n} in with \`${[p.binary, ...LOGIN_ARGS[providerId]].join(" ")}\` (finish it in the browser if one opens)...`);
|
|
228
|
+
const code = launch(p, n, LOGIN_ARGS[providerId], env);
|
|
229
|
+
const after = authStatusFor(providerId, n);
|
|
230
|
+
const ok = after.state === "connected" || after.state === "expiring";
|
|
231
|
+
logActivity("login", `${providerId}/${n}`, ok ? "signed in again" : "sign-in not confirmed", "user ran `agent-switch login`", ok);
|
|
232
|
+
if (!ok) {
|
|
233
|
+
console.error(`${providerId}/${n} is still not signed in (${after.detail})${code !== 0 ? ` — ${p.binary} exited with ${code}` : ""}.`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
// Its own sign-in ends the lineage it shared with the default install.
|
|
237
|
+
if (sharesDefaultLogin(providerId, n))
|
|
238
|
+
setSharesDefaultLogin(providerId, n, false);
|
|
239
|
+
const id = identity(providerId, n);
|
|
240
|
+
console.log(`${providerId}/${n} is signed in${id ? ` as ${id}` : ""}.`);
|
|
132
241
|
}
|
|
133
242
|
/**
|
|
134
243
|
* Login-free import of the default Claude install into a profile. Takes Claude
|
|
@@ -158,6 +267,8 @@ async function importClaude(name) {
|
|
|
158
267
|
fs.writeFileSync(path.join(dst, ".credentials.json"), creds, { mode: 0o600 });
|
|
159
268
|
return creds !== null;
|
|
160
269
|
}));
|
|
270
|
+
if (seeded)
|
|
271
|
+
setSharesDefaultLogin("claude", name, true);
|
|
161
272
|
console.log(`Imported default Claude setup into profile "${name}".`);
|
|
162
273
|
console.log(seeded
|
|
163
274
|
? "Credentials were seeded — no re-login needed. Claude Code migrates the seed into\n" +
|
|
@@ -185,6 +296,8 @@ function importFileProvider(p, name) {
|
|
|
185
296
|
if (path.join(srcDir, file) === p.credentialPath(srcDir))
|
|
186
297
|
seeded = true;
|
|
187
298
|
}
|
|
299
|
+
if (seeded)
|
|
300
|
+
setSharesDefaultLogin(p.id, name, true);
|
|
188
301
|
console.log(`Imported default ${p.binary} setup into profile "${name}".`);
|
|
189
302
|
console.log(seeded
|
|
190
303
|
? `Credential seeded — no re-login needed.\nIMPORTANT: this profile shares one OAuth ` +
|
|
@@ -203,7 +316,10 @@ async function cmdImport(providerId, name) {
|
|
|
203
316
|
}
|
|
204
317
|
function cmdUse(providerId, name) {
|
|
205
318
|
const n = requireProfile(providerId, name, "use");
|
|
319
|
+
if (isDisabled(providerId, n))
|
|
320
|
+
die(`${n} is disabled — turn it back on first: agent-switch enable ${n}${providerId === "claude" ? "" : ` --provider ${providerId}`}`);
|
|
206
321
|
setActive(providerId, n);
|
|
322
|
+
logActivity("switch", `${providerId}/${n}`, "set as active profile", "user ran `agent-switch use`");
|
|
207
323
|
const id = identity(providerId, n);
|
|
208
324
|
console.log(`Active ${providerId} profile: ${n}${id ? ` (${id})` : ""}`);
|
|
209
325
|
console.log(`New \`${provider(providerId).binary}\` sessions use this profile. Running sessions are unaffected.`);
|
|
@@ -223,11 +339,17 @@ function cmdDeactivate(providerId) {
|
|
|
223
339
|
return;
|
|
224
340
|
}
|
|
225
341
|
setActive(providerId, null);
|
|
342
|
+
logActivity("switch", `${providerId}/${active}`, "deactivated", "user ran `agent-switch deactivate`");
|
|
226
343
|
console.log(`Deactivated ${providerId} profile "${active}". No ${providerId} profile is active.`);
|
|
227
344
|
console.log(`New \`${provider(providerId).binary}\` sessions fall back to the default install until you \`use\` one.`);
|
|
228
345
|
}
|
|
229
|
-
function cmdRun(providerId, name, args) {
|
|
346
|
+
function cmdRun(providerId, name, args, cwd) {
|
|
230
347
|
const n = requireProfile(providerId, name, "run");
|
|
348
|
+
if (cwd !== undefined) {
|
|
349
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory())
|
|
350
|
+
die(`no such folder: ${cwd}`);
|
|
351
|
+
process.chdir(cwd);
|
|
352
|
+
}
|
|
231
353
|
const p = provider(providerId);
|
|
232
354
|
// `--tmux` and `--force` are agent-switch flags, not passthrough flags — strip
|
|
233
355
|
// them before the rest reaches the provider binary.
|
|
@@ -287,6 +409,8 @@ function cmdList(providerId, json = false) {
|
|
|
287
409
|
name: n,
|
|
288
410
|
identity: identity(pid, n),
|
|
289
411
|
label: labelFor(pid, n),
|
|
412
|
+
color: colorOf(pid, n),
|
|
413
|
+
disabled: isDisabled(pid, n),
|
|
290
414
|
active: activeFor(pid) === n,
|
|
291
415
|
liveSessions: pid === "claude" ? liveSessionPids(configDir("claude", n)).length : 0,
|
|
292
416
|
})));
|
|
@@ -340,7 +464,7 @@ async function claudeSnapshot(name) {
|
|
|
340
464
|
if (cached)
|
|
341
465
|
return cached;
|
|
342
466
|
}
|
|
343
|
-
const token =
|
|
467
|
+
const token = usageToken(configDir("claude", name), defaultFreshenDeps(readBinaryPath("claude")), sharesDefaultLogin("claude", name));
|
|
344
468
|
if (!token)
|
|
345
469
|
return null;
|
|
346
470
|
const raw = await fetchUsage(token);
|
|
@@ -366,8 +490,11 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
366
490
|
// every displayed profile on each refresh, so recording the fresh snapshot
|
|
367
491
|
// here (throttled to ~hourly) fills the chart for all profiles without
|
|
368
492
|
// depending on the background daemon or an "active" profile being set.
|
|
369
|
-
if (usage)
|
|
493
|
+
if (usage) {
|
|
370
494
|
recordHistorySample(path.join(profileDir(pid, active), "usage-history.json"), usage);
|
|
495
|
+
// Unthrottled: `dir` balances on usage only while it is minutes old.
|
|
496
|
+
writeLatestSnapshot(path.join(profileDir(pid, active), LATEST_FILE), usage);
|
|
497
|
+
}
|
|
371
498
|
const wc = worstLiveContext(pid, active);
|
|
372
499
|
const context = wc
|
|
373
500
|
? { sessionId: wc.sessionId, pct: wc.pct, contextTokens: wc.contextTokens, windowTokens: wc.windowTokens, model: wc.model, confidence: wc.confidence }
|
|
@@ -401,9 +528,12 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
401
528
|
console.log(" (no usage readout for this provider — shows own usage only where available)");
|
|
402
529
|
continue;
|
|
403
530
|
}
|
|
404
|
-
const token =
|
|
531
|
+
const token = usageToken(configDir("claude", n), defaultFreshenDeps(readBinaryPath("claude")), sharesDefaultLogin("claude", n));
|
|
405
532
|
if (!token) {
|
|
406
|
-
|
|
533
|
+
const auth = authStatusFor("claude", n);
|
|
534
|
+
console.log(auth.state === "sign-in-needed"
|
|
535
|
+
? ` (signed out — sign in again: agent-switch login ${n})`
|
|
536
|
+
: " (credential not readable — profile may not have run yet)");
|
|
407
537
|
continue;
|
|
408
538
|
}
|
|
409
539
|
const [profileInfo, raw] = await Promise.all([fetchProfile(token), fetchUsage(token)]);
|
|
@@ -425,6 +555,61 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
425
555
|
}
|
|
426
556
|
}
|
|
427
557
|
}
|
|
558
|
+
/** Cached usage for one profile: the daemon's snapshot or the newest history
|
|
559
|
+
* sample, whichever is newer. Local reads only — `dir` runs on every start. */
|
|
560
|
+
function cachedUsage(providerId, name) {
|
|
561
|
+
const fromDaemonOrHistory = cachedUsageFromDaemonOrHistory(providerId, name);
|
|
562
|
+
// The app's own reads land here unthrottled; take whichever source is newest.
|
|
563
|
+
const latest = readLatestSnapshot(path.join(profileDir(providerId, name), LATEST_FILE));
|
|
564
|
+
return latest && (fromDaemonOrHistory.capturedMs === null || latest.capturedMs > fromDaemonOrHistory.capturedMs) ? latest : fromDaemonOrHistory;
|
|
565
|
+
}
|
|
566
|
+
function cachedUsageFromDaemonOrHistory(providerId, name) {
|
|
567
|
+
const daemonSnap = readDaemonState()?.profiles?.[`${providerId}/${name}`] ?? null;
|
|
568
|
+
const daemonMs = daemonSnap ? Date.parse(daemonSnap.capturedAt) : NaN;
|
|
569
|
+
const hist = readHistory(path.join(profileDir(providerId, name), "usage-history.json"));
|
|
570
|
+
const last = hist.length ? hist[hist.length - 1] : null;
|
|
571
|
+
const histMs = last ? Date.parse(last.at) : NaN;
|
|
572
|
+
if (last && !isNaN(histMs) && (isNaN(daemonMs) || histMs > daemonMs)) {
|
|
573
|
+
// History samples keep no reset time; the daemon's last one still holds
|
|
574
|
+
// while it lies ahead (a window's reset only moves once it has passed).
|
|
575
|
+
const resetOf = (key) => {
|
|
576
|
+
const r = daemonSnap?.windows.find((d) => d.key === key)?.resetsAt ?? null;
|
|
577
|
+
return r && Date.parse(r) > histMs ? r : null;
|
|
578
|
+
};
|
|
579
|
+
const windows = last.windows.map((w) => ({ key: w.key, label: w.key, utilization: w.utilization, resetsAt: resetOf(w.key) }));
|
|
580
|
+
return { snapshot: { windows, routines: null, capturedAt: last.at }, capturedMs: histMs };
|
|
581
|
+
}
|
|
582
|
+
return { snapshot: daemonSnap, capturedMs: isNaN(daemonMs) ? null : daemonMs };
|
|
583
|
+
}
|
|
584
|
+
function tildePath(p) {
|
|
585
|
+
return p === HOME || p.startsWith(HOME + path.sep) ? "~" + p.slice(HOME.length) : p;
|
|
586
|
+
}
|
|
587
|
+
/** Profile names are validated by `profileDir`, which exits on a bad name —
|
|
588
|
+
* a pool ref or a stale mapping must never kill the shell wrapper's hot path. */
|
|
589
|
+
function profileExistsSafe(providerId, name) {
|
|
590
|
+
return /^[A-Za-z0-9_-]+$/.test(name) && profileExists(providerId, name);
|
|
591
|
+
}
|
|
592
|
+
function resolveDir(providerId, plain = false) {
|
|
593
|
+
return resolveForDir({
|
|
594
|
+
providerId,
|
|
595
|
+
mapping: plain ? null : resolveMapping(process.cwd(), providerId),
|
|
596
|
+
names: listProfiles(providerId),
|
|
597
|
+
threshold: (n) => readProtection(providerId, n).thresholdPct,
|
|
598
|
+
active: activeFor(providerId),
|
|
599
|
+
exists: (n) => profileExistsSafe(providerId, n),
|
|
600
|
+
signedIn: (n) => identity(providerId, n) !== null,
|
|
601
|
+
disabled: (n) => isDisabled(providerId, n),
|
|
602
|
+
sharesDefaultLogin: (n) => profileExistsSafe(providerId, n) && sharesDefaultLogin(providerId, n),
|
|
603
|
+
pool: (id) => findPool(id),
|
|
604
|
+
usage: (n) => cachedUsage(providerId, n),
|
|
605
|
+
// Only providers that write a live-session signal can report one; the
|
|
606
|
+
// check runs for pool members only.
|
|
607
|
+
live: (n) => provider(providerId).hasLiveSessionSignal && liveSessionPids(configDir(providerId, n)).length > 0,
|
|
608
|
+
guard: readGuard(),
|
|
609
|
+
now: Date.now(),
|
|
610
|
+
displayPath: tildePath,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
428
613
|
/**
|
|
429
614
|
* Resolve which profile a shell wrapper should load: directory mapping (nearest
|
|
430
615
|
* ancestor of CWD) > active-for-provider. Empty output → the wrapper falls back
|
|
@@ -436,14 +621,26 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
436
621
|
* and talk to the human; a distinct exit code is what lets the wrapper block on
|
|
437
622
|
* a refusal without ever blocking on a missing or older `agent-switch`.
|
|
438
623
|
*/
|
|
439
|
-
function cmdDir(providerId, guarded = false) {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
624
|
+
function cmdDir(providerId, guarded = false, json = false, plain = false) {
|
|
625
|
+
const r = resolveDir(providerId, plain);
|
|
626
|
+
if (json) {
|
|
627
|
+
console.log(JSON.stringify({
|
|
628
|
+
profile: r.name ? `${providerId}/${r.name}` : null,
|
|
629
|
+
configDir: r.name ? configDir(providerId, r.name) : null,
|
|
630
|
+
why: r.why,
|
|
631
|
+
...(r.blocked ? { blocked: r.blocked } : {}),
|
|
632
|
+
}, null, 2));
|
|
633
|
+
if (r.blocked)
|
|
634
|
+
process.exit(GUARD_REFUSED_EXIT);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
// A folder rule that cannot be served refuses — starting on an account
|
|
638
|
+
// outside the rule is the one outcome the rule exists to prevent.
|
|
639
|
+
if (r.blocked) {
|
|
640
|
+
console.error(`agent-switch: not starting here — ${r.blocked}.`);
|
|
641
|
+
process.exit(GUARD_REFUSED_EXIT);
|
|
642
|
+
}
|
|
643
|
+
const name = r.name;
|
|
447
644
|
if (!name)
|
|
448
645
|
return;
|
|
449
646
|
console.log(configDir(providerId, name));
|
|
@@ -457,8 +654,28 @@ function cmdDir(providerId, guarded = false) {
|
|
|
457
654
|
if (guard.verdict === "refuse")
|
|
458
655
|
process.exit(GUARD_REFUSED_EXIT);
|
|
459
656
|
}
|
|
460
|
-
function cmdMap(providerId, name, dir) {
|
|
461
|
-
|
|
657
|
+
function cmdMap(providerId, name, dir, flags = {}) {
|
|
658
|
+
if (flags.pause || flags.resume) {
|
|
659
|
+
// `map --pause [dir]`: the positional is the folder, there is no profile.
|
|
660
|
+
const target = name ?? process.cwd();
|
|
661
|
+
const on = !!flags.pause;
|
|
662
|
+
if (!setPaused(target, providerId, on))
|
|
663
|
+
die(`no mapping for ${target} (${providerId})`);
|
|
664
|
+
console.log(on
|
|
665
|
+
? `Paused the ${providerId} rule for ${normalizePath(target)} — it stays listed but no longer picks the account. Resume: agent-switch map --resume ${target}`
|
|
666
|
+
: `Resumed the ${providerId} rule for ${normalizePath(target)}.`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (name && isPoolRef(name)) {
|
|
670
|
+
const pool = findPool(poolIdOf(name));
|
|
671
|
+
if (!pool)
|
|
672
|
+
die(`no pool "${poolIdOf(name)}" — create it with: agent-switch pool create <name>`);
|
|
673
|
+
const key = setMapping(dir ?? process.cwd(), providerId, `${POOL_PREFIX}${pool.id}`);
|
|
674
|
+
console.log(`Mapped ${key} -> pool ${pool.id} (${providerId})`);
|
|
675
|
+
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`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const n = requireProfile(providerId, name, `map [--provider ${providerId}] <profile|pool:<id>> [dir]`);
|
|
462
679
|
const key = setMapping(dir ?? process.cwd(), providerId, n);
|
|
463
680
|
console.log(`Mapped ${key} -> ${providerId}/${n}`);
|
|
464
681
|
console.log(`\`${provider(providerId).binary}\` in this directory (and subdirectories) now uses this profile.`);
|
|
@@ -470,14 +687,20 @@ function cmdUnmap(providerId, dir) {
|
|
|
470
687
|
else
|
|
471
688
|
die(`no mapping for ${target}${providerId ? ` (${providerId})` : ""}`);
|
|
472
689
|
}
|
|
473
|
-
function cmdMappings() {
|
|
690
|
+
function cmdMappings(json = false) {
|
|
474
691
|
const rows = mappingRows();
|
|
692
|
+
if (json) {
|
|
693
|
+
console.log(JSON.stringify(rows.map((r) => ({ path: r.path, provider: r.provider, target: r.name, paused: r.paused }))));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
475
696
|
if (rows.length === 0) {
|
|
476
697
|
console.log("No directory mappings. Create one with: agent-switch map <profile> [dir]");
|
|
477
698
|
return;
|
|
478
699
|
}
|
|
479
|
-
for (const r of rows)
|
|
480
|
-
|
|
700
|
+
for (const r of rows) {
|
|
701
|
+
const line = isPoolRef(r.name) ? `${r.path} -> ${r.name} (${r.provider})` : `${r.path} -> ${r.provider}/${r.name}`;
|
|
702
|
+
console.log(r.paused ? `${line} [paused]` : line);
|
|
703
|
+
}
|
|
481
704
|
}
|
|
482
705
|
// share + web are Claude-config concepts and stay Claude-scoped.
|
|
483
706
|
function cmdShare(mode, flags = []) {
|
|
@@ -503,7 +726,9 @@ function cmdShare(mode, flags = []) {
|
|
|
503
726
|
const actions = applySharing(source, configDir("claude", p), withHistory);
|
|
504
727
|
console.log(` ${p}: ${actions.length > 0 ? actions.join(", ") : "up to date"}`);
|
|
505
728
|
}
|
|
506
|
-
console.log("
|
|
729
|
+
console.log("Prompt history (history.jsonl — what ↑ and Ctrl+R search) is shared too; a profile's own\n" +
|
|
730
|
+
"file was merged in and kept as history.jsonl.pre-share.\n" +
|
|
731
|
+
"Shared directories (skills/, commands/, agents/) write through the link. Shared files\n" +
|
|
507
732
|
"(settings.json, ...) fork on an in-profile /config edit — run `agent-switch share sync`\n" +
|
|
508
733
|
"to push a fork back to the source and re-link.");
|
|
509
734
|
}
|
|
@@ -529,7 +754,11 @@ function cmdShare(mode, flags = []) {
|
|
|
529
754
|
// flag: sharing is active when any profile carries agent-switch-managed links.
|
|
530
755
|
const rows = profiles
|
|
531
756
|
.filter((p) => configDir("claude", p) !== source)
|
|
532
|
-
.map((p) =>
|
|
757
|
+
.map((p) => {
|
|
758
|
+
const links = sharedLinkHealth(configDir("claude", p));
|
|
759
|
+
const prompts = links.some((l) => l.name === PROMPT_HISTORY && l.state === "linked");
|
|
760
|
+
return { name: p, shared: links.length > 0, prompts };
|
|
761
|
+
});
|
|
533
762
|
const active = rows.some((r) => r.shared);
|
|
534
763
|
if (flags.includes("--json")) {
|
|
535
764
|
console.log(JSON.stringify({ active, source: sourceName ?? "default", profiles: rows }));
|
|
@@ -537,7 +766,7 @@ function cmdShare(mode, flags = []) {
|
|
|
537
766
|
}
|
|
538
767
|
console.log(`share: ${active ? "on" : "off"} (source ${sourceName ?? "default"})`);
|
|
539
768
|
for (const r of rows)
|
|
540
|
-
console.log(` ${r.name}: ${r.shared ? "shared" : "own"}`);
|
|
769
|
+
console.log(` ${r.name}: ${r.shared ? "shared" : "own"}${r.shared && !r.prompts ? " (prompt history not linked — run share on)" : ""}`);
|
|
541
770
|
}
|
|
542
771
|
else {
|
|
543
772
|
die("usage: agent-switch share on|sync|off|status [--history] [--source <profile|default>]");
|
|
@@ -822,6 +1051,40 @@ function cmdHookEvent() {
|
|
|
822
1051
|
* GUI contract: a flat array, metadata only — transcript content never leaves
|
|
823
1052
|
* the profile dir (only the guarded first-line header is read).
|
|
824
1053
|
*/
|
|
1054
|
+
/** The regular Desktop app's data dir, then every profile's own instance. */
|
|
1055
|
+
function desktopInstances() {
|
|
1056
|
+
const app = findApp("claude-desktop");
|
|
1057
|
+
const regular = process.platform === "darwin"
|
|
1058
|
+
? path.join(HOME, "Library", "Application Support", "Claude")
|
|
1059
|
+
: process.platform === "win32"
|
|
1060
|
+
? path.join(process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming"), "Claude")
|
|
1061
|
+
: path.join(HOME, ".config", "Claude");
|
|
1062
|
+
return [{ profile: null, dir: regular }, ...(app ? listProfiles("claude").map((p) => ({ profile: p, dir: guiDataDir(app, p) })) : [])];
|
|
1063
|
+
}
|
|
1064
|
+
function desktopScan() {
|
|
1065
|
+
const byAccount = new Map();
|
|
1066
|
+
for (const p of listProfiles("claude")) {
|
|
1067
|
+
const uuid = readJson(path.join(configDir("claude", p), ".claude.json"))?.oauthAccount?.accountUuid;
|
|
1068
|
+
if (typeof uuid === "string")
|
|
1069
|
+
byAccount.set(uuid, p);
|
|
1070
|
+
}
|
|
1071
|
+
return scanDesktop({ instances: desktopInstances(), claudeHome: CLAUDE_HOME, profileOf: (u) => byAccount.get(u) ?? null, now: Date.now() });
|
|
1072
|
+
}
|
|
1073
|
+
/** Which accounts ran sessions in each Claude Desktop instance; `mismatch` =
|
|
1074
|
+
* a profile's own instance signed in with another account. */
|
|
1075
|
+
function cmdDesktop(json) {
|
|
1076
|
+
const { instances } = desktopScan();
|
|
1077
|
+
const shown = instances.filter((i) => i.accounts.length > 0);
|
|
1078
|
+
if (json)
|
|
1079
|
+
return void console.log(JSON.stringify(shown, null, 2));
|
|
1080
|
+
if (!shown.length)
|
|
1081
|
+
return void console.log("No Claude Desktop sessions found.");
|
|
1082
|
+
for (const i of shown) {
|
|
1083
|
+
console.log(`${i.profile ? `${i.profile}'s instance` : "Claude Desktop"} (${tildePath(i.dir)})`);
|
|
1084
|
+
for (const a of i.accounts)
|
|
1085
|
+
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" : ""}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
825
1088
|
function cmdSessions(providerId, providerExplicit, name, flags = {}) {
|
|
826
1089
|
if (providerExplicit && providerId !== "claude" && providerId !== "codex") {
|
|
827
1090
|
die(`sessions supports claude and codex for now (${providerId} parity is gated on its Phase-0 spike — see scripts/spikes/)`);
|
|
@@ -834,6 +1097,8 @@ function cmdSessions(providerId, providerExplicit, name, flags = {}) {
|
|
|
834
1097
|
if (profiles.length === 0)
|
|
835
1098
|
die(`no ${providerId} profiles`);
|
|
836
1099
|
const all = [];
|
|
1100
|
+
// Claude Desktop's own sessions, filed by account (see desktop-sessions.ts).
|
|
1101
|
+
const desktop = providerId === "claude" ? desktopScan().rows : [];
|
|
837
1102
|
for (const p of profiles) {
|
|
838
1103
|
const cfg = configDir(providerId, p);
|
|
839
1104
|
// Codex stores date-partitioned rollout files (no encoded-cwd dir); its live
|
|
@@ -842,7 +1107,24 @@ function cmdSessions(providerId, providerExplicit, name, flags = {}) {
|
|
|
842
1107
|
const rows = providerId === "codex" ? listCodexSessions(cfg, limit) : listSessions(cfg, limit);
|
|
843
1108
|
if (providerId === "claude")
|
|
844
1109
|
markLive(cfg, rows); // POSIX: pid→cwd→dir match; win32: recent-only
|
|
845
|
-
|
|
1110
|
+
if (providerId === "claude") {
|
|
1111
|
+
const mtime = new Map(rows.map((r) => [r.sessionId, r.mtimeMs]));
|
|
1112
|
+
const waiting = waitingSessions(readEvents(eventFile(ROOT, providerId, p)), (id) => mtime.get(id) ?? null);
|
|
1113
|
+
// Hooks report a prompt; an open question also shows in the transcript itself.
|
|
1114
|
+
for (const r of rows) {
|
|
1115
|
+
const why = (r.file ? openQuestion(r.file) : null) ?? waiting.get(r.sessionId) ?? null;
|
|
1116
|
+
if (why)
|
|
1117
|
+
Object.assign(r, { waiting: true, waitingFor: why });
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const own = new Set(rows.map((r) => r.sessionId));
|
|
1121
|
+
for (const d of desktop)
|
|
1122
|
+
if (d.profile === p && !own.has(d.sessionId)) {
|
|
1123
|
+
const q = d.file ? openQuestion(d.file) : null;
|
|
1124
|
+
rows.push(q ? { ...d, waiting: true, waitingFor: q } : d);
|
|
1125
|
+
}
|
|
1126
|
+
rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1127
|
+
all.push(...rows.slice(0, limit).map((r) => ({ ...r, profile: p })));
|
|
846
1128
|
}
|
|
847
1129
|
if (flags.json) {
|
|
848
1130
|
console.log(JSON.stringify(all.map((r) => {
|
|
@@ -1319,7 +1601,25 @@ function takeoverCodex(sessionId, target, opts) {
|
|
|
1319
1601
|
if (!printOnly && process.stdin.isTTY)
|
|
1320
1602
|
process.exit(launch(provider("codex"), target, resumeArgs));
|
|
1321
1603
|
}
|
|
1322
|
-
|
|
1604
|
+
/**
|
|
1605
|
+
* Ask before a destructive or scarce-credit action. `--yes` skips the question.
|
|
1606
|
+
* Off a terminal there is nobody to ask, so the action proceeds as it always
|
|
1607
|
+
* did (scripts and the GUI, which confirms in its own UI, keep working) and a
|
|
1608
|
+
* note on stderr says how to make that explicit.
|
|
1609
|
+
*/
|
|
1610
|
+
async function confirmAction(question, yes) {
|
|
1611
|
+
if (yes)
|
|
1612
|
+
return true;
|
|
1613
|
+
if (!process.stdin.isTTY) {
|
|
1614
|
+
console.error("agent-switch: not a terminal — proceeding without confirmation (pass --yes to make this explicit).");
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1618
|
+
const answer = await new Promise((resolve) => rl.question(`${question} [y/N] `, resolve));
|
|
1619
|
+
rl.close();
|
|
1620
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1621
|
+
}
|
|
1622
|
+
async function cmdRemove(providerId, name, force = false, yes = false) {
|
|
1323
1623
|
const n = requireProfile(providerId, name, "remove");
|
|
1324
1624
|
if (activeFor(providerId) === n && !force) {
|
|
1325
1625
|
die(`"${n}" is the active ${providerId} profile. Switch away first or use --force.`);
|
|
@@ -1330,15 +1630,54 @@ function cmdRemove(providerId, name, force = false) {
|
|
|
1330
1630
|
die(`profile "${n}" has live Claude Code sessions (PIDs ${pids.join(", ")}). Close them or use --force.`);
|
|
1331
1631
|
}
|
|
1332
1632
|
}
|
|
1633
|
+
// An active rebind keeps both original credentials in two Keychain stash
|
|
1634
|
+
// entries. Deleting the profile would orphan them — restore first.
|
|
1635
|
+
const binding = providerId === "claude" ? readBinding(configDir("claude", n)) : null;
|
|
1636
|
+
if (binding && !force) {
|
|
1637
|
+
die(`profile "${n}" is rebound to "${binding.boundToProfile}"'s account. Restore it first: ` +
|
|
1638
|
+
`agent-switch rebind --restore --profile ${n} — or use --force to drop the rebind (its stashed credentials are deleted).`);
|
|
1639
|
+
}
|
|
1640
|
+
// The reverse case: another profile is serving THIS profile's account. Its
|
|
1641
|
+
// restore would write into the store of a profile that no longer exists.
|
|
1642
|
+
const borrower = providerId === "claude" ? borrowerOf(n) : null;
|
|
1643
|
+
if (borrower && !force) {
|
|
1644
|
+
die(`profile "${n}"'s account is currently borrowed by "${borrower}" (rebind). Restore it first: ` +
|
|
1645
|
+
`agent-switch rebind --restore --profile ${borrower} — or use --force to restore "${borrower}" and then remove "${n}".`);
|
|
1646
|
+
}
|
|
1647
|
+
const who = identity(providerId, n);
|
|
1648
|
+
if (!(await confirmAction(`Remove ${providerId} profile "${n}"${who ? ` (${who})` : ""} and its login? This cannot be undone.`, yes))) {
|
|
1649
|
+
console.log("Cancelled — nothing removed.");
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1333
1652
|
// Keychain entries exist only for claude (darwin); codex/antigravity are file-based,
|
|
1334
1653
|
// so computing a Claude hash for them would be a conceptual no-op — skip it.
|
|
1654
|
+
let restoredBorrower = null;
|
|
1655
|
+
if (borrower) {
|
|
1656
|
+
try {
|
|
1657
|
+
restoredBorrower = await releaseBorrowedAccount(n);
|
|
1658
|
+
}
|
|
1659
|
+
catch (e) {
|
|
1660
|
+
die(`could not restore "${borrower}" first, so "${n}" was not removed: ${e.message}`);
|
|
1661
|
+
}
|
|
1662
|
+
if (restoredBorrower)
|
|
1663
|
+
logActivity("rebind", `claude/${restoredBorrower}`, `restored own account (was on ${n})`, "user ran `agent-switch remove --force`");
|
|
1664
|
+
}
|
|
1665
|
+
const droppedRebind = binding ? discardBinding(n) : false;
|
|
1335
1666
|
const removedEntry = providerId === "claude" ? credentials.removeEntry(configDir(providerId, n)) : false;
|
|
1667
|
+
removeLaunchersFor(providerId, n); // they would start a profile that no longer exists
|
|
1336
1668
|
fs.rmSync(profileDir(providerId, n), { recursive: true, force: true });
|
|
1337
1669
|
if (activeFor(providerId) === n)
|
|
1338
1670
|
setActive(providerId, null);
|
|
1339
1671
|
clearLabel(providerId, n); // drop its label so it can't linger as an orphan
|
|
1340
1672
|
const pruned = pruneMappings(providerId, n);
|
|
1673
|
+
forgetProtection(providerId, n);
|
|
1674
|
+
renamePoolMember(`${providerId}/${n}`, null);
|
|
1675
|
+
logActivity("remove", `${providerId}/${n}`, "profile removed", yes ? "user ran `agent-switch remove --yes`" : "user ran `agent-switch remove`");
|
|
1341
1676
|
console.log(`Removed ${providerId} profile "${n}".`);
|
|
1677
|
+
if (restoredBorrower)
|
|
1678
|
+
console.log(`Restored "${restoredBorrower}" to its own account first (it was borrowing "${n}").`);
|
|
1679
|
+
if (droppedRebind)
|
|
1680
|
+
console.log(`Dropped its active rebind (stashed credentials deleted; "${binding.boundToProfile}" keeps its own login).`);
|
|
1342
1681
|
if (removedEntry)
|
|
1343
1682
|
console.log("Removed its keychain credential entry.");
|
|
1344
1683
|
if (pruned.length > 0)
|
|
@@ -1363,6 +1702,64 @@ function cmdLabel(providerId, name, label) {
|
|
|
1363
1702
|
setLabel(providerId, name, label);
|
|
1364
1703
|
console.log(`Labeled ${providerId}/${name} as ${label}.`);
|
|
1365
1704
|
}
|
|
1705
|
+
/** Disable keeps a profile (login, history, settings) but takes it out of every
|
|
1706
|
+
* pick: no active slot, no pool or folder rule, no background polling. */
|
|
1707
|
+
/** `mark-shared <name> [--off]`: mark (or unmark) a profile as sharing the
|
|
1708
|
+
* default install's login — for imports made before `import` set the mark. */
|
|
1709
|
+
function cmdMarkShared(providerId, name, off) {
|
|
1710
|
+
const n = requireProfile(providerId, name, "mark-shared");
|
|
1711
|
+
setSharesDefaultLogin(providerId, n, !off);
|
|
1712
|
+
console.log(off
|
|
1713
|
+
? `${providerId}/${n} no longer counts as sharing the default install's login.`
|
|
1714
|
+
: `${providerId}/${n} now counts as sharing the default install's login: agent-switch never refreshes it and never starts it by balancing. \`agent-switch login ${n}\` gives it its own login.`);
|
|
1715
|
+
}
|
|
1716
|
+
function cmdDisable(providerId, name, disabled) {
|
|
1717
|
+
const n = requireProfile(providerId, name, disabled ? "disable" : "enable");
|
|
1718
|
+
const wasActive = activeFor(providerId) === n;
|
|
1719
|
+
setDisabled(providerId, n, disabled);
|
|
1720
|
+
if (!disabled)
|
|
1721
|
+
return void console.log(`Turned ${providerId}/${n} back on.`);
|
|
1722
|
+
console.log(`Disabled ${providerId}/${n}. Nothing was deleted; \`agent-switch enable ${n}\` turns it back on.`);
|
|
1723
|
+
if (wasActive)
|
|
1724
|
+
console.log(`It was the active ${providerId} profile — new sessions now use the default login until you pick another.`);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Make this CLI the one terminals and the background service use. Each app runs
|
|
1728
|
+
* it on start, so a dev build and an installed app each bring their own CLI:
|
|
1729
|
+
* the shell wrapper reads <home>/active-cli, and a service installed from the
|
|
1730
|
+
* other CLI is re-installed onto this one. An uninstalled service stays so.
|
|
1731
|
+
*/
|
|
1732
|
+
function cmdActivate(json) {
|
|
1733
|
+
const cli = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
1734
|
+
ensureRoot();
|
|
1735
|
+
fs.writeFileSync(path.join(ROOT, "active-cli"), `${cli}\n`, { mode: 0o600 });
|
|
1736
|
+
const before = serviceTarget();
|
|
1737
|
+
if (before === "other-cli")
|
|
1738
|
+
serviceInstall(() => { });
|
|
1739
|
+
const service = before === "other-cli" ? "repointed" : before === "this-cli" ? "unchanged" : "not-installed";
|
|
1740
|
+
if (json)
|
|
1741
|
+
return void console.log(JSON.stringify({ cli, service }));
|
|
1742
|
+
console.log(`Terminals now use ${cli}.`);
|
|
1743
|
+
if (service === "repointed")
|
|
1744
|
+
console.log("The background service was moved onto it.");
|
|
1745
|
+
if (service === "not-installed")
|
|
1746
|
+
console.log("The background service isn't installed (agent-switch service install).");
|
|
1747
|
+
}
|
|
1748
|
+
function cmdColor(providerId, name, color) {
|
|
1749
|
+
// Like a label, independent state keyed by provider/name: the GUI picks the
|
|
1750
|
+
// colour before `add` has created the profile.
|
|
1751
|
+
if (!name)
|
|
1752
|
+
die(`usage: agent-switch color <profile> [${PROFILE_COLORS.join("|")}|none] [--provider P]`);
|
|
1753
|
+
if (color === undefined || color === "none" || color === "clear") {
|
|
1754
|
+
setColor(providerId, name, null);
|
|
1755
|
+
console.log(`Cleared the colour of ${providerId}/${name}.`);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (!isProfileColor(color))
|
|
1759
|
+
die(`invalid colour "${color}" (choose: ${PROFILE_COLORS.join(", ")}, or "none" to clear)`);
|
|
1760
|
+
setColor(providerId, name, color);
|
|
1761
|
+
console.log(`Coloured ${providerId}/${name} ${color}.`);
|
|
1762
|
+
}
|
|
1366
1763
|
/** Enable/disable opt-in near-limit notifications (notify + suggest, never an
|
|
1367
1764
|
* automatic switch), or show the current setting. */
|
|
1368
1765
|
/** Rename a profile (move its config dir + carry active/label/mappings). */
|
|
@@ -1392,15 +1789,23 @@ function cmdRename(providerId, from, to) {
|
|
|
1392
1789
|
if (m.provider === providerId && m.name === n)
|
|
1393
1790
|
setMapping(m.path, providerId, to);
|
|
1394
1791
|
}
|
|
1792
|
+
renameProtection(providerId, n, to);
|
|
1793
|
+
renamePoolMember(`${providerId}/${n}`, `${providerId}/${to}`);
|
|
1794
|
+
renameLaunchersFor(providerId, n, to);
|
|
1395
1795
|
console.log(`Renamed ${providerId} profile "${n}" → "${to}".`);
|
|
1396
1796
|
}
|
|
1397
1797
|
/** Manually redeem one banked rate-limit reset for a Codex profile. Consumes a
|
|
1398
1798
|
* real, scarce credit — the GUI gates this behind a confirmation. */
|
|
1399
|
-
async function cmdReset(providerId, name) {
|
|
1799
|
+
async function cmdReset(providerId, name, yes = false) {
|
|
1400
1800
|
if (providerId !== "codex")
|
|
1401
1801
|
die("reset is only available for codex (banked rate-limit resets).");
|
|
1402
1802
|
const n = requireProfile("codex", name, "reset");
|
|
1803
|
+
if (!(await confirmAction(`Redeem one banked rate-limit reset for codex/${n}? Credits are scarce and cannot be returned.`, yes))) {
|
|
1804
|
+
console.log("Cancelled — nothing redeemed.");
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1403
1807
|
const r = await redeemResetCredit(configDir("codex", n));
|
|
1808
|
+
logActivity("redeem", `codex/${n}`, r.ok ? "redeemed one banked reset" : `redeem failed: ${r.reason ?? "unknown"}`, "user ran `agent-switch reset`", r.ok);
|
|
1404
1809
|
if (r.ok) {
|
|
1405
1810
|
console.log(`Redeemed a reset for codex/${n} (windows_reset=${r.windowsReset ?? "?"}).`);
|
|
1406
1811
|
}
|
|
@@ -1441,7 +1846,7 @@ function cmdAutoswitch(providerId, providerExplicit, mode, flags = {}) {
|
|
|
1441
1846
|
if (cfg.enabled) {
|
|
1442
1847
|
console.log(`Once the active ${providerId} profile hits the threshold, the daemon NOTIFIES you and SUGGESTS the\n` +
|
|
1443
1848
|
"account with the most headroom — it never switches automatically; switching stays a manual action.\n" +
|
|
1444
|
-
"On Codex it
|
|
1849
|
+
"On Codex it also tells you when a banked reset is available — redeeming one stays your call (`agent-switch reset`).\n" +
|
|
1445
1850
|
"Run `agent-switch service start` so the daemon is watching.");
|
|
1446
1851
|
}
|
|
1447
1852
|
return;
|
|
@@ -1528,6 +1933,7 @@ function cmdUninstall(flags = {}) {
|
|
|
1528
1933
|
console.log(` - ${allProfiles.length} profile(s) and their configs under ${ROOT}`);
|
|
1529
1934
|
console.log(` - ${claudeProfiles.length} Claude keychain credential entr(y/ies) (macOS)`);
|
|
1530
1935
|
console.log(" - directory mappings, active-profile state, and the background daemon/service");
|
|
1936
|
+
console.log(" - app launchers it created in ~/Applications/Agent Switch");
|
|
1531
1937
|
console.log("It does NOT touch your default claude/codex/antigravity installs.");
|
|
1532
1938
|
console.log("\nRe-run with --force to proceed: agent-switch uninstall --force");
|
|
1533
1939
|
return;
|
|
@@ -1554,8 +1960,12 @@ function cmdUninstall(flags = {}) {
|
|
|
1554
1960
|
if (credentials.removeEntry(configDir("claude", n)))
|
|
1555
1961
|
removedEntries++;
|
|
1556
1962
|
}
|
|
1963
|
+
// Launchers live in ~/Applications, outside ROOT, and are listed in its manifest.
|
|
1964
|
+
const removedLaunchers = removeAllLaunchers();
|
|
1557
1965
|
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
1558
1966
|
console.log(`Removed ${allProfiles.length} profile(s) and all agent-switch data under ${ROOT}.`);
|
|
1967
|
+
if (removedLaunchers > 0)
|
|
1968
|
+
console.log(`Removed ${removedLaunchers} app launcher(s) from ~/Applications/Agent Switch.`);
|
|
1559
1969
|
if (removedEntries > 0)
|
|
1560
1970
|
console.log(`Removed ${removedEntries} Claude keychain entr(y/ies).`);
|
|
1561
1971
|
console.log("Follow-ups (manual, if you set them up):");
|
|
@@ -1602,6 +2012,7 @@ function cmdApps(json = false) {
|
|
|
1602
2012
|
provider: a.provider,
|
|
1603
2013
|
strategy: a.strategy,
|
|
1604
2014
|
installed: isInstalled(a),
|
|
2015
|
+
copiesSettings: hasSettingsLayout(a.id),
|
|
1605
2016
|
}));
|
|
1606
2017
|
console.log(JSON.stringify(rows, null, 2));
|
|
1607
2018
|
return;
|
|
@@ -1630,7 +2041,9 @@ function cmdNotify(flags) {
|
|
|
1630
2041
|
}
|
|
1631
2042
|
if (!title && !message)
|
|
1632
2043
|
die("agent-switch notify needs at least --title or --message");
|
|
1633
|
-
|
|
2044
|
+
// --repeat: each occurrence is its own moment (a session waiting again), so
|
|
2045
|
+
// an identical notice is not collapsed into the last one.
|
|
2046
|
+
const created = appendNotification({ kind: kind, title, message }, undefined, undefined, !flags.repeat);
|
|
1634
2047
|
if (flags.json)
|
|
1635
2048
|
console.log(JSON.stringify(created));
|
|
1636
2049
|
}
|
|
@@ -1717,7 +2130,35 @@ async function cmdUpdate() {
|
|
|
1717
2130
|
console.log(`Updating ${c.current} → ${c.release.tag} via npm…`);
|
|
1718
2131
|
else
|
|
1719
2132
|
console.log(`Update check failed (${c.message}); attempting \`npm install\` anyway…`);
|
|
1720
|
-
|
|
2133
|
+
const code = selfUpdate();
|
|
2134
|
+
logActivity("update", null, code === 0 ? "self-update installed" : `self-update failed (exit ${code})`, "user ran `agent-switch update`", code === 0);
|
|
2135
|
+
if (code === 0)
|
|
2136
|
+
await serviceAfterSelfUpdate();
|
|
2137
|
+
process.exit(code);
|
|
2138
|
+
}
|
|
2139
|
+
/** Keeps the usage service on the version just installed. Without it the
|
|
2140
|
+
* balanced start has no fresh usage to weigh. */
|
|
2141
|
+
async function serviceAfterSelfUpdate() {
|
|
2142
|
+
switch (serviceAfterUpdate(serviceTarget(), !!process.stdin.isTTY)) {
|
|
2143
|
+
case "reinstall":
|
|
2144
|
+
console.log("Restarting the background service on the new version…");
|
|
2145
|
+
if (serviceInstallFromDisk() !== 0)
|
|
2146
|
+
console.error("Couldn't restart the service — run `agent-switch service install`.");
|
|
2147
|
+
return;
|
|
2148
|
+
case "ask":
|
|
2149
|
+
if (await confirmAction("Install the background service that keeps usage fresh (starts at login)?", false)) {
|
|
2150
|
+
if (serviceInstallFromDisk() !== 0)
|
|
2151
|
+
console.error("Couldn't install the service — run `agent-switch service install`.");
|
|
2152
|
+
}
|
|
2153
|
+
else
|
|
2154
|
+
console.log("Skipped. Install it any time with `agent-switch service install`.");
|
|
2155
|
+
return;
|
|
2156
|
+
case "hint":
|
|
2157
|
+
console.log("Tip: `agent-switch service install` keeps usage fresh so starts can be balanced.");
|
|
2158
|
+
return;
|
|
2159
|
+
case "leave":
|
|
2160
|
+
console.log("The background service runs another agent-switch build — left as it is.");
|
|
2161
|
+
}
|
|
1721
2162
|
}
|
|
1722
2163
|
function cmdOpen(appId, name) {
|
|
1723
2164
|
if (!appId)
|
|
@@ -1733,6 +2174,12 @@ function cmdOpen(appId, name) {
|
|
|
1733
2174
|
requireProfile(app.provider, n, "open");
|
|
1734
2175
|
if (!isInstalled(app))
|
|
1735
2176
|
die(`${app.displayName} is not installed.`);
|
|
2177
|
+
// A second launch on the same data dir would only hit the app's single-instance
|
|
2178
|
+
// lock — bring the window that profile already has to the front instead.
|
|
2179
|
+
if (app.strategy !== "env" && focusRunning(guiDataDir(app, n))) {
|
|
2180
|
+
console.log(`${app.displayName} on ${app.provider}/${n} is already open — brought it to the front.`);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
1736
2183
|
let firstLaunch = false;
|
|
1737
2184
|
if (app.strategy === "user-data-dir") {
|
|
1738
2185
|
const dir = guiDataDir(app, n);
|
|
@@ -1747,6 +2194,95 @@ function cmdOpen(appId, name) {
|
|
|
1747
2194
|
console.log("is saved to this profile. Quit other windows of this app before signing in.");
|
|
1748
2195
|
}
|
|
1749
2196
|
}
|
|
2197
|
+
/** `agent-switch launcher add|remove <app> <profile>` · `list [--json]` · `repair`. */
|
|
2198
|
+
function cmdLauncher(sub, args, flags) {
|
|
2199
|
+
const target = (verb) => {
|
|
2200
|
+
const app = findApp(args[0] ?? "");
|
|
2201
|
+
if (!app)
|
|
2202
|
+
die(`usage: agent-switch launcher ${verb} <app> <profile> (see \`agent-switch apps\`)`);
|
|
2203
|
+
const n = requireProfile(app.provider, args[1], `launcher ${verb}`);
|
|
2204
|
+
return { app, n };
|
|
2205
|
+
};
|
|
2206
|
+
switch (sub) {
|
|
2207
|
+
case "add": {
|
|
2208
|
+
const { app, n } = target("add");
|
|
2209
|
+
try {
|
|
2210
|
+
const e = createLauncher(app, n);
|
|
2211
|
+
logActivity("launcher", `${app.provider}/${n}`, `created ${app.displayName} launcher`, "user ran `agent-switch launcher add`");
|
|
2212
|
+
console.log(`Created ${e.path}`);
|
|
2213
|
+
console.log("Start it from Spotlight or drag it into the Dock. It opens the app on this account only.");
|
|
2214
|
+
}
|
|
2215
|
+
catch (err) {
|
|
2216
|
+
die(err instanceof Error ? err.message : String(err));
|
|
2217
|
+
}
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
case "remove":
|
|
2221
|
+
case "rm": {
|
|
2222
|
+
const { app, n } = target("remove");
|
|
2223
|
+
if (!removeLauncher(app.id, n))
|
|
2224
|
+
die(`no ${app.displayName} launcher for ${app.provider}/${n}.`);
|
|
2225
|
+
logActivity("launcher", `${app.provider}/${n}`, `removed ${app.displayName} launcher`, "user ran `agent-switch launcher remove`");
|
|
2226
|
+
console.log(`Removed the ${app.displayName} launcher for ${app.provider}/${n}.`);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
case "repair": {
|
|
2230
|
+
const r = repairLaunchers();
|
|
2231
|
+
for (const e of r.rebuilt)
|
|
2232
|
+
console.log(`Rebuilt ${e.path}`);
|
|
2233
|
+
for (const f of r.failed)
|
|
2234
|
+
console.error(`Couldn't rebuild ${f.entry.path}: ${f.reason}`);
|
|
2235
|
+
if (!r.rebuilt.length && !r.failed.length)
|
|
2236
|
+
console.log("No launchers to repair.");
|
|
2237
|
+
if (r.failed.length)
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
case "list":
|
|
2242
|
+
case undefined: {
|
|
2243
|
+
const rows = readLaunchers().map((e) => ({ ...e, exists: fs.existsSync(e.path) }));
|
|
2244
|
+
if (flags.json)
|
|
2245
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
2246
|
+
if (!rows.length)
|
|
2247
|
+
return void console.log("No launchers. Create one with `agent-switch launcher add <app> <profile>`.");
|
|
2248
|
+
for (const r of rows)
|
|
2249
|
+
console.log(`${r.exists ? "●" : "○ missing"} ${r.app.padEnd(16)} ${r.provider}/${r.profile} ${r.path}`);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
default:
|
|
2253
|
+
die("usage: agent-switch launcher add|remove <app> <profile> | list [--json] | repair");
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
/** `agent-switch app-settings copy <app> --from <profile|default> --to <profile>`. */
|
|
2257
|
+
function cmdAppSettings(sub, args, flags) {
|
|
2258
|
+
const app = findApp(args[0] ?? "");
|
|
2259
|
+
const from = typeof flags.from === "string" ? flags.from : null;
|
|
2260
|
+
const to = typeof flags.to === "string" ? flags.to : null;
|
|
2261
|
+
if (sub !== "copy" || !app || !from || !to)
|
|
2262
|
+
die("usage: agent-switch app-settings copy <app> --from <profile|default> --to <profile>");
|
|
2263
|
+
if (!hasSettingsLayout(app.id))
|
|
2264
|
+
die(`copying settings is not supported for ${app.displayName}.`);
|
|
2265
|
+
if (from === to)
|
|
2266
|
+
die("--from and --to are the same profile.");
|
|
2267
|
+
const toProfile = requireProfile(app.provider, to, "app-settings copy");
|
|
2268
|
+
const fromDir = from === "default" ? DEFAULT_DATA_DIR[app.id] : guiDataDir(app, requireProfile(app.provider, from, "app-settings copy"));
|
|
2269
|
+
const toDir = guiDataDir(app, toProfile);
|
|
2270
|
+
if (!fs.existsSync(fromDir))
|
|
2271
|
+
die(`${from === "default" ? `The default ${app.displayName}` : `${app.provider}/${from}`} has no ${app.displayName} data yet.`);
|
|
2272
|
+
// The running app rewrites its own files on quit and would undo the copy.
|
|
2273
|
+
if (findRunning(toDir))
|
|
2274
|
+
die(`${app.displayName} is open on ${app.provider}/${toProfile} — quit it first, then copy.`);
|
|
2275
|
+
const r = copyDesktopSettings(app.id, fromDir, toDir);
|
|
2276
|
+
logActivity("settings", `${app.provider}/${toProfile}`, `copied ${app.displayName} settings from ${from}`, "user ran `agent-switch app-settings copy`");
|
|
2277
|
+
if (flags.json)
|
|
2278
|
+
return void console.log(JSON.stringify({ app: app.id, from, to: toProfile, ...r }, null, 2));
|
|
2279
|
+
if (!r.copied.length)
|
|
2280
|
+
return void console.log(`Nothing to copy — ${from} has no ${app.displayName} settings yet.`);
|
|
2281
|
+
console.log(`Copied ${r.copied.join(", ")} from ${from} to ${app.provider}/${toProfile}.`);
|
|
2282
|
+
if (r.backups.length)
|
|
2283
|
+
console.log(`Previous files kept as ${r.backups.map((b) => path.basename(b)).join(", ")}.`);
|
|
2284
|
+
console.log("Logins, chats and account-bound preferences were not copied.");
|
|
2285
|
+
}
|
|
1750
2286
|
/**
|
|
1751
2287
|
* `agent-switch guard [block|warn|off]` — read or set the single-load policy.
|
|
1752
2288
|
* The bare form also reports what is loaded RIGHT NOW, because "what is my
|
|
@@ -1758,6 +2294,7 @@ function cmdGuard(value, json = false) {
|
|
|
1758
2294
|
die(`unknown guard policy "${value}" (choose: ${GUARD_POLICIES.join(", ")})`);
|
|
1759
2295
|
}
|
|
1760
2296
|
setGuard(value);
|
|
2297
|
+
logActivity("guard", null, `set ${value}`, "user ran `agent-switch guard`");
|
|
1761
2298
|
}
|
|
1762
2299
|
const policy = readGuard();
|
|
1763
2300
|
// Every profile of every provider that can report a live session.
|
|
@@ -1804,8 +2341,17 @@ function cmdGuard(value, json = false) {
|
|
|
1804
2341
|
console.log(`Override once: ${OVERRIDE_ENV}=1 <binary> · Change: agent-switch guard <${GUARD_POLICIES.join("|")}>`);
|
|
1805
2342
|
}
|
|
1806
2343
|
}
|
|
1807
|
-
function cmdShellenv(shellArg) {
|
|
1808
|
-
|
|
2344
|
+
function cmdShellenv(shellArg, sub, json = false) {
|
|
2345
|
+
if (sub !== "install")
|
|
2346
|
+
return void console.log(shellenvScript(detectShell(shellArg)));
|
|
2347
|
+
const shell = detectShell(shellArg);
|
|
2348
|
+
const present = shellIntegrationCheck(HOME).status === "ok";
|
|
2349
|
+
const r = installShellIntegration(shell, HOME, present);
|
|
2350
|
+
if (json)
|
|
2351
|
+
return void console.log(JSON.stringify(r));
|
|
2352
|
+
console.log(r.alreadyInstalled
|
|
2353
|
+
? "The shell integration is already installed."
|
|
2354
|
+
: `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.`);
|
|
1809
2355
|
}
|
|
1810
2356
|
/** `agent-switch tooling` — ecosystem tool readout (agent-config, rtk, provider
|
|
1811
2357
|
* CLIs). `--json` is the GUI IPC contract: the GUI renders this readout and
|
|
@@ -1922,8 +2468,11 @@ function usage() {
|
|
|
1922
2468
|
Provider defaults to claude; pass --provider codex|antigravity for the others.
|
|
1923
2469
|
Run with no command to launch the tray/menubar GUI (single-instance).
|
|
1924
2470
|
|
|
1925
|
-
agent-switch add [--provider P] <name>
|
|
2471
|
+
agent-switch add [--provider P] <name> [--browser B] create a profile and sign it in
|
|
2472
|
+
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
|
|
2473
|
+
agent-switch browsers [--json] browsers --browser can open (macOS / Linux)
|
|
1926
2474
|
agent-switch import [--provider P] <name> migrate the default install (no re-login)
|
|
2475
|
+
agent-switch mark-shared [--provider P] <name> [--off] mark a profile as sharing the default install's login (never refreshed or balanced)
|
|
1927
2476
|
agent-switch use [--provider P] <name> set the active profile for a provider
|
|
1928
2477
|
agent-switch deactivate [--provider P] clear the active profile for a provider
|
|
1929
2478
|
agent-switch run [--provider P] <name> [--tmux] [..] launch the provider's CLI (--tmux = managed tmux session, POSIX)
|
|
@@ -1931,9 +2480,10 @@ Run with no command to launch the tray/menubar GUI (single-instance).
|
|
|
1931
2480
|
agent-switch status [--provider P] [name] [--json] identity (+ Claude usage); --json = active only
|
|
1932
2481
|
agent-switch current [--provider P] show the active profile(s)
|
|
1933
2482
|
agent-switch whoami [--provider P] [name] show a profile's account identity
|
|
1934
|
-
agent-switch dir [--provider P] [--guard]
|
|
1935
|
-
agent-switch guard [block|warn|off] one profile, one live session (default
|
|
1936
|
-
agent-switch map [--provider P] <name
|
|
2483
|
+
agent-switch dir [--provider P] [--guard] [--json] [--plain] resolve profile for CWD (rule > balanced pick > active); --json adds why
|
|
2484
|
+
agent-switch guard [block|warn|off] one profile, one live session (default warn on new installs)
|
|
2485
|
+
agent-switch map [--provider P] <name|pool:<id>> [dir] map a directory to a profile or a pool
|
|
2486
|
+
agent-switch map --pause|--resume [--provider P] [dir] pause a folder rule (kept, but skipped) or turn it back on
|
|
1937
2487
|
agent-switch unmap [--provider P] [dir] remove a directory mapping
|
|
1938
2488
|
agent-switch mappings list directory mappings
|
|
1939
2489
|
agent-switch share on|sync|off|status [--history] [--source <profile|default>] [--json] (Claude)
|
|
@@ -1945,18 +2495,29 @@ Run with no command to launch the tray/menubar GUI (single-instance).
|
|
|
1945
2495
|
agent-switch takeover <id> --to <profile> [--from <profile>] [--keep-source] [--in-place] [--print-only] [--force] move a session to another profile and resume it
|
|
1946
2496
|
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)
|
|
1947
2497
|
agent-switch web <name> claude.ai in a persistent browser (Claude)
|
|
1948
|
-
agent-switch remove [--provider P] <name> [--force] delete a profile
|
|
2498
|
+
agent-switch remove [--provider P] <name> [--force] [--yes] delete a profile (asks on a terminal; --yes skips)
|
|
1949
2499
|
agent-switch label [--provider P] <name> [Work|Personal|Other|none] tag a profile
|
|
2500
|
+
agent-switch desktop [--json] which accounts ran sessions in each Claude Desktop instance
|
|
2501
|
+
agent-switch activate [--json] make this CLI the one terminals and the background service use
|
|
2502
|
+
agent-switch disable|enable [--provider P] <name> keep a profile but stop using it / turn it back on
|
|
2503
|
+
agent-switch color [--provider P] <name> [orange|red|yellow|green|teal|blue|purple|pink|none] pick a profile's colour
|
|
1950
2504
|
agent-switch autoswitch on|off|status [--provider P] [--threshold <1-100>] notify + suggest near a usage limit; never switches automatically (default OFF)
|
|
1951
|
-
agent-switch reset <profile> --provider codex
|
|
2505
|
+
agent-switch reset <profile> --provider codex [--yes] redeem one banked Codex rate-limit reset (asks on a terminal)
|
|
2506
|
+
agent-switch protection [--json] | protection set <provider/name> [--on|--off] [--threshold N] per-profile limit warning (default on, 98%)
|
|
2507
|
+
agent-switch pool [list|create|rename|delete|add|remove|suggest] [--json] optional groups a folder rule can point at
|
|
2508
|
+
agent-switch auth-status [--provider P] [--json] per-profile login state from the provider's store (no tokens)
|
|
2509
|
+
agent-switch activity [--limit N] [--json] what agent-switch itself did, and why
|
|
1952
2510
|
agent-switch rename <old> <new> [--provider P] rename a profile (name & keep its tag)
|
|
1953
2511
|
agent-switch providers enable|disable|status [--provider P] [--surface cli|ui] enable/disable a provider (default: Claude + Codex)
|
|
1954
2512
|
agent-switch [gui] launch the tray/menubar GUI (the default with no command; won't start a second if one is running)
|
|
1955
2513
|
agent-switch check-update [--json] check GitHub Releases for a newer version
|
|
1956
|
-
agent-switch update self-update to the latest version (via npm)
|
|
2514
|
+
agent-switch update self-update to the latest version (via npm); restarts the service on it, or offers to install one
|
|
1957
2515
|
agent-switch apps list launchable GUI apps (macOS)
|
|
1958
|
-
agent-switch open <app> [profile] launch a GUI app on a profile, isolated (macOS)
|
|
2516
|
+
agent-switch open <app> [profile] launch a GUI app on a profile, isolated; focuses it if already open (macOS)
|
|
2517
|
+
agent-switch launcher add|remove <app> <profile> | list [--json] | repair Dock/Spotlight launcher per account in ~/Applications/Agent Switch (macOS)
|
|
2518
|
+
agent-switch app-settings copy <app> --from <profile|default> --to <profile> [--json] copy desktop settings (MCP, preferences, extensions) — never logins
|
|
1959
2519
|
agent-switch shellenv [--shell zsh|bash|fish|powershell] shell integration
|
|
2520
|
+
agent-switch shellenv install [--shell S] [--json] add it to your shell's startup file
|
|
1960
2521
|
agent-switch service run|start|stop|status|install|uninstall background usage daemon
|
|
1961
2522
|
agent-switch notifications [clear] [--json] recent notifications (usage-limit suggestions, fetch failures)
|
|
1962
2523
|
agent-switch notify --kind K --title T --message M [--json] record a notification event
|
|
@@ -1966,7 +2527,7 @@ Run with no command to launch the tray/menubar GUI (single-instance).
|
|
|
1966
2527
|
agent-switch tooling [--json] ecosystem tool readout (agent-config, rtk, provider CLIs)
|
|
1967
2528
|
agent-switch tooling install|upgrade <tool> install/upgrade an ecosystem tool (agent-config, rtk, claude, codex)
|
|
1968
2529
|
agent-switch tooling notes [<tool>] [--latest] [--json] read an ecosystem tool's release notes (default agent-config; fetches from GitHub)
|
|
1969
|
-
agent-switch doctor
|
|
2530
|
+
agent-switch doctor [--json] per-OS, per-provider self-check`);
|
|
1970
2531
|
}
|
|
1971
2532
|
/**
|
|
1972
2533
|
* The interactive limit dialog (Phase 4) — `agent-switch rebind` with no account
|
|
@@ -2039,6 +2600,7 @@ async function rebindDialog(running) {
|
|
|
2039
2600
|
}
|
|
2040
2601
|
try {
|
|
2041
2602
|
const r = await rebind({ account: choice.profile, profile: running });
|
|
2603
|
+
logActivity("rebind", `claude/${running}`, `now serves ${choice.profile}'s account`, "user picked it in the rebind dialog");
|
|
2042
2604
|
console.log(r.uxNote);
|
|
2043
2605
|
}
|
|
2044
2606
|
catch (e) {
|
|
@@ -2088,6 +2650,7 @@ async function cmdRebind(providerId, providerExplicit, account, flags) {
|
|
|
2088
2650
|
requireProfile("claude", profile, "rebind --restore --profile");
|
|
2089
2651
|
try {
|
|
2090
2652
|
const r = await restoreRebind({ profile });
|
|
2653
|
+
logActivity("rebind", `claude/${profile}`, `restored own account (was on ${r.wasBoundTo})`, "user ran `agent-switch rebind --restore`");
|
|
2091
2654
|
console.log(`Restored "${r.restoredProfile}" to its own account (was rebound to "${r.wasBoundTo}").`);
|
|
2092
2655
|
}
|
|
2093
2656
|
catch (e) {
|
|
@@ -2110,12 +2673,233 @@ async function cmdRebind(providerId, providerExplicit, account, flags) {
|
|
|
2110
2673
|
requireProfile("claude", profile, "rebind --profile");
|
|
2111
2674
|
try {
|
|
2112
2675
|
const r = await rebind({ account, profile });
|
|
2676
|
+
logActivity("rebind", `claude/${profile}`, `now serves ${account}'s account`, "user ran `agent-switch rebind`");
|
|
2113
2677
|
console.log(r.uxNote);
|
|
2114
2678
|
}
|
|
2115
2679
|
catch (e) {
|
|
2116
2680
|
die(e.message);
|
|
2117
2681
|
}
|
|
2118
2682
|
}
|
|
2683
|
+
// ---------- gui-settings / activity / auth-status / protection / pool ----------
|
|
2684
|
+
/** `gui-settings [--json]` · `gui-settings set pet-welcomed` ·
|
|
2685
|
+
* `gui-settings set autostart enabled|disabled --by first-run|user` ·
|
|
2686
|
+
* `gui-settings set autostart-degraded --reason <text>|none`. The GUI's
|
|
2687
|
+
* file-backed preferences (see gui-settings.ts); prints the result as JSON. */
|
|
2688
|
+
function cmdGuiSettings(sub, args, flags) {
|
|
2689
|
+
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";
|
|
2690
|
+
if (sub === undefined || sub === "get" || sub === "status") {
|
|
2691
|
+
console.log(JSON.stringify(readGuiSettings(), null, 2));
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
if (sub !== "set")
|
|
2695
|
+
die(usageLine);
|
|
2696
|
+
const [key, value] = args;
|
|
2697
|
+
let u;
|
|
2698
|
+
if (key === "pet-welcomed")
|
|
2699
|
+
u = { key };
|
|
2700
|
+
else if (key === "autostart" && (value === "enabled" || value === "disabled")) {
|
|
2701
|
+
const by = flags.by === "first-run" || flags.by === "user" ? flags.by : die(usageLine);
|
|
2702
|
+
u = { key, state: value, by };
|
|
2703
|
+
}
|
|
2704
|
+
else if (key === "autostart-degraded") {
|
|
2705
|
+
const reason = typeof flags.reason === "string" ? flags.reason : die(usageLine);
|
|
2706
|
+
u = { key, reason: reason === "none" ? null : reason };
|
|
2707
|
+
}
|
|
2708
|
+
else
|
|
2709
|
+
die(usageLine);
|
|
2710
|
+
const before = readGuiSettings();
|
|
2711
|
+
let after;
|
|
2712
|
+
try {
|
|
2713
|
+
after = applyGuiSetting(before, u);
|
|
2714
|
+
}
|
|
2715
|
+
catch (e) {
|
|
2716
|
+
die(e.message);
|
|
2717
|
+
}
|
|
2718
|
+
writeGuiSettings(after);
|
|
2719
|
+
if (u.key === "autostart" && JSON.stringify(after.autostart) !== JSON.stringify(before.autostart)) {
|
|
2720
|
+
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");
|
|
2721
|
+
}
|
|
2722
|
+
if (u.key === "autostart-degraded" && u.reason) {
|
|
2723
|
+
logActivity("autostart", null, "start at login could not be confirmed", "first-run default; retried next launch", false);
|
|
2724
|
+
}
|
|
2725
|
+
console.log(JSON.stringify(after, null, 2));
|
|
2726
|
+
}
|
|
2727
|
+
function cmdActivity(flags) {
|
|
2728
|
+
const limit = typeof flags.limit === "string" ? Number(flags.limit) : undefined;
|
|
2729
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1))
|
|
2730
|
+
die("--limit must be a whole number of at least 1");
|
|
2731
|
+
const entries = readActivity(limit ?? (flags.json ? undefined : 50));
|
|
2732
|
+
if (flags.json) {
|
|
2733
|
+
console.log(JSON.stringify({ entries, capacity: ACTIVITY_MAX_LINES }, null, 2));
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
if (entries.length === 0) {
|
|
2737
|
+
console.log("No activity recorded yet.");
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
for (const e of entries) {
|
|
2741
|
+
console.log(`${e.at} ${e.ok ? " " : "!"} ${e.kind.padEnd(10)} ${e.target ?? "-"} ${e.action} — ${e.reason}`);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function authStatusFor(providerId, name) {
|
|
2745
|
+
const s = rawAuthStatusFor(providerId, name);
|
|
2746
|
+
const keptFreshBy = keptFreshByFor(providerId, { shared: sharesDefaultLogin(providerId, name), serviceInstalled: serviceTarget() !== "not-installed" }, s.keptFreshBy);
|
|
2747
|
+
return { ...s, keptFreshBy };
|
|
2748
|
+
}
|
|
2749
|
+
function rawAuthStatusFor(providerId, name) {
|
|
2750
|
+
const now = Date.now();
|
|
2751
|
+
const cfg = provider(providerId).configDirFor(configDir(providerId, name));
|
|
2752
|
+
if (providerId === "claude") {
|
|
2753
|
+
const file = claudeCredentialFile(cfg);
|
|
2754
|
+
const fromFile = readText(file);
|
|
2755
|
+
return claudeAuthStatus(name, credentials.read(cfg), fromFile !== null ? fileMtimeMs(file) : null, now);
|
|
2756
|
+
}
|
|
2757
|
+
if (providerId === "codex")
|
|
2758
|
+
return codexAuthStatus(name, readText(provider("codex").credentialPath(cfg)), now);
|
|
2759
|
+
return antigravityAuthStatus(name, fs.existsSync(provider("antigravity").credentialPath(cfg)));
|
|
2760
|
+
}
|
|
2761
|
+
function cmdAuthStatus(providerId, json) {
|
|
2762
|
+
const rows = (providerId ? listProfiles(providerId).map((n) => ({ provider: providerId, name: n })) : listAllProfiles()).map((r) => authStatusFor(r.provider, r.name));
|
|
2763
|
+
if (json) {
|
|
2764
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
if (rows.length === 0)
|
|
2768
|
+
return void console.log("No profiles.");
|
|
2769
|
+
for (const r of rows) {
|
|
2770
|
+
const exp = r.expiresAt ? `, ends ${r.expiresAt}` : "";
|
|
2771
|
+
console.log(`${`${r.provider}/${r.name}`.padEnd(24)} ${r.state}${exp} — ${r.detail}`);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
function parseMemberArg(v, usageLine) {
|
|
2775
|
+
const m = v ? parseMember(v) : null;
|
|
2776
|
+
if (!m)
|
|
2777
|
+
die(usageLine);
|
|
2778
|
+
return m;
|
|
2779
|
+
}
|
|
2780
|
+
function cmdProtection(sub, args, flags) {
|
|
2781
|
+
const usageLine = "usage: agent-switch protection [--json] | protection set <provider/name> [--on|--off] [--threshold <1-100>]";
|
|
2782
|
+
if (sub === undefined || sub === "status" || sub === "list") {
|
|
2783
|
+
const rows = listAllProfiles().map((p) => ({ profile: `${p.provider}/${p.name}`, ...readProtection(p.provider, p.name) }));
|
|
2784
|
+
if (flags.json)
|
|
2785
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
2786
|
+
if (rows.length === 0)
|
|
2787
|
+
return void console.log("No profiles.");
|
|
2788
|
+
for (const r of rows)
|
|
2789
|
+
console.log(`${r.profile.padEnd(24)} ${r.enabled ? "on " : "off"} at ${r.thresholdPct}%`);
|
|
2790
|
+
console.log("\nA warning when a profile reaches its threshold — nothing is paused or switched.");
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (sub !== "set")
|
|
2794
|
+
die(usageLine);
|
|
2795
|
+
const m = parseMemberArg(args[0], usageLine);
|
|
2796
|
+
requireProfile(m.provider, m.name, "protection set");
|
|
2797
|
+
if (flags.on && flags.off)
|
|
2798
|
+
die("pass either --on or --off, not both");
|
|
2799
|
+
const threshold = typeof flags.threshold === "string" ? Number(flags.threshold) : undefined;
|
|
2800
|
+
if (threshold !== undefined && !(Number.isInteger(threshold) && threshold >= 1 && threshold <= 100))
|
|
2801
|
+
die("--threshold must be a whole number between 1 and 100");
|
|
2802
|
+
if (!flags.on && !flags.off && threshold === undefined)
|
|
2803
|
+
die(usageLine);
|
|
2804
|
+
const next = setProtection(m.provider, m.name, {
|
|
2805
|
+
...(flags.on ? { enabled: true } : {}),
|
|
2806
|
+
...(flags.off ? { enabled: false } : {}),
|
|
2807
|
+
...(threshold !== undefined ? { thresholdPct: threshold } : {}),
|
|
2808
|
+
});
|
|
2809
|
+
logActivity("protection", `${m.provider}/${m.name}`, `${next.enabled ? "on" : "off"} at ${next.thresholdPct}%`, "user ran `agent-switch protection set`");
|
|
2810
|
+
if (flags.json)
|
|
2811
|
+
return void console.log(JSON.stringify({ profile: `${m.provider}/${m.name}`, ...next }, null, 2));
|
|
2812
|
+
console.log(`Session protection for ${m.provider}/${m.name}: ${next.enabled ? "on" : "off"} at ${next.thresholdPct}%.`);
|
|
2813
|
+
}
|
|
2814
|
+
function poolMappings(id) {
|
|
2815
|
+
return mappingRows().filter((r) => r.name === `${POOL_PREFIX}${id}`).map((r) => `${r.path} (${r.provider})`);
|
|
2816
|
+
}
|
|
2817
|
+
function cmdPool(sub, args, flags) {
|
|
2818
|
+
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]";
|
|
2819
|
+
const json = !!flags.json;
|
|
2820
|
+
const out = (v, text) => console.log(json ? JSON.stringify(v, null, 2) : text);
|
|
2821
|
+
try {
|
|
2822
|
+
switch (sub) {
|
|
2823
|
+
case undefined:
|
|
2824
|
+
case "list": {
|
|
2825
|
+
const pools = readPools().map((p) => ({ ...p, rules: poolMappings(p.id) }));
|
|
2826
|
+
if (json)
|
|
2827
|
+
return void console.log(JSON.stringify(pools, null, 2));
|
|
2828
|
+
if (pools.length === 0)
|
|
2829
|
+
return void console.log("No pools. Pools are optional — create one with: agent-switch pool create <name>");
|
|
2830
|
+
for (const p of pools) {
|
|
2831
|
+
console.log(`${p.id} "${p.name}" ${p.members.length ? p.members.join(", ") : "(no members)"}${p.rules.length ? ` · folders: ${p.rules.join(", ")}` : ""}`);
|
|
2832
|
+
}
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
case "create": {
|
|
2836
|
+
if (!args[0])
|
|
2837
|
+
die(usageLine);
|
|
2838
|
+
const p = createPool(args.join(" "));
|
|
2839
|
+
logActivity("pool", p.id, "created", "user ran `agent-switch pool create`");
|
|
2840
|
+
return out(p, `Created pool ${p.id} ("${p.name}"). Add members: agent-switch pool add ${p.id} <provider/name>`);
|
|
2841
|
+
}
|
|
2842
|
+
case "rename": {
|
|
2843
|
+
if (!args[0] || !args[1])
|
|
2844
|
+
die(usageLine);
|
|
2845
|
+
const p = renamePool(args[0], args.slice(1).join(" "));
|
|
2846
|
+
logActivity("pool", p.id, `renamed to "${p.name}"`, "user ran `agent-switch pool rename`");
|
|
2847
|
+
return out(p, `Pool ${p.id} is now called "${p.name}" (its id and folder rules are unchanged).`);
|
|
2848
|
+
}
|
|
2849
|
+
case "delete": {
|
|
2850
|
+
if (!args[0])
|
|
2851
|
+
die(usageLine);
|
|
2852
|
+
const rules = poolMappings(args[0]);
|
|
2853
|
+
if (rules.length > 0)
|
|
2854
|
+
die(`pool "${args[0]}" is used by folder rules: ${rules.join(", ")}. Remove them first (agent-switch unmap <dir>).`);
|
|
2855
|
+
deletePool(args[0]);
|
|
2856
|
+
logActivity("pool", args[0], "deleted", "user ran `agent-switch pool delete`");
|
|
2857
|
+
return out({ deleted: args[0] }, `Deleted pool ${args[0]}. Its profiles are untouched.`);
|
|
2858
|
+
}
|
|
2859
|
+
case "add":
|
|
2860
|
+
case "remove": {
|
|
2861
|
+
if (!args[0])
|
|
2862
|
+
die(usageLine);
|
|
2863
|
+
const m = parseMemberArg(args[1], usageLine);
|
|
2864
|
+
if (sub === "add")
|
|
2865
|
+
requireProfile(m.provider, m.name, "pool add");
|
|
2866
|
+
const key = `${m.provider}/${m.name}`;
|
|
2867
|
+
const p = sub === "add" ? addPoolMember(args[0], key) : removePoolMember(args[0], key);
|
|
2868
|
+
logActivity("pool", p.id, `${sub === "add" ? "added" : "removed"} ${key}`, `user ran \`agent-switch pool ${sub}\``);
|
|
2869
|
+
return out(p, `${sub === "add" ? "Added" : "Removed"} ${key} ${sub === "add" ? "to" : "from"} pool ${p.id}.`);
|
|
2870
|
+
}
|
|
2871
|
+
case "suggest": {
|
|
2872
|
+
const proposals = suggestPoolsFromLabels();
|
|
2873
|
+
if (!flags.confirm) {
|
|
2874
|
+
if (json)
|
|
2875
|
+
return void console.log(JSON.stringify({ preview: true, proposals }, null, 2));
|
|
2876
|
+
if (proposals.length === 0)
|
|
2877
|
+
return void console.log("No labelled profiles — nothing to suggest.");
|
|
2878
|
+
console.log("Preview — nothing is created until you run `agent-switch pool suggest --confirm`:");
|
|
2879
|
+
for (const p of proposals)
|
|
2880
|
+
console.log(` ${p.id} ${p.members.join(", ")}${p.exists ? " (exists — skipped)" : ""}`);
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
const created = [];
|
|
2884
|
+
for (const prop of proposals) {
|
|
2885
|
+
if (prop.exists)
|
|
2886
|
+
continue;
|
|
2887
|
+
const pool = createPool(prop.name);
|
|
2888
|
+
for (const m of prop.members)
|
|
2889
|
+
addPoolMember(pool.id, m);
|
|
2890
|
+
created.push(pool.id);
|
|
2891
|
+
logActivity("pool", pool.id, `created from label ${prop.name}`, "user confirmed `agent-switch pool suggest`");
|
|
2892
|
+
}
|
|
2893
|
+
return out({ created }, created.length ? `Created pools: ${created.join(", ")}.` : "Nothing to create.");
|
|
2894
|
+
}
|
|
2895
|
+
default:
|
|
2896
|
+
die(usageLine);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
catch (e) {
|
|
2900
|
+
die(e.message);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2119
2903
|
// ---------- main -------------------------------------------------------------
|
|
2120
2904
|
async function main() {
|
|
2121
2905
|
const argv = process.argv.slice(2);
|
|
@@ -2144,13 +2928,22 @@ async function main() {
|
|
|
2144
2928
|
}
|
|
2145
2929
|
}
|
|
2146
2930
|
switch (cmd) {
|
|
2147
|
-
case "add": return cmdAdd(providerId, positional[0]);
|
|
2931
|
+
case "add": return cmdAdd(providerId, positional[0], typeof flags.browser === "string" ? flags.browser : undefined);
|
|
2932
|
+
case "login": return cmdLogin(providerId, positional[0], typeof flags.browser === "string" ? flags.browser : undefined);
|
|
2933
|
+
case "browsers": return cmdBrowsers(!!flags.json);
|
|
2148
2934
|
case "import": return cmdImport(providerId, positional[0]);
|
|
2149
2935
|
case "use": return cmdUse(providerId, positional[0]);
|
|
2150
2936
|
case "deactivate": return cmdDeactivate(providerId);
|
|
2151
2937
|
case "label": return cmdLabel(providerId, positional[0], positional[1]);
|
|
2938
|
+
case "color":
|
|
2939
|
+
case "colour": return cmdColor(providerId, positional[0], positional[1]);
|
|
2940
|
+
case "activate": return cmdActivate(!!flags.json);
|
|
2941
|
+
case "desktop": return cmdDesktop(!!flags.json);
|
|
2942
|
+
case "disable": return cmdDisable(providerId, positional[0], true);
|
|
2943
|
+
case "enable": return cmdDisable(providerId, positional[0], false);
|
|
2944
|
+
case "mark-shared": return cmdMarkShared(providerId, positional[0], !!flags.off);
|
|
2152
2945
|
case "autoswitch": return cmdAutoswitch(providerId, providerExplicit, positional[0], flags);
|
|
2153
|
-
case "reset": return cmdReset(providerId, positional[0]);
|
|
2946
|
+
case "reset": return cmdReset(providerId, positional[0], !!flags.yes);
|
|
2154
2947
|
case "rename": return cmdRename(providerId, positional[0], positional[1]);
|
|
2155
2948
|
case "providers": return cmdProviders(providerId, providerExplicit, positional[0], flags);
|
|
2156
2949
|
case "gui": return cmdGui();
|
|
@@ -2159,14 +2952,23 @@ async function main() {
|
|
|
2159
2952
|
case "check-update":
|
|
2160
2953
|
case "check-updates": return cmdCheckUpdate(!!flags.json);
|
|
2161
2954
|
case "open": return cmdOpen(positional[0], positional[1]);
|
|
2955
|
+
case "launcher":
|
|
2956
|
+
case "launchers": return cmdLauncher(positional[0], positional.slice(1), flags);
|
|
2957
|
+
case "app-settings": return cmdAppSettings(positional[0], positional.slice(1), flags);
|
|
2162
2958
|
case "apps": return cmdApps(!!flags.json);
|
|
2163
2959
|
case "notify": return cmdNotify(flags);
|
|
2164
2960
|
case "notifications": return cmdNotifications(positional[0], !!flags.json);
|
|
2165
2961
|
case "os-notify": return cmdOsNotify(positional[0], !!flags.json);
|
|
2166
2962
|
case "uninstall": return cmdUninstall(flags);
|
|
2167
2963
|
case "run": {
|
|
2168
|
-
|
|
2169
|
-
|
|
2964
|
+
let r;
|
|
2965
|
+
try {
|
|
2966
|
+
r = parseRun(rest);
|
|
2967
|
+
}
|
|
2968
|
+
catch (e) {
|
|
2969
|
+
die(e.message);
|
|
2970
|
+
}
|
|
2971
|
+
return cmdRun(r.providerId, r.name, r.args, r.cwd);
|
|
2170
2972
|
}
|
|
2171
2973
|
case "handoff": return cmdHandoff(providerId, positional[0], positional.slice(1), flags);
|
|
2172
2974
|
case "list":
|
|
@@ -2174,11 +2976,11 @@ async function main() {
|
|
|
2174
2976
|
case "status": return cmdStatus(providerExplicit ? providerId : undefined, positional[0], !!flags.json);
|
|
2175
2977
|
case "current": return cmdCurrent(providerExplicit ? providerId : undefined);
|
|
2176
2978
|
case "whoami": return cmdWhoami(providerId, positional[0]);
|
|
2177
|
-
case "dir": return cmdDir(providerId, !!flags.guard);
|
|
2979
|
+
case "dir": return cmdDir(providerId, !!flags.guard, !!flags.json, !!flags.plain);
|
|
2178
2980
|
case "guard": return cmdGuard(positional[0], !!flags.json);
|
|
2179
|
-
case "map": return cmdMap(providerId, positional[0], positional[1]);
|
|
2981
|
+
case "map": return cmdMap(providerId, positional[0], positional[1], flags);
|
|
2180
2982
|
case "unmap": return cmdUnmap(providerExplicit ? providerId : undefined, positional[0]);
|
|
2181
|
-
case "mappings": return cmdMappings();
|
|
2983
|
+
case "mappings": return cmdMappings(rest.includes("--json"));
|
|
2182
2984
|
case "share": return cmdShare(positional[0], rest.slice(1));
|
|
2183
2985
|
case "config": return cmdConfig(positional[0], positional[1], flags);
|
|
2184
2986
|
case "sessions":
|
|
@@ -2196,8 +2998,11 @@ async function main() {
|
|
|
2196
2998
|
case "rebind": return cmdRebind(providerId, providerExplicit, positional[0], flags);
|
|
2197
2999
|
case "web": return cmdWeb(positional[0]);
|
|
2198
3000
|
case "remove":
|
|
2199
|
-
case "rm": return cmdRemove(providerId, positional[0], !!flags.force);
|
|
2200
|
-
case "shellenv":
|
|
3001
|
+
case "rm": return cmdRemove(providerId, positional[0], !!flags.force, !!flags.yes);
|
|
3002
|
+
case "shellenv":
|
|
3003
|
+
return positional[0] === "install"
|
|
3004
|
+
? cmdShellenv(flags.shell, "install", !!flags.json)
|
|
3005
|
+
: cmdShellenv(flags.shell ?? positional[0]);
|
|
2201
3006
|
case "service": return cmdService(positional[0]);
|
|
2202
3007
|
case "usage": return cmdUsageHistory(providerId, positional[0], flags);
|
|
2203
3008
|
case "tooling":
|
|
@@ -2210,7 +3015,13 @@ async function main() {
|
|
|
2210
3015
|
die(`usage: agent-switch tooling [install|upgrade <${TOOL_IDS.join("|")}>] [notes [<tool>]] [--json]`);
|
|
2211
3016
|
}
|
|
2212
3017
|
return cmdTooling(!!flags.json);
|
|
2213
|
-
case "doctor": return process.exit(await runDoctor());
|
|
3018
|
+
case "doctor": return process.exit(await runDoctor(!!flags.json));
|
|
3019
|
+
case "auth-status": return cmdAuthStatus(providerExplicit ? providerId : undefined, !!flags.json);
|
|
3020
|
+
case "activity": return cmdActivity(flags);
|
|
3021
|
+
case "protection": return cmdProtection(positional[0], positional.slice(1), flags);
|
|
3022
|
+
case "pool":
|
|
3023
|
+
case "pools": return cmdPool(positional[0], positional.slice(1), flags);
|
|
3024
|
+
case "gui-settings": return cmdGuiSettings(positional[0], positional.slice(1), flags);
|
|
2214
3025
|
case "help":
|
|
2215
3026
|
case "--help":
|
|
2216
3027
|
case "-h": return usage();
|