claude-multiacc 1.0.11 → 1.0.12

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.
@@ -421,8 +421,21 @@ cmd_add() {
421
421
  mutate_lock || die "could not acquire the account lock — try again"
422
422
  owner="$(email_owner "$got")"
423
423
  if [ -n "$owner" ]; then
424
- mutate_unlock
425
- echo "$got is already added as $owner skipping (nothing added)."
424
+ # The sign-in itself succeeded and produced a fresh credential for an account
425
+ # that is already registered don't throw it away: save it to that account,
426
+ # exactly as `login $owner` would have. This machine now runs $owner.
427
+ local od="$ACC_ROOT/$owner"
428
+ seed_account_dir "$od"
429
+ if mv -f "$d/auth.json" "$od/auth.json" 2>/dev/null; then
430
+ chmod 600 "$od/auth.json" 2>/dev/null || true
431
+ clear_auth_markers "$od"
432
+ mutate_unlock
433
+ log_to ops.log "add redirect: signed in as $got (already $owner) — credential saved to $owner"
434
+ echo "$got is already added as $owner — the fresh sign-in was saved to $owner (usable on this machine now)."
435
+ else
436
+ mutate_unlock
437
+ echo "$got is already added as $owner — skipping (nothing added)."
438
+ fi
426
439
  return 0 # RESERVED_DIR still set -> trap removes the temp dir
427
440
  fi
428
441
  if [ -n "$email" ] && [ "$got" != "$email" ]; then
@@ -692,6 +705,26 @@ PYEOF
692
705
  got_lc="$(printf '%s' "$got" | tr '[:upper:]' '[:lower:]')"
693
706
  email_lc="$(printf '%s' "$email" | tr '[:upper:]' '[:lower:]')"
694
707
  if [ -n "$got" ] && [ "$got_lc" != "$email_lc" ] && [ "$force" != "1" ]; then
708
+ # Signed in as a DIFFERENT account. If that account is registered in this pool,
709
+ # the sign-in was not a mistake worth discarding — route the fresh credential to
710
+ # ITS account dir (what `login <owner>` would have produced), restore this
711
+ # account's prior state, and report honestly that THIS account is still unfixed.
712
+ local owner
713
+ owner="$(email_owner "$got")"
714
+ if [ -n "$owner" ] && [ "$owner" != "$id" ]; then
715
+ local od="$ACC_ROOT/$owner"
716
+ seed_account_dir "$od"
717
+ if mv -f "$d/auth.json" "$od/auth.json" 2>/dev/null; then
718
+ chmod 600 "$od/auth.json" 2>/dev/null || true
719
+ clear_auth_markers "$od"
720
+ restore_snap
721
+ trap - INT TERM
722
+ log_to ops.log "login redirect: signed in as $got -> credential saved to $owner (target was $id)"
723
+ echo "you signed in as $got — that is $owner, so the credential was saved to $owner (usable now)."
724
+ echo "$id ($email) still needs its own sign-in: codex-accounts login $id"
725
+ return 1
726
+ fi
727
+ fi
695
728
  restore_snap
696
729
  trap - INT TERM
697
730
  die "you signed in as $got but $id is $email — nothing saved, previous credential restored (use --force to override)"
@@ -819,12 +852,27 @@ cmd_relogin() {
819
852
  read -r ans
820
853
  case "$ans" in y|Y|yes) ;; *) echo "aborted"; return 1 ;; esac
821
854
  fi
855
+ # Accounts that were BROKEN when this run started: if one of them turns healthy
856
+ # mid-run (a redirected sign-in from an earlier iteration landed its credential),
857
+ # asking the user to sign it in again would be pure friction — it gets skipped.
858
+ # Accounts that were healthy at start (explicit ids / --all) are never skipped:
859
+ # the user asked to re-authenticate them on purpose.
860
+ local pre_bad
861
+ pre_bad=" $(accounts_needing_login 2>/dev/null | tr '\n' ' ') "
822
862
  # One sync at the end instead of one per account: each auto_sync is an ssh round trip.
823
863
  local prev_no_sync="${CODEX_MULTIACC_NO_SYNC:-0}" failed="" done_ok=0
824
864
  export CODEX_MULTIACC_NO_SYNC=1
825
865
  for id in $ids; do
826
866
  echo
827
867
  echo "=== $id ==============================================================="
868
+ case "$pre_bad" in
869
+ *" $id "*)
870
+ if [ "$(account_audit 2>/dev/null | awk -F'\t' -v i="$id" '$1 == i { print $4 }')" = "ok" ]; then
871
+ echo "$id already has a working login (a credential landed during an earlier sign-in) — skipping."
872
+ done_ok=$((done_ok + 1))
873
+ continue
874
+ fi ;;
875
+ esac
828
876
  if [ "$browser" = "1" ]; then
829
877
  ( cmd_login "$id" --browser ) && done_ok=$((done_ok + 1)) || failed="$failed $id"
830
878
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1757,6 +1757,62 @@ EOF
1757
1757
  )"
1758
1758
  [ "$got_email" = "d@cx" ] && t_ok "codex: refused login restores the prior credential" \
1759
1759
  || t_fail "codex login restore" "auth.json now belongs to: $got_email"
1760
+ # signing in as a DIFFERENT-but-REGISTERED account routes the credential to that
1761
+ # account instead of discarding it (and the target account stays honestly unfixed)
1762
+ rm -f "$CX/acct-01/auth.json"
1763
+ out="$(FAKE_EMAIL=a@cx codex-accounts login acct-04 2>&1)"
1764
+ rc=$?
1765
+ check "codex: cross-account sign-in is redirected, not discarded" "credential was saved to acct-01" "$out"
1766
+ check "codex: redirect says the target still needs its sign-in" "acct-04 (d@cx) still needs its own sign-in" "$out"
1767
+ [ "$rc" != "0" ] && t_ok "codex: redirected login still exits nonzero for the target" \
1768
+ || t_fail "codex redirect rc" "rc=0 for an account that was not fixed"
1769
+ [ -s "$CX/acct-01/auth.json" ] && t_ok "codex: redirected credential landed at its owner" \
1770
+ || t_fail "codex redirect landing" "acct-01/auth.json missing"
1771
+ got_email="$(python3 - "$CX/acct-04/auth.json" <<'EOF'
1772
+ import base64, json, sys
1773
+ t = json.load(open(sys.argv[1]))['tokens']['id_token']
1774
+ p = t.split('.')[1]; p += '=' * (-len(p) % 4)
1775
+ print(json.loads(base64.urlsafe_b64decode(p)).get('email', ''))
1776
+ EOF
1777
+ )"
1778
+ [ "$got_email" = "d@cx" ] && t_ok "codex: redirect restored the target's prior credential" \
1779
+ || t_fail "codex redirect restore" "acct-04 auth.json belongs to: $got_email"
1780
+ # `add` while signed into an already-registered account also keeps the credential
1781
+ rm -f "$CX/acct-01/auth.json"
1782
+ before_dirs="$(ls "$CX" | sort)"
1783
+ out="$(FAKE_EMAIL=a@cx codex-accounts add 2>&1)"
1784
+ rc=$?
1785
+ check "codex: add of a registered account saves the fresh sign-in to it" "fresh sign-in was saved to acct-01" "$out"
1786
+ [ "$rc" = "0" ] && t_ok "codex: add redirect exits 0 (nothing new to register)" || t_fail "codex add redirect rc" "rc=$rc"
1787
+ [ -s "$CX/acct-01/auth.json" ] && t_ok "codex: add redirect landed the credential" \
1788
+ || t_fail "codex add redirect" "acct-01/auth.json missing"
1789
+ [ "$before_dirs" = "$(ls "$CX" | sort)" ] && t_ok "codex: add redirect leaves no reserved dir behind" \
1790
+ || t_fail "codex add redirect cleanup" "pool dirs changed"
1791
+ n="$(grep -c '"email": "a@cx"' "$CX/accounts.json")"
1792
+ [ "$n" = "1" ] && t_ok "codex: add redirect creates no duplicate entry" || t_fail "codex add redirect dup" "count=$n"
1793
+ # relogin end-to-end: a redirect mid-run fixes the OTHER account, whose own turn is
1794
+ # then skipped instead of demanding a second sign-in. Both accounts' home must be
1795
+ # THIS machine's kind, or a credless account reads as 'remote' (grant lives
1796
+ # elsewhere) on the other platform and never enters the worklist.
1797
+ cx_mk=mac; [ "$(uname -s)" = "Darwin" ] || cx_mk=linux
1798
+ python3 - "$CX/accounts.json" "$cx_mk" <<'EOF'
1799
+ import json, os, sys
1800
+ d = json.load(open(sys.argv[1]))
1801
+ for a in d['accounts']:
1802
+ if a['id'] in ('acct-01', 'acct-04'):
1803
+ a['home'] = sys.argv[2]
1804
+ json.dump(d, open(sys.argv[1] + '.tmp', 'w'), indent=2); os.replace(sys.argv[1] + '.tmp', sys.argv[1])
1805
+ EOF
1806
+ rm -f "$CX/acct-01/auth.json" "$CX/acct-04/auth.json"
1807
+ out="$(FAKE_EMAIL=d@cx codex-accounts relogin --yes 2>&1)"
1808
+ rc=$?
1809
+ check "codex: relogin redirect saves the mis-ordered sign-in" "credential was saved to acct-04" "$out"
1810
+ check "codex: relogin skips an account fixed mid-run" "already has a working login" "$out"
1811
+ check "codex: relogin counts the redirect-fixed account" "re-authenticated 1 of 2" "$out"
1812
+ check "codex: relogin still reports the unfixed account" "still failing: acct-01" "$out"
1813
+ [ "$rc" != "0" ] && t_ok "codex: relogin with an unfixed account exits nonzero" || t_fail "codex relogin redirect rc" "rc=0"
1814
+ mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
1815
+ mk_cx_auth "$CX/acct-04/auth.json" d@cx "$FUTURE_EXP"
1760
1816
  # the sign-in ceremony is DEVICE-CODE by default (the localhost browser callback
1761
1817
  # cannot reach a remote/SSH machine); --browser opts into the callback flow
1762
1818
  export FAKE_LOGIN_ARGS="$WORK/cx-login-args"