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/README.md +11 -138
- package/bin/claude-accounts +2 -3
- package/bin/claude-multiacc +3 -0
- package/bin/cli.mjs +16 -18
- package/bin/codex-accounts +2 -3
- package/bin/multiacc-select +4 -0
- package/docs/ACCOUNT_OPERATIONS.md +3 -61
- package/docs/CODEX.md +83 -0
- package/docs/INSTALLATION.md +91 -0
- package/docs/UNIFIED_SELECTOR.md +2 -0
- package/docs/VERIFICATION.md +68 -0
- package/install.sh +9 -284
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/install-help.txt +20 -0
- package/lib/install_actions.sh +238 -0
- package/lib/install_calendar.sh +59 -0
- package/package.json +5 -2
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/packaged_command_support.py +81 -0
- package/tests/test_packaged_commands.py +170 -0
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
|
-
<!--
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|