@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/dist/shellenv.js CHANGED
@@ -34,6 +34,9 @@
34
34
  * A side effect worth having: a genuinely broken agent-switch now surfaces its
35
35
  * error instead of silently degrading the shell onto the default config dir.
36
36
  */
37
+ import { spawnSync } from "node:child_process";
38
+ import * as fs from "node:fs";
39
+ import * as path from "node:path";
37
40
  import { allProviders } from "./providers.js";
38
41
  import { GUARD_REFUSED_EXIT } from "./single-load.js";
39
42
  export const SHELLS = ["zsh", "bash", "fish", "powershell"];
@@ -59,11 +62,56 @@ export function detectShell(requested, env = process.env, platform = process.pla
59
62
  return "zsh";
60
63
  return "zsh"; // sensible POSIX default when $SHELL is unset
61
64
  }
65
+ // The CLI the running app activated (`agent-switch activate` writes its path to
66
+ // <home>/active-cli), so a dev build and an installed app each bring their own;
67
+ // a missing or vanished path falls back to agent-switch on PATH.
68
+ const POSIX_BIN = `__agent_switch_bin() {
69
+ local p
70
+ p="$(cat "\${AGENT_SWITCH_HOME:-$HOME/.agent-switch}/active-cli" 2>/dev/null)"
71
+ if [ -n "$p" ] && [ -x "$p" ]; then printf '%s\\n' "$p"
72
+ elif command -v agent-switch >/dev/null 2>&1; then printf 'agent-switch\\n'
73
+ fi
74
+ }
75
+ __agent_switch() {
76
+ local as
77
+ as="$(__agent_switch_bin)"
78
+ command "\${as:-agent-switch}" "$@"
79
+ }`;
80
+ const FISH_BIN = `function __agent_switch_bin
81
+ set -l home $HOME/.agent-switch
82
+ set -q AGENT_SWITCH_HOME; and set home $AGENT_SWITCH_HOME
83
+ if test -r $home/active-cli
84
+ set -l p (cat $home/active-cli)
85
+ if test -x "$p"
86
+ echo $p
87
+ return
88
+ end
89
+ end
90
+ type -q agent-switch; and echo agent-switch
91
+ end
92
+ function __agent_switch
93
+ set -l as (__agent_switch_bin)
94
+ test -n "$as"; or set as agent-switch
95
+ command $as $argv
96
+ end`;
97
+ const POWERSHELL_BIN = `function __agent_switch_bin {
98
+ $home_ = if ($env:AGENT_SWITCH_HOME) { $env:AGENT_SWITCH_HOME } else { Join-Path $HOME '.agent-switch' }
99
+ $p = Get-Content -Path (Join-Path $home_ 'active-cli') -ErrorAction SilentlyContinue | Select-Object -First 1
100
+ if ($p -and (Test-Path $p)) { return $p }
101
+ if (Get-Command agent-switch -ErrorAction SilentlyContinue) { return 'agent-switch' }
102
+ return $null
103
+ }
104
+ function __agent_switch {
105
+ $as = __agent_switch_bin
106
+ if (-not $as) { $as = 'agent-switch' }
107
+ if ($as -like '*.js') { & node $as @args } else { & $as @args }
108
+ }`;
62
109
  function posixWrapper(binary, envVar, id) {
63
110
  return `${binary}() {
64
- local dir rc
65
- command -v agent-switch >/dev/null 2>&1 || { command ${binary} "$@"; return; }
66
- dir="$(command agent-switch dir --provider ${id} --guard)"
111
+ local dir rc as
112
+ as="$(__agent_switch_bin)"
113
+ [ -n "$as" ] || { command ${binary} "$@"; return; }
114
+ dir="$(command "$as" dir --provider ${id} --guard)"
67
115
  rc=$?
68
116
  if [ $rc -eq ${GUARD_REFUSED_EXIT} ]; then return $rc; fi
69
117
  if [ -n "$dir" ]; then
@@ -75,11 +123,12 @@ function posixWrapper(binary, envVar, id) {
75
123
  }
76
124
  function fishWrapper(binary, envVar, id) {
77
125
  return `function ${binary}
78
- if not type -q agent-switch
126
+ set -l as (__agent_switch_bin)
127
+ if test -z "$as"
79
128
  command ${binary} $argv
80
129
  return
81
130
  end
82
- set -l dir (command agent-switch dir --provider ${id} --guard)
131
+ set -l dir (command $as dir --provider ${id} --guard)
83
132
  set -l rc $status
84
133
  if test $rc -eq ${GUARD_REFUSED_EXIT}
85
134
  return $rc
@@ -95,8 +144,9 @@ function powershellWrapper(binary, envVar, id) {
95
144
  return `function ${binary} {
96
145
  $exe = Get-Command ${binary} -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
97
146
  if (-not $exe) { Write-Error '${binary} not found on PATH.'; return }
98
- if (-not (Get-Command agent-switch -ErrorAction SilentlyContinue)) { & $exe.Source @args; return }
99
- $dir = (& agent-switch dir --provider ${id} --guard)
147
+ $as = __agent_switch_bin
148
+ if (-not $as) { & $exe.Source @args; return }
149
+ $dir = (__agent_switch dir --provider ${id} --guard)
100
150
  if ($LASTEXITCODE -eq ${GUARD_REFUSED_EXIT}) { return }
101
151
  if ($dir) {
102
152
  $prev = Get-Item -Path Env:\\${envVar} -ErrorAction SilentlyContinue
@@ -107,8 +157,8 @@ function powershellWrapper(binary, envVar, id) {
107
157
  }
108
158
  }`;
109
159
  }
110
- function build(header, wrappers, asw) {
111
- return [header, ...wrappers, asw].join("\n");
160
+ function build(header, bin, wrappers, asw) {
161
+ return [header, bin, ...wrappers, asw].join("\n");
112
162
  }
113
163
  export function shellenvScript(shell) {
114
164
  const providers = allProviders();
@@ -117,34 +167,85 @@ export function shellenvScript(shell) {
117
167
  const asw = `# Convenience: "asw" lists all; "asw work" switches claude; "asw codex work" switches a provider
118
168
  function asw
119
169
  if test (count $argv) -eq 0
120
- command agent-switch list
170
+ __agent_switch list
121
171
  else if contains -- $argv[1] claude codex antigravity
122
- command agent-switch use $argv[2] --provider $argv[1]
172
+ __agent_switch use $argv[2] --provider $argv[1]
123
173
  else
124
- command agent-switch use $argv
174
+ __agent_switch use $argv
125
175
  end
126
176
  end`;
127
- return build(header, providers.map((p) => fishWrapper(p.binary, p.envVar, p.id)), asw);
177
+ return build(header, FISH_BIN, providers.map((p) => fishWrapper(p.binary, p.envVar, p.id)), asw);
128
178
  }
129
179
  if (shell === "powershell") {
130
180
  const header = `# agent-switch shell integration — add to $PROFILE: agent-switch shellenv --shell powershell | Out-String | Invoke-Expression`;
131
181
  const asw = `# Convenience: "asw" lists all; "asw work" switches claude; "asw codex work" switches a provider
132
182
  function asw {
133
- if ($args.Count -eq 0) { command agent-switch list; return }
134
- if (@('claude','codex','antigravity') -contains $args[0]) { & agent-switch use $args[1] --provider $args[0] }
135
- else { & agent-switch use @args }
183
+ if ($args.Count -eq 0) { __agent_switch list; return }
184
+ if (@('claude','codex','antigravity') -contains $args[0]) { __agent_switch use $args[1] --provider $args[0] }
185
+ else { __agent_switch use @args }
136
186
  }`;
137
- return build(header, providers.map((p) => powershellWrapper(p.binary, p.envVar, p.id)), asw);
187
+ return build(header, POWERSHELL_BIN, providers.map((p) => powershellWrapper(p.binary, p.envVar, p.id)), asw);
138
188
  }
139
189
  // zsh + bash share the POSIX snippet.
140
190
  const header = `# agent-switch shell integration — add to your rc file: eval "$(agent-switch shellenv)"`;
141
191
  const asw = `# Convenience: "asw" lists all; "asw work" switches claude; "asw codex work" switches a provider
142
192
  asw() {
143
- if [ $# -eq 0 ]; then command agent-switch list; return; fi
193
+ if [ $# -eq 0 ]; then __agent_switch list; return; fi
144
194
  case "$1" in
145
- claude|codex|antigravity) command agent-switch use "$2" --provider "$1" ;;
146
- *) command agent-switch use "$@" ;;
195
+ claude|codex|antigravity) __agent_switch use "$2" --provider "$1" ;;
196
+ *) __agent_switch use "$@" ;;
147
197
  esac
148
198
  }`;
149
- return build(header, providers.map((p) => posixWrapper(p.binary, p.envVar, p.id)), asw);
199
+ return build(header, POSIX_BIN, providers.map((p) => posixWrapper(p.binary, p.envVar, p.id)), asw);
200
+ }
201
+ /** The line that loads the integration, per shell (same as the docs table). */
202
+ export const SHELLENV_LINE = {
203
+ zsh: 'eval "$(agent-switch shellenv)"',
204
+ bash: 'eval "$(agent-switch shellenv)"',
205
+ fish: "agent-switch shellenv --shell fish | source",
206
+ powershell: "agent-switch shellenv --shell powershell | Out-String | Invoke-Expression",
207
+ };
208
+ /** The startup file `shellenv install` writes to. macOS opens login shells,
209
+ * which read ~/.bash_profile rather than ~/.bashrc. For PowerShell only the
210
+ * shell knows its profile — Windows PowerShell 5.1 and a Documents folder
211
+ * redirected to OneDrive both live elsewhere — so it is asked first. */
212
+ export function rcFileFor(shell, home, platform = process.platform, askProfile = powershellProfile) {
213
+ if (shell === "zsh")
214
+ return path.join(home, ".zshrc");
215
+ if (shell === "bash")
216
+ return path.join(home, platform === "darwin" ? ".bash_profile" : ".bashrc");
217
+ if (shell === "fish")
218
+ return path.join(home, ".config", "fish", "config.fish");
219
+ return askProfile() ?? path.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
220
+ }
221
+ /** Which PowerShell to ask first: the one this command runs under. Windows
222
+ * PowerShell 5.1 puts only `WindowsPowerShell` paths on PSModulePath; pwsh adds
223
+ * its own `PowerShell\7` (or `Documents\PowerShell`) ones. */
224
+ export function powershellOrder(env = process.env) {
225
+ const p = env.PSModulePath ?? "";
226
+ const pwsh = /[\\/]PowerShell[\\/](7|Modules)/i.test(p);
227
+ return !pwsh && /WindowsPowerShell/i.test(p) ? ["powershell", "pwsh"] : ["pwsh", "powershell"];
228
+ }
229
+ /** `$PROFILE` from the PowerShell the user runs, then the other one; null when
230
+ * neither answers. */
231
+ export function powershellProfile() {
232
+ for (const exe of powershellOrder()) {
233
+ const r = spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE"], { encoding: "utf8", timeout: 5000, windowsHide: true });
234
+ const out = r.status === 0 ? r.stdout.trim() : "";
235
+ if (out)
236
+ return out;
237
+ }
238
+ return null;
239
+ }
240
+ /** Append the loader line to the shell's startup file, once. `present` says
241
+ * whether any known startup file already loads it (the doctor check). */
242
+ export function installShellIntegration(shell, home, present) {
243
+ const file = rcFileFor(shell, home);
244
+ if (present)
245
+ return { shell, file, alreadyInstalled: true };
246
+ const before = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
247
+ const sep = before === "" || before.endsWith("\n") ? "" : "\n";
248
+ fs.mkdirSync(path.dirname(file), { recursive: true });
249
+ fs.writeFileSync(file, `${before}${sep}\n# agent-switch shell integration\n${SHELLENV_LINE[shell]}\n`);
250
+ return { shell, file, alreadyInstalled: false };
150
251
  }
@@ -25,7 +25,12 @@ import * as path from "node:path";
25
25
  import { liveSessionPids } from "./api.js";
26
26
  import { pidCwd } from "./sessions.js";
27
27
  export const GUARD_POLICIES = ["block", "warn", "off"];
28
+ /** What a state file without a `guard` key meant before 2.2 — kept so an
29
+ * upgrade never changes a user's effective guard. */
28
30
  export const DEFAULT_GUARD_POLICY = "block";
31
+ /** A fresh install (no state file and no profile yet) starts on `warn`: a second load is
32
+ * reported, never refused, so nothing changes how a tool starts. */
33
+ export const FRESH_INSTALL_GUARD_POLICY = "warn";
29
34
  export function isGuardPolicy(v) {
30
35
  return typeof v === "string" && GUARD_POLICIES.includes(v);
31
36
  }
@@ -152,3 +152,14 @@ export function freshAccessToken(configDir, deps = defaultFreshenDeps()) {
152
152
  const creds = deps.readCredential(configDir);
153
153
  return creds ? accessTokenOf(creds) : null;
154
154
  }
155
+ /**
156
+ * The token for a usage read. A profile that shares its login with the default
157
+ * install (seeded by `import`) is never refreshed: whichever side refreshes
158
+ * first invalidates the other, so its stored token is used only while valid.
159
+ */
160
+ export function usageToken(configDir, deps, sharesDefaultLogin) {
161
+ if (!sharesDefaultLogin)
162
+ return freshAccessToken(configDir, deps);
163
+ const creds = deps.readCredential(configDir);
164
+ return creds && !needsFreshen(creds, deps.now(), deps.bufferMs) ? accessTokenOf(creds) : null;
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-switch",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Switch between multiple Claude Code accounts on macOS, Linux, and Windows via isolated CLAUDE_CONFIG_DIR profiles",
5
5
  "repository": {
6
6
  "type": "git",