claude-multiacc 2.0.27 → 2.0.28

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/install.sh CHANGED
@@ -20,6 +20,7 @@ set -u
20
20
  REPO_DIR="$(cd "$(dirname "$0")" && pwd -P)"
21
21
  # shellcheck source=lib/common.sh
22
22
  . "$REPO_DIR/lib/common.sh"
23
+ . "$REPO_DIR/lib/install_actions.sh"
23
24
  # common.sh is sourced in claude mode (ACC_ROOT = the claude pool); the codex pool
24
25
  # root is needed here too for the codex agents and uninstall/purge messaging.
25
26
  CODEX_ACC_ROOT="${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-$HOME/.codex-accounts}}"
@@ -31,6 +32,7 @@ NO_SCHEDULE=0
31
32
  INSTANCE_OVERRIDE=""
32
33
  while [ $# -gt 0 ]; do
33
34
  case "$1" in
35
+ --help|-h) cat "$REPO_DIR/lib/install-help.txt"; exit 0 ;;
34
36
  --uninstall) UNINSTALL=1; shift ;;
35
37
  --purge-data) PURGE=1; shift ;;
36
38
  --server) SERVER_OVERRIDE="${2:?--server requires a value}"; shift 2 ;;
@@ -127,50 +129,9 @@ cron_env_prefix() {
127
129
  printf "CLAUDE_ACCOUNTS_ROOT='%s' CODEX_ACCOUNTS_ROOT='%s' " "$ACC_ROOT" "$CODEX_ACC_ROOT"
128
130
  }
129
131
 
130
- is_pool_root() { # true when $1 is safe to delete as an account pool
131
- case "$1" in
132
- ''|/|"$HOME") return 1 ;;
133
- */.claude-accounts|*/.codex-accounts) return 0 ;;
134
- /*) [ -f "$1/accounts.json" ] ;;
135
- *) return 1 ;;
136
- esac
137
- }
138
-
139
- strip_block() { # remove our marked block from a file (portable, no sed -i)
140
- local f="$1"
141
- [ -f "$f" ] || return 0
142
- # Unmatched begin marker (end line hand-deleted): stripping would eat the rest
143
- # of the rc file. Leave it alone and say so.
144
- if grep -qF "$MARK_BEGIN" "$f" && ! grep -qF "$MARK_END" "$f"; then
145
- echo " WARNING: $f has an unterminated claude-multiacc block — fix it by hand; not touching this file" >&2
146
- return 1
147
- fi
148
- awk -v b="$MARK_BEGIN" -v e="$MARK_END" '
149
- $0 == b { skip = 1; next }
150
- $0 == e { skip = 0; next }
151
- !skip { print }
152
- ' "$f" > "$f.claude-multiacc.tmp" && mv "$f.claude-multiacc.tmp" "$f"
153
- }
154
-
155
- path_block_body() {
156
- # Move (not just add) the shim dir to the front: later rc lines prepend
157
- # ~/.local/bin, so a plain add-once guard would leave the real binary first.
158
- printf 'PATH="$(printf %%s ":$PATH:" | sed '\''s|:%s/bin:|:|g; s|^:||; s|:$||'\'')"\n' "$REPO_DIR"
159
- printf 'export PATH="%s/bin:$PATH"\n' "$REPO_DIR"
160
- }
161
-
162
- append_block() { # strip then append our PATH block to a file
163
- local f="$1"
164
- strip_block "$f" || return 1 # never create a second block next to a broken one
165
- {
166
- printf '%s\n' "$MARK_BEGIN"
167
- path_block_body
168
- printf '%s\n' "$MARK_END"
169
- } >> "$f"
170
- }
171
-
172
132
  mac_schedule_install() {
173
133
  mkdir -p "$HOME/Library/LaunchAgents"
134
+ . "$REPO_DIR/lib/install_calendar.sh"
174
135
  cat > "$PLIST_LIMITS" <<EOF
175
136
  <?xml version="1.0" encoding="UTF-8"?>
176
137
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -183,52 +144,12 @@ mac_schedule_install() {
183
144
  </array>
184
145
  $(plist_env_block) <key>StartInterval</key><integer>900</integer>
185
146
  <key>RunAtLoad</key><true/>
186
- <!-- The pass hands its readings to the peers from a detached push; launchd
187
- must not kill that push with the job's process group. -->
147
+ <!-- Preserve the detached telemetry push after this job exits. -->
188
148
  <key>AbandonProcessGroup</key><true/>
189
149
  <key>StandardOutPath</key><string>/dev/null</string>
190
150
  <key>StandardErrorPath</key><string>/dev/null</string>
191
151
  </dict></plist>
192
152
  EOF
193
- cat > "$PLIST_HEALTH" <<EOF
194
- <?xml version="1.0" encoding="UTF-8"?>
195
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
196
- <plist version="1.0"><dict>
197
- <key>Label</key><string>$LABEL.health</string>
198
- <key>ProgramArguments</key><array>
199
- <string>$REPO_DIR/bin/claude-accounts</string>
200
- <string>health</string>
201
- </array>
202
- $(plist_env_block) <key>StartCalendarInterval</key><dict>
203
- <key>Weekday</key><integer>1</integer>
204
- <key>Hour</key><integer>9</integer>
205
- <key>Minute</key><integer>17</integer>
206
- </dict>
207
- <key>StandardOutPath</key><string>/dev/null</string>
208
- <key>StandardErrorPath</key><string>/dev/null</string>
209
- </dict></plist>
210
- EOF
211
- # Daily auto-update (04:07). No-op unless this is an npm or git install.
212
- cat > "$PLIST_UPDATE" <<EOF
213
- <?xml version="1.0" encoding="UTF-8"?>
214
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
215
- <plist version="1.0"><dict>
216
- <key>Label</key><string>$LABEL.update</string>
217
- <key>ProgramArguments</key><array>
218
- <string>$REPO_DIR/bin/claude-accounts</string>
219
- <string>self-update</string>
220
- <string>--quiet</string>
221
- </array>
222
- $(plist_env_block) <key>StartCalendarInterval</key><dict>
223
- <key>Hour</key><integer>4</integer>
224
- <key>Minute</key><integer>7</integer>
225
- </dict>
226
- <key>StandardOutPath</key><string>/dev/null</string>
227
- <key>StandardErrorPath</key><string>/dev/null</string>
228
- </dict></plist>
229
- EOF
230
- # Codex pool: its own limits/health agents, so either provider's telemetry can
231
- # fail or be removed without touching the other's.
232
153
  cat > "$PLIST_CODEX_LIMITS" <<EOF
233
154
  <?xml version="1.0" encoding="UTF-8"?>
234
155
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -246,30 +167,11 @@ $(plist_env_block) <key>StartInterval</key><integer>300</integer>
246
167
  <key>StandardErrorPath</key><string>/dev/null</string>
247
168
  </dict></plist>
248
169
  EOF
249
- cat > "$PLIST_CODEX_HEALTH" <<EOF
250
- <?xml version="1.0" encoding="UTF-8"?>
251
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
252
- <plist version="1.0"><dict>
253
- <key>Label</key><string>$LABEL.codex-health</string>
254
- <key>ProgramArguments</key><array>
255
- <string>$REPO_DIR/bin/codex-accounts</string>
256
- <string>health</string>
257
- </array>
258
- $(plist_env_block) <key>StartCalendarInterval</key><dict>
259
- <key>Weekday</key><integer>1</integer>
260
- <key>Hour</key><integer>9</integer>
261
- <key>Minute</key><integer>37</integer>
262
- </dict>
263
- <key>StandardOutPath</key><string>/dev/null</string>
264
- <key>StandardErrorPath</key><string>/dev/null</string>
265
- </dict></plist>
266
- EOF
267
- launchctl unload "$PLIST_LIMITS" 2>/dev/null || true
268
- launchctl unload "$PLIST_HEALTH" 2>/dev/null || true
269
- launchctl unload "$PLIST_UPDATE" 2>/dev/null || true
270
- launchctl unload "$PLIST_CODEX_LIMITS" 2>/dev/null || true
271
- launchctl unload "$PLIST_CODEX_HEALTH" 2>/dev/null || true
272
- launchctl load -w "$PLIST_LIMITS" 2>/dev/null || echo " (launchctl load limits agent failed — limits refresh will rely on the shim's opportunistic kick)"
170
+ local plist
171
+ for plist in "$PLIST_LIMITS" "$PLIST_HEALTH" "$PLIST_UPDATE" "$PLIST_CODEX_LIMITS" "$PLIST_CODEX_HEALTH"; do
172
+ launchctl unload "$plist" 2>/dev/null || true
173
+ done
174
+ launchctl load -w "$PLIST_LIMITS" 2>/dev/null || echo " (limits agent failed — the shim will refresh on demand)"
273
175
  launchctl load -w "$PLIST_HEALTH" 2>/dev/null || true
274
176
  launchctl load -w "$PLIST_CODEX_LIMITS" 2>/dev/null || true
275
177
  launchctl load -w "$PLIST_CODEX_HEALTH" 2>/dev/null || true
@@ -317,183 +219,6 @@ linux_schedule_remove() {
317
219
  rm -f "$tmp"
318
220
  }
319
221
 
320
- do_uninstall() {
321
- echo "claude-multiacc: uninstalling (restoring stock behavior)"
322
- # An instance install never wrote a shell rc block, a /etc/profile.d file or the
323
- # /usr/local/bin shims — those belong to the DEFAULT install and are shared with it.
324
- # Removing them here would disable the operator's shim while only one instance was
325
- # being uninstalled, so an instance removes exactly its own agents.
326
- if [ -n "$INSTANCE" ]; then
327
- echo " instance $INSTANCE: removing its agents only (PATH block and shims belong to the default install)"
328
- else
329
- strip_block "$HOME/.zshenv"
330
- strip_block "$HOME/.zprofile"
331
- strip_block "$HOME/.zshrc"
332
- strip_block "$HOME/.bashrc"
333
- strip_block "$HOME/.bash_profile"
334
- strip_block "$HOME/.profile"
335
- fi
336
- if [ "$(machine_kind)" = "mac" ]; then
337
- mac_schedule_remove
338
- else
339
- linux_schedule_remove
340
- if [ -z "$INSTANCE" ]; then
341
- [ -f "$PROFILED" ] && rm -f "$PROFILED"
342
- if [ -L /usr/local/bin/claude ] && [ "$(canon_path /usr/local/bin/claude)" = "$(canon_path "$REPO_DIR/bin/claude")" ]; then
343
- rm -f /usr/local/bin/claude
344
- echo " removed /usr/local/bin/claude shim symlink"
345
- fi
346
- if [ -L /usr/local/bin/codex ] && [ "$(canon_path /usr/local/bin/codex)" = "$(canon_path "$REPO_DIR/bin/codex")" ]; then
347
- rm -f /usr/local/bin/codex
348
- echo " removed /usr/local/bin/codex shim symlink"
349
- fi
350
- fi
351
- fi
352
- local root
353
- for root in "$ACC_ROOT" "$CODEX_ACC_ROOT"; do
354
- if [ "$PURGE" = "1" ]; then
355
- # Either the conventional location, or a directory that is provably a pool (it
356
- # holds a manifest) — an instance root lives anywhere, so the manifest is what
357
- # makes `rm -rf` safe. Anything else is left alone.
358
- if is_pool_root "$root"; then
359
- rm -rf "$root"; echo " purged $root"
360
- else
361
- echo " refusing to purge unusual accounts root: $root" >&2
362
- fi
363
- fi
364
- done
365
- if [ "$PURGE" != "1" ]; then
366
- echo " account data kept at $ACC_ROOT and $CODEX_ACC_ROOT (use --purge-data to remove)"
367
- fi
368
- echo "uninstall complete — stock claude behavior restored"
369
- }
370
-
371
- do_install() {
372
- local kind
373
- kind="$(machine_kind)"
374
- echo "claude-multiacc: installing (mode: $kind, repo: $REPO_DIR)"
375
- if [ -n "$INSTANCE" ]; then
376
- echo " instance: $INSTANCE (agents labelled $LABEL.*, pools $ACC_ROOT + $CODEX_ACC_ROOT)"
377
- fi
378
-
379
- [ -x "$REPO_DIR/bin/claude" ] || chmod +x "$REPO_DIR/bin/claude" 2>/dev/null || true
380
- [ -x "$REPO_DIR/bin/claude-accounts" ] || chmod +x "$REPO_DIR/bin/claude-accounts" 2>/dev/null || true
381
- [ -x "$REPO_DIR/bin/codex" ] || chmod +x "$REPO_DIR/bin/codex" 2>/dev/null || true
382
- [ -x "$REPO_DIR/bin/codex-accounts" ] || chmod +x "$REPO_DIR/bin/codex-accounts" 2>/dev/null || true
383
-
384
- local real
385
- if real="$(find_real_claude "$REPO_DIR/bin/claude")"; then
386
- echo " real claude binary: $real ($("$real" --version 2>/dev/null | head -1 || echo 'version unknown'))"
387
- else
388
- echo " WARNING: no real claude binary found yet — install Claude Code first (https://claude.com/claude-code)" >&2
389
- fi
390
- local real_codex
391
- if real_codex="$(find_real_codex "$REPO_DIR/bin/codex")"; then
392
- echo " real codex binary: $real_codex ($("$real_codex" --version 2>/dev/null | head -1 || echo 'version unknown'))"
393
- else
394
- echo " note: no codex binary found — the codex pool stays idle until Codex CLI is installed (npm i -g @openai/codex)"
395
- fi
396
-
397
- # Keychain-mode note: Claude Code keeps per-config-dir logins in the login Keychain
398
- # (one item per dir) whenever the session can open it, and only sessions without
399
- # keychain access (ssh, launchd background jobs) fall back to .credentials.json. The
400
- # pool reads both (lib/keychain.py); what an operator has to know is that a login
401
- # made from a GUI session is invisible to their ssh sessions — mint a portable
402
- # token for anything that must work from everywhere.
403
- if [ "$kind" = "mac" ] && [ ! -f "$HOME/.claude/.credentials.json" ] \
404
- && security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then
405
- echo " note: this Mac keeps Claude Code logins in the Keychain. Per-dir logins made here"
406
- echo " are read from it; ssh/background sessions cannot open it (they see 'locked') —"
407
- echo " use 'claude-accounts mint <acct-NN>' for accounts that must work from everywhere."
408
- fi
409
-
410
- manifest_init "${SERVER_OVERRIDE:-$DEFAULT_SERVER}"
411
- if [ -n "$SERVER_OVERRIDE" ]; then
412
- "$PYBIN" - "$MANIFEST" "$SERVER_OVERRIDE" <<'PYEOF'
413
- import json, os, sys
414
- doc = json.load(open(sys.argv[1]))
415
- doc['server'] = sys.argv[2]
416
- with open(sys.argv[1] + '.tmp', 'w') as f:
417
- json.dump(doc, f, indent=2)
418
- f.write('\n')
419
- os.replace(sys.argv[1] + '.tmp', sys.argv[1])
420
- PYEOF
421
- fi
422
- if sync_target_is_local "$(sync_target)"; then
423
- echo " account pool: $ACC_ROOT (manifest ready; sync target: none — local-only)"
424
- else
425
- echo " account pool: $ACC_ROOT (manifest ready; sync target: $(sync_target))"
426
- fi
427
- # The codex pool gets its own skeleton + manifest (same schema, separate root).
428
- if ! "$REPO_DIR/bin/codex-accounts" init-pool "${SERVER_OVERRIDE:-}" >/dev/null 2>&1; then
429
- echo " WARNING: codex pool init failed (codex-accounts init-pool)" >&2
430
- else
431
- echo " codex account pool: $CODEX_ACC_ROOT (manifest ready)"
432
- fi
433
-
434
- # An INSTANCE install never rewrites the shell rc blocks: one interactive PATH
435
- # cannot serve two pools, and clobbering the default install's block would point
436
- # the operator's shell at an instance pool. Its agents (below) carry the roots
437
- # instead, and the runner daemon invokes the shims by absolute path.
438
- if [ -n "$INSTANCE" ]; then
439
- echo " PATH block: skipped (instance install) — for a shell against this pool:"
440
- echo " export CLAUDE_ACCOUNTS_ROOT='$ACC_ROOT' CODEX_ACCOUNTS_ROOT='$CODEX_ACC_ROOT'"
441
- echo " export PATH=\"$REPO_DIR/bin:\$PATH\""
442
- fi
443
- if [ "$kind" = "mac" ]; then
444
- # .zshenv covers non-interactive zsh; the .zshrc block must be LAST so it wins
445
- # over ~/.local/bin re-prepends done earlier in .zshrc/.zprofile.
446
- # zsh reads: .zshenv always; .zprofile for login; .zshrc for interactive.
447
- # The block must end each file that later re-prepends ~/.local/bin.
448
- if [ -z "$INSTANCE" ]; then
449
- touch "$HOME/.zshenv"
450
- append_block "$HOME/.zshenv"
451
- touch "$HOME/.zprofile"
452
- append_block "$HOME/.zprofile"
453
- touch "$HOME/.zshrc"
454
- append_block "$HOME/.zshrc"
455
- [ -f "$HOME/.bash_profile" ] && append_block "$HOME/.bash_profile"
456
- [ -f "$HOME/.bashrc" ] && append_block "$HOME/.bashrc"
457
- echo " PATH block: end of ~/.zshenv, ~/.zprofile, ~/.zshrc (+ bash rc files if present)"
458
- fi
459
- [ "$NO_SCHEDULE" = "1" ] || mac_schedule_install
460
- else
461
- if [ -z "$INSTANCE" ]; then
462
- touch "$HOME/.bashrc"
463
- append_block "$HOME/.bashrc"
464
- if [ -w /etc/profile.d ] 2>/dev/null || [ "$(id -u)" = "0" ]; then
465
- {
466
- printf '%s\n' "$MARK_BEGIN"
467
- path_block_body
468
- printf '%s\n' "$MARK_END"
469
- } > "$PROFILED"
470
- echo " PATH block: ~/.bashrc + $PROFILED"
471
- fi
472
- fi
473
- if [ "$(id -u)" = "0" ]; then
474
- if [ -e /usr/local/bin/claude ] && [ ! -L /usr/local/bin/claude ]; then
475
- echo " WARNING: /usr/local/bin/claude exists and is a real file — NOT overwriting." >&2
476
- else
477
- ln -sfn "$REPO_DIR/bin/claude" /usr/local/bin/claude
478
- echo " shim: /usr/local/bin/claude -> $REPO_DIR/bin/claude (systemd-PATH compatible)"
479
- fi
480
- if [ -e /usr/local/bin/codex ] && [ ! -L /usr/local/bin/codex ]; then
481
- echo " WARNING: /usr/local/bin/codex exists and is a real file — NOT overwriting." >&2
482
- else
483
- ln -sfn "$REPO_DIR/bin/codex" /usr/local/bin/codex
484
- echo " shim: /usr/local/bin/codex -> $REPO_DIR/bin/codex (systemd-PATH compatible)"
485
- fi
486
- fi
487
- [ "$NO_SCHEDULE" = "1" ] || linux_schedule_install
488
- fi
489
-
490
- echo
491
- echo "install complete. Open a new shell (or 'export PATH=\"$REPO_DIR/bin:\$PATH\"'), then:"
492
- echo " claude-accounts list # the Claude Code pool"
493
- echo " codex-accounts list # the Codex pool"
494
- echo " claude-accounts status | codex-accounts status # auth + limits detail"
495
- }
496
-
497
222
  if [ "$UNINSTALL" = "1" ]; then
498
223
  do_uninstall
499
224
  else
@@ -0,0 +1,20 @@
1
+ USAGE
2
+ claude-multiacc install [options]
3
+ ./install.sh [options]
4
+ claude-multiacc uninstall [--purge-data]
5
+ ./install.sh --uninstall [--purge-data]
6
+
7
+ OPTIONS
8
+ --server user@host set the sync target
9
+ --no-server use local-only pools
10
+ --instance NAME label this instance's scheduled jobs
11
+ --no-schedule install without scheduled jobs
12
+ --uninstall remove shell setup and jobs; keep account data
13
+ --purge-data also delete both account pools when uninstalling
14
+ -h, --help show this help without installing or uninstalling
15
+
16
+ Use CLAUDE_ACCOUNTS_ROOT and CODEX_ACCOUNTS_ROOT to select isolated pools.
17
+ After npm install -g claude-multiacc, both claude-accounts and codex-accounts
18
+ are available alongside claude-multiacc in npm's bin directory immediately.
19
+ Git/npx installs add the package's bin directory through shell startup files;
20
+ open a new shell or use the export PATH command printed by the installer.
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bash
2
+ # Installer actions, sourced by install.sh. Bash 3.2 compatible.
3
+
4
+ is_pool_root() { # true when $1 is safe to delete as an account pool
5
+ case "$1" in
6
+ ''|/|"$HOME") return 1 ;;
7
+ */.claude-accounts|*/.codex-accounts) return 0 ;;
8
+ /*) [ -f "$1/accounts.json" ] ;;
9
+ *) return 1 ;;
10
+ esac
11
+ }
12
+
13
+ strip_block() { # remove our marked block from a file (portable, no sed -i)
14
+ local f="$1"
15
+ [ -f "$f" ] || return 0
16
+ # Unmatched begin marker (end line hand-deleted): stripping would eat the rest
17
+ # of the rc file. Leave it alone and say so.
18
+ if grep -qF "$MARK_BEGIN" "$f" && ! grep -qF "$MARK_END" "$f"; then
19
+ echo " WARNING: $f has an unterminated claude-multiacc block — fix it by hand; not touching this file" >&2
20
+ return 1
21
+ fi
22
+ awk -v b="$MARK_BEGIN" -v e="$MARK_END" '
23
+ $0 == b { skip = 1; next }
24
+ $0 == e { skip = 0; next }
25
+ !skip { print }
26
+ ' "$f" > "$f.claude-multiacc.tmp" && mv "$f.claude-multiacc.tmp" "$f"
27
+ }
28
+
29
+ path_block_body() {
30
+ # Move (not just add) the shim dir to the front: later rc lines prepend
31
+ # ~/.local/bin, so a plain add-once guard would leave the real binary first.
32
+ printf 'PATH="$(printf %%s ":$PATH:" | sed '\''s|:%s/bin:|:|g; s|^:||; s|:$||'\'')"\n' "$REPO_DIR"
33
+ printf 'export PATH="%s/bin:$PATH"\n' "$REPO_DIR"
34
+ }
35
+
36
+ append_block() { # strip then append our PATH block to a file
37
+ local f="$1"
38
+ strip_block "$f" || return 1 # never create a second block next to a broken one
39
+ {
40
+ printf '%s\n' "$MARK_BEGIN"
41
+ path_block_body
42
+ printf '%s\n' "$MARK_END"
43
+ } >> "$f"
44
+ }
45
+
46
+ do_uninstall() {
47
+ echo "claude-multiacc: uninstalling (restoring stock behavior)"
48
+ # An instance install never wrote a shell rc block, a /etc/profile.d file or the
49
+ # /usr/local/bin shims — those belong to the DEFAULT install and are shared with it.
50
+ if [ -n "$INSTANCE" ]; then
51
+ echo " instance $INSTANCE: removing its agents only (PATH block and shims belong to the default install)"
52
+ else
53
+ strip_block "$HOME/.zshenv"
54
+ strip_block "$HOME/.zprofile"
55
+ strip_block "$HOME/.zshrc"
56
+ strip_block "$HOME/.bashrc"
57
+ strip_block "$HOME/.bash_profile"
58
+ strip_block "$HOME/.profile"
59
+ fi
60
+ if [ "$(machine_kind)" = "mac" ]; then
61
+ mac_schedule_remove
62
+ else
63
+ linux_schedule_remove
64
+ if [ -z "$INSTANCE" ]; then
65
+ [ -f "$PROFILED" ] && rm -f "$PROFILED"
66
+ if [ -L /usr/local/bin/claude ] \
67
+ && [ "$(canon_path /usr/local/bin/claude)" = "$(canon_path "$REPO_DIR/bin/claude")" ]; then
68
+ rm -f /usr/local/bin/claude
69
+ echo " removed /usr/local/bin/claude shim symlink"
70
+ fi
71
+ if [ -L /usr/local/bin/codex ] \
72
+ && [ "$(canon_path /usr/local/bin/codex)" = "$(canon_path "$REPO_DIR/bin/codex")" ]; then
73
+ rm -f /usr/local/bin/codex
74
+ echo " removed /usr/local/bin/codex shim symlink"
75
+ fi
76
+ fi
77
+ fi
78
+ local root
79
+ for root in "$ACC_ROOT" "$CODEX_ACC_ROOT"; do
80
+ if [ "$PURGE" = "1" ]; then
81
+ # Either the conventional location, or a directory that is provably a pool (it
82
+ # holds a manifest) — an instance root lives anywhere, so the manifest is what
83
+ # makes `rm -rf` safe. Anything else is left alone.
84
+ if is_pool_root "$root"; then
85
+ rm -rf "$root"; echo " purged $root"
86
+ else
87
+ echo " refusing to purge unusual accounts root: $root" >&2
88
+ fi
89
+ fi
90
+ done
91
+ if [ "$PURGE" != "1" ]; then
92
+ echo " account data kept at $ACC_ROOT and $CODEX_ACC_ROOT (use --purge-data to remove)"
93
+ fi
94
+ echo "uninstall complete — stock claude behavior restored"
95
+ }
96
+
97
+ install_binaries() {
98
+ [ -x "$REPO_DIR/bin/claude" ] || chmod +x "$REPO_DIR/bin/claude" 2>/dev/null || true
99
+ [ -x "$REPO_DIR/bin/claude-accounts" ] || chmod +x "$REPO_DIR/bin/claude-accounts" 2>/dev/null || true
100
+ [ -x "$REPO_DIR/bin/codex" ] || chmod +x "$REPO_DIR/bin/codex" 2>/dev/null || true
101
+ [ -x "$REPO_DIR/bin/codex-accounts" ] || chmod +x "$REPO_DIR/bin/codex-accounts" 2>/dev/null || true
102
+
103
+ local real
104
+ if real="$(find_real_claude "$REPO_DIR/bin/claude")"; then
105
+ echo " real claude binary: $real ($("$real" --version 2>/dev/null | head -1 || echo 'version unknown'))"
106
+ else
107
+ echo " WARNING: no real claude binary found — install Claude Code (https://claude.com/claude-code)" >&2
108
+ fi
109
+ local real_codex
110
+ if real_codex="$(find_real_codex "$REPO_DIR/bin/codex")"; then
111
+ echo " real codex binary: $real_codex"
112
+ "$real_codex" --version 2>/dev/null | head -1 || echo ' version unknown'
113
+ else
114
+ echo " note: no codex binary found — install Codex CLI with: npm i -g @openai/codex"
115
+ fi
116
+
117
+ # Keychain-mode note: Claude Code keeps per-config-dir logins in the login Keychain
118
+ # (one item per dir) whenever the session can open it, and only sessions without
119
+ # keychain access (ssh, launchd background jobs) fall back to .credentials.json. The
120
+ # pool reads both (lib/keychain.py); what an operator has to know is that a login
121
+ # made from a GUI session is invisible to their ssh sessions — mint a portable
122
+ # token for anything that must work from everywhere.
123
+ if [ "$kind" = "mac" ] && [ ! -f "$HOME/.claude/.credentials.json" ] \
124
+ && security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then
125
+ echo " note: this Mac keeps Claude Code logins in the Keychain. Per-dir logins made here"
126
+ echo " are read from it; ssh/background sessions cannot open it (they see 'locked') —"
127
+ echo " use 'claude-accounts mint <acct-NN>' for accounts that must work from everywhere."
128
+ fi
129
+ }
130
+
131
+ install_pools() {
132
+ manifest_init "${SERVER_OVERRIDE:-$DEFAULT_SERVER}"
133
+ if [ -n "$SERVER_OVERRIDE" ]; then
134
+ "$PYBIN" - "$MANIFEST" "$SERVER_OVERRIDE" <<'PYEOF'
135
+ import json, os, sys
136
+ doc = json.load(open(sys.argv[1]))
137
+ doc['server'] = sys.argv[2]
138
+ with open(sys.argv[1] + '.tmp', 'w') as f:
139
+ json.dump(doc, f, indent=2)
140
+ f.write('\n')
141
+ os.replace(sys.argv[1] + '.tmp', sys.argv[1])
142
+ PYEOF
143
+ fi
144
+ if sync_target_is_local "$(sync_target)"; then
145
+ echo " account pool: $ACC_ROOT (manifest ready; sync target: none — local-only)"
146
+ else
147
+ echo " account pool: $ACC_ROOT (manifest ready; sync target: $(sync_target))"
148
+ fi
149
+ # The codex pool gets its own skeleton + manifest (same schema, separate root).
150
+ if ! "$REPO_DIR/bin/codex-accounts" init-pool "${SERVER_OVERRIDE:-}" >/dev/null 2>&1; then
151
+ echo " WARNING: codex pool init failed (codex-accounts init-pool)" >&2
152
+ else
153
+ echo " codex account pool: $CODEX_ACC_ROOT (manifest ready)"
154
+ fi
155
+ }
156
+
157
+ install_mac_paths() {
158
+ # .zshenv covers non-interactive zsh; the .zshrc block must be LAST so it wins
159
+ # over ~/.local/bin re-prepends done earlier in .zshrc/.zprofile.
160
+ # zsh reads: .zshenv always; .zprofile for login; .zshrc for interactive.
161
+ # The block must end each file that later re-prepends ~/.local/bin.
162
+ if [ -z "$INSTANCE" ]; then
163
+ touch "$HOME/.zshenv"
164
+ append_block "$HOME/.zshenv"
165
+ touch "$HOME/.zprofile"
166
+ append_block "$HOME/.zprofile"
167
+ touch "$HOME/.zshrc"
168
+ append_block "$HOME/.zshrc"
169
+ [ -f "$HOME/.bash_profile" ] && append_block "$HOME/.bash_profile"
170
+ [ -f "$HOME/.bashrc" ] && append_block "$HOME/.bashrc"
171
+ echo " PATH block: end of ~/.zshenv, ~/.zprofile, ~/.zshrc (+ bash rc files if present)"
172
+ fi
173
+ }
174
+
175
+ install_linux_paths() {
176
+ if [ -z "$INSTANCE" ]; then
177
+ touch "$HOME/.bashrc"
178
+ append_block "$HOME/.bashrc"
179
+ if [ -w /etc/profile.d ] 2>/dev/null || [ "$(id -u)" = "0" ]; then
180
+ {
181
+ printf '%s\n' "$MARK_BEGIN"
182
+ path_block_body
183
+ printf '%s\n' "$MARK_END"
184
+ } > "$PROFILED"
185
+ echo " PATH block: ~/.bashrc + $PROFILED"
186
+ fi
187
+ fi
188
+ if [ -z "$INSTANCE" ] && [ "$(id -u)" = "0" ]; then
189
+ if [ -e /usr/local/bin/claude ] && [ ! -L /usr/local/bin/claude ]; then
190
+ echo " WARNING: /usr/local/bin/claude exists and is a real file — NOT overwriting." >&2
191
+ else
192
+ ln -sfn "$REPO_DIR/bin/claude" /usr/local/bin/claude
193
+ echo " shim: /usr/local/bin/claude -> $REPO_DIR/bin/claude (systemd-PATH compatible)"
194
+ fi
195
+ if [ -e /usr/local/bin/codex ] && [ ! -L /usr/local/bin/codex ]; then
196
+ echo " WARNING: /usr/local/bin/codex exists and is a real file — NOT overwriting." >&2
197
+ else
198
+ ln -sfn "$REPO_DIR/bin/codex" /usr/local/bin/codex
199
+ echo " shim: /usr/local/bin/codex -> $REPO_DIR/bin/codex (systemd-PATH compatible)"
200
+ fi
201
+ fi
202
+ }
203
+
204
+ do_install() {
205
+ local kind
206
+ kind="$(machine_kind)"
207
+ echo "claude-multiacc: installing (mode: $kind, repo: $REPO_DIR)"
208
+ if [ -n "$INSTANCE" ]; then
209
+ echo " instance: $INSTANCE (agents labelled $LABEL.*, pools $ACC_ROOT + $CODEX_ACC_ROOT)"
210
+ fi
211
+
212
+ install_binaries
213
+ install_pools
214
+
215
+ # An INSTANCE install never rewrites the shell rc blocks: one interactive PATH
216
+ # cannot serve two pools, and clobbering the default install's block would point
217
+ # the operator's shell at an instance pool. Its agents (below) carry the roots
218
+ # instead, and the runner daemon invokes the shims by absolute path.
219
+ if [ -n "$INSTANCE" ]; then
220
+ echo " PATH block: skipped (instance install) — for a shell against this pool:"
221
+ echo " export CLAUDE_ACCOUNTS_ROOT='$ACC_ROOT' CODEX_ACCOUNTS_ROOT='$CODEX_ACC_ROOT'"
222
+ echo " export PATH=\"$REPO_DIR/bin:\$PATH\""
223
+ fi
224
+ if [ "$kind" = "mac" ]; then
225
+ install_mac_paths
226
+ [ "$NO_SCHEDULE" = "1" ] || mac_schedule_install
227
+ else
228
+ install_linux_paths
229
+ [ "$NO_SCHEDULE" = "1" ] || linux_schedule_install
230
+ fi
231
+
232
+ echo
233
+ echo "install complete. Open a new shell (or 'export PATH=\"$REPO_DIR/bin:\$PATH\"'), then:"
234
+ echo " claude-accounts list # the Claude Code pool"
235
+ echo " codex-accounts list # the Codex pool"
236
+ echo " claude-accounts status # Claude auth + limits detail"
237
+ echo " codex-accounts status # Codex auth + limits detail"
238
+ }