claude-multiacc 1.0.1 → 1.0.2

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 CHANGED
@@ -163,32 +163,37 @@ acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side).
163
163
  ### Add a fresh account (~1 min, one browser sign-in)
164
164
 
165
165
  ```bash
166
- claude-accounts add # no email needed — it's read back from the sign-in
167
- # or name it explicitly (fast-fails on a duplicate before sign-in):
166
+ claude-accounts add # no email needed — read back from the sign-in
167
+ # or name it explicitly (fast-fails on a duplicate, and pre-fills the login page):
168
168
  claude-accounts add new@example.com
169
169
  ```
170
170
 
171
- It prints a **clickable sign-in link**. Open it in a browser signed into the account you
172
- want to add, approve, and paste the code back into the same prompt. The sign-in screen
173
- asks only for *"Contribute to your Claude subscription usage"* (`user:inference` scope)
174
- that's intentional: the token can run `claude` / `claude -p` and nothing else, and it
175
- draws on the subscription, never API credits.
171
+ This runs the **normal full Claude Code login** the exact same
172
+ `claude auth login` flow the app itself uses (`claude.com/cai/oauth/authorize`, full
173
+ Claude Code scopes). A browser opens (or a sign-in link is printed); sign in to the
174
+ account you're adding, approve, and if a code is shown, paste it back. It draws on the
175
+ subscription never API keys.
176
176
 
177
- You don't have to pass the email the command reads the authenticated address back from
178
- the account and registers under that. The account is registered **only after the sign-in
179
- is verified** an aborted or failed sign-in leaves nothing behind, and a duplicate email
180
- is refused. The captured token has a ~1-year lifetime, is inference-only, and does not
181
- rotate it works on the Mac **and** the server with no cross-machine refresh races, so
182
- the account is usable everywhere the moment `add` returns.
177
+ Using the full login on purpose: it does **not** hit the *"Sign in again to continue"*
178
+ step-up wall that the narrow long-lived-token grant triggers, and it stores an
179
+ auto-refreshing `.credentials.json`. That credential is **machine-local** (never synced),
180
+ which is what keeps two machines from invalidating each other's refresh token so an
181
+ account you `add` on the Mac runs on the Mac, and you `add` it on the server (over SSH) if
182
+ you want it there too. The account is registered **only after the signed-in email is read
183
+ back**; an aborted login leaves nothing behind, and duplicate emails are refused.
183
184
 
184
185
  ```bash
185
- claude-accounts login acct-NN # re-run the same flow for an existing account
186
- # (complete missing auth, or refresh an aging token)
187
- claude-accounts add x@y.com --tui # alternative: interactive /login instead, storing
188
- # auto-refreshing OAuth creds (that machine only)
186
+ claude-accounts login acct-NN # complete/refresh an existing account (full login)
187
+ claude-accounts add new@example.com --token # portable setup-token instead: works on BOTH
188
+ # machines and syncs to the server, but needs a
189
+ # recent sign-in (may show the step-up screen)
190
+ claude-accounts login acct-NN --token # same, for an existing account
189
191
  ```
190
192
 
191
- `status` warns when a token nears end of life; re-run `login` for that account.
193
+ Use `--token` when you specifically want one account usable on the server via the Mac's
194
+ sync (the ~1-year inference-only token that mirrors over). For everyday adds, the default
195
+ full login is smoother. `status` warns when a token nears end of life; re-run `login`.
196
+
192
197
  Both accounts currently in the pool are live and verified: acct-01 (support@gowalkae.com,
193
198
  Mac) and acct-02 (hasan@gowalkqa.com, server) — each adopted from its machine's existing
194
199
  login, so each runs on the machine that holds its credential.
@@ -255,6 +260,11 @@ repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
255
260
  - **A service bypasses the shim** — it spawns an absolute path. Point its env
256
261
  (`CLAUDE_BIN` etc.) at `/usr/local/bin/claude`. Docker-internal `claude` installs
257
262
  (e.g. openclaw live-test containers) are out of scope for a host shim.
263
+ - **`add` sign-in says "Sign in again to continue"** — that's Claude's step-up
264
+ authentication, not a tool error: granting a long-lived token needs a *recent* sign-in.
265
+ Open a fresh incognito window, sign in to the target account at claude.ai **first**,
266
+ then paste the link and approve. The approval scope is `user:inference`
267
+ (*"Contribute to your Claude subscription usage"*) — that's correct and minimal.
258
268
  - **Limits look stale** — launchd: `launchctl list | grep claude-multiacc`;
259
269
  cron: `crontab -l | grep claude-multiacc`. The shim also kicks a refresh when data
260
270
  is >3 min old.
@@ -20,13 +20,15 @@ claude-accounts — multi-account pool manager for claude-multiacc
20
20
  USAGE
21
21
  claude-accounts list brief account list
22
22
  claude-accounts status full health: auth, per-bucket limits, markers
23
- claude-accounts add [email] [--force|--tui]
24
- login-FIRST: shows a sign-in link, you paste the browser code back, and the
25
- account is registered only after auth is verified. The email is OPTIONAL — omit
26
- it and it's read back from whoever you sign in as. Duplicate emails refused.
27
- --tui uses interactive /login for full OAuth creds instead of a setup-token.
28
- claude-accounts login <acct-NN> [--force] complete/refresh auth for an existing
29
- account (same link+code flow)
23
+ claude-accounts add [email] [--token] [--force]
24
+ login-FIRST: runs the full Claude Code login (`claude auth login` the normal
25
+ browser sign-in, no long-lived-token step-up), then registers only after the
26
+ signed-in email is read back. Email is OPTIONAL (derived from the sign-in).
27
+ Stores auto-refreshing .credentials.json valid on THIS machine. Duplicates refused.
28
+ --token instead mints a portable setup-token (works on Mac AND server; needs a
29
+ recent sign-in — use it when you want the account usable on the server too).
30
+ claude-accounts login <acct-NN> [--token] [--force]
31
+ complete/refresh auth for an existing account (full login, or --token)
30
32
  claude-accounts import <email> [opts] register an account, optionally with credentials
31
33
  --id acct-NN explicit id (default: next free)
32
34
  --home mac|server which machine owns the OAuth grant (default: this one)
@@ -235,15 +237,16 @@ cmd_add() {
235
237
  # argument is OPTIONAL — omit it and it's derived from whoever you sign in as.
236
238
  # An aborted or failed sign-in leaves zero traces.
237
239
  require_manifest
238
- local email="" force=0 tui=0
240
+ local email="" force=0 token=0
239
241
  while [ $# -gt 0 ]; do
240
242
  case "$1" in
241
243
  --force) force=1; shift ;;
242
- --tui) tui=1; shift ;;
244
+ --token) token=1; shift ;; # portable setup-token instead of full login (server use)
245
+ --tui) shift ;; # accepted for compatibility; full login is now the default
243
246
  --*) die "unknown option: $1" ;;
244
247
  *)
245
248
  if [ -z "$email" ]; then email="$1"; shift
246
- else die "unexpected argument: $1 (usage: claude-accounts add [email] [--tui] [--force])"; fi ;;
249
+ else die "unexpected argument: $1 (usage: claude-accounts add [email] [--token] [--force])"; fi ;;
247
250
  esac
248
251
  done
249
252
  # If an email was named, fast-fail on a duplicate now. If not, we learn (and dedup)
@@ -276,35 +279,9 @@ cmd_add() {
276
279
  id="$(next_id)"
277
280
  d="$ACC_ROOT/$id"
278
281
  seed_account_dir "$d"
279
- if [ "$tui" = "1" ]; then
280
- # Full-OAuth variant: interactive /login in the new dir (auto-refreshing creds).
281
- cat <<EOF
282
- Preparing $id for $elabel — NOTHING is registered until login succeeds.
283
- An interactive claude session opens now with no account:
284
- 1. type /login (press c to copy the URL into an incognito window)
285
- 2. /status must show the right email
286
- 3. exit the session (/exit or ctrl+d) to finish registration
287
- EOF
288
- CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" || true
289
- if [ ! -f "$d/.credentials.json" ]; then
290
- rm -rf "${ACC_ROOT:?}/${id:?}"
291
- die "no login detected — nothing was created"
292
- fi
293
- got="$(CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth status 2>/dev/null | "$PYBIN" -c '
294
- import json, sys
295
- try:
296
- doc = json.loads(sys.stdin.read())
297
- print(doc.get("email", "") if doc.get("loggedIn") else "")
298
- except Exception:
299
- print("")')"
300
- if [ -z "$got" ]; then
301
- rm -rf "${ACC_ROOT:?}/${id:?}"
302
- die "login could not be verified — nothing was created"
303
- fi
304
- else
305
- # Default: sign-in link -> user pastes the code back here -> token captured.
306
- # The token works on BOTH machines (no refresh rotation, ~1y lifetime).
307
- echo "Preparing $id for $elabel — NOTHING is registered until sign-in completes."
282
+ if [ "$token" = "1" ]; then
283
+ # Portable setup-token variant (works on Mac AND server; needs a recent sign-in).
284
+ echo "Preparing $id for $elabel (portable token) — NOTHING is registered until sign-in completes."
308
285
  if ! run_token_ceremony "$d"; then
309
286
  rm -rf "${ACC_ROOT:?}/${id:?}"
310
287
  die "sign-in failed or aborted — nothing was created"
@@ -312,17 +289,27 @@ except Exception:
312
289
  ( umask 077; printf '%s' "$CEREMONY_TOKEN" > "$d/server.token" )
313
290
  chmod 600 "$d/server.token"
314
291
  got="$(token_email "$CEREMONY_TOKEN")"
315
- if [ -z "$got" ]; then
316
- # Signed in, but we could not read back WHICH account. Without a verified email
317
- # we cannot label or dedup the account, so refuse — unless an email was named
318
- # AND --force was given (then we trust the named email).
319
- if [ -n "$email" ] && [ "$force" = "1" ]; then
320
- warn "identity unverified registering as $email because --force was given"
321
- got="$email"
322
- else
323
- rm -rf "${ACC_ROOT:?}/${id:?}"
324
- die "signed in, but the account identity could not be read back — nothing was created (retry; or 'add <email> --force' to trust a named email)"
325
- fi
292
+ else
293
+ # Default: full Claude Code login (the flow that just works full scopes, no
294
+ # long-lived-token step-up). Writes auto-refreshing .credentials.json, valid on
295
+ # THIS machine (creds are never synced, so no cross-machine refresh races).
296
+ echo "Preparing $id for $elabel NOTHING is registered until login completes."
297
+ if ! run_login_ceremony "$d" "$email"; then
298
+ rm -rf "${ACC_ROOT:?}/${id:?}"
299
+ die "login failed or aborted — nothing was created"
300
+ fi
301
+ got="$(config_dir_email "$d")"
302
+ fi
303
+ if [ -z "$got" ]; then
304
+ # Signed in, but we could not read back WHICH account. Without a verified email we
305
+ # cannot label or dedup the account, so refuse — unless an email was named AND
306
+ # --force was given (then we trust the named email).
307
+ if [ -n "$email" ] && [ "$force" = "1" ]; then
308
+ warn "identity unverified — registering as $email because --force was given"
309
+ got="$email"
310
+ else
311
+ rm -rf "${ACC_ROOT:?}/${id:?}"
312
+ die "signed in, but the account identity could not be read back — nothing was created (retry; or 'add <email> --force' to trust a named email)"
326
313
  fi
327
314
  fi
328
315
  # `got` is now the authoritative signed-in email. Dedup against it, warn on mismatch.
@@ -491,8 +478,17 @@ run_token_ceremony() { # $1 = config dir
491
478
  umask 077 # the capture file briefly holds the raw token (removed inline below).
492
479
  # NB: no EXIT trap here — cmd_add owns the EXIT trap for its mutation lock, and a
493
480
  # second EXIT trap would clobber it and leak the lock.
494
- echo "A sign-in link will appear below — open it in a browser signed into the"
495
- echo "RIGHT account, approve, then paste the code back here."
481
+ cat <<'TIP'
482
+ A sign-in link will appear below. For a smooth grant (avoids the "Sign in again
483
+ to continue" step-up screen):
484
+ 1. Open a fresh incognito/private window.
485
+ 2. Sign in at claude.ai to the EXACT account you're adding — do this FIRST.
486
+ 3. THEN paste the link below into that same window and approve
487
+ ("Contribute to your Claude subscription usage" is the correct permission).
488
+ 4. Copy the code it shows and paste it back here.
489
+ If you do see "Sign in again to continue", that is Claude's security step, not an
490
+ error — just sign in to that account and approve; the code still appears.
491
+ TIP
496
492
  if [ -t 0 ]; then
497
493
  if [ "$(machine_kind)" = "mac" ]; then
498
494
  script -q "$cap" env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" setup-token
@@ -528,6 +524,56 @@ except Exception:
528
524
  print("")'
529
525
  }
530
526
 
527
+ config_dir_email() { # $1 = config dir; prints the logged-in email or empty
528
+ local real
529
+ real="$(find_real_claude "$_self")" || { echo ""; return 0; }
530
+ CLAUDE_CONFIG_DIR="$1" CLAUDE_SHIM_ACTIVE=1 "$real" auth status 2>/dev/null | "$PYBIN" -c '
531
+ import json, sys
532
+ try:
533
+ doc = json.loads(sys.stdin.read())
534
+ print(doc.get("email", "") if doc.get("loggedIn") else "")
535
+ except Exception:
536
+ print("")'
537
+ }
538
+
539
+ # Full subscription login: `claude auth login --claudeai`, the SAME flow the
540
+ # interactive app uses (claude.com/cai/oauth/authorize, full Claude Code scopes,
541
+ # normal sign-in — no long-lived-token step-up wall). Writes <dir>/.credentials.json
542
+ # (auto-refreshing). Returns success iff the credential landed. $2 = optional email hint.
543
+ run_login_ceremony() { # $1 = config dir, $2 = email hint
544
+ local d="$1" hint="${2:-}" real
545
+ real="$(find_real_claude "$_self")" || { warn "real claude binary not found"; return 1; }
546
+ cat <<'TIP'
547
+ A browser will open (or a sign-in link will be printed) for the FULL Claude Code
548
+ login. Sign in as the account you want to add and approve; if a code is shown,
549
+ paste it back here. This is the normal login — it draws on the subscription, no
550
+ API keys, and refreshes itself over time.
551
+ TIP
552
+ if [ -t 0 ]; then
553
+ if [ "$(machine_kind)" = "mac" ]; then
554
+ if [ -n "$hint" ]; then
555
+ script -q /dev/null env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai --email "$hint"
556
+ else
557
+ script -q /dev/null env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai
558
+ fi
559
+ else
560
+ if [ -n "$hint" ]; then
561
+ script -q -c "CLAUDE_CONFIG_DIR='$d' CLAUDE_SHIM_ACTIVE=1 '$real' auth login --claudeai --email '$hint'" /dev/null
562
+ else
563
+ script -q -c "CLAUDE_CONFIG_DIR='$d' CLAUDE_SHIM_ACTIVE=1 '$real' auth login --claudeai" /dev/null
564
+ fi
565
+ fi
566
+ else
567
+ # Headless (tests / automation): no PTY.
568
+ if [ -n "$hint" ]; then
569
+ CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai --email "$hint" || true
570
+ else
571
+ CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai || true
572
+ fi
573
+ fi
574
+ [ -f "$d/.credentials.json" ]
575
+ }
576
+
531
577
  cmd_mint() {
532
578
  require_manifest
533
579
  local id="${1:-}" paste="${2:-}"
@@ -556,11 +602,20 @@ cmd_mint() {
556
602
  }
557
603
 
558
604
  cmd_login() {
559
- # Complete (or refresh) auth for an EXISTING account: sign-in link -> paste
560
- # code -> token saved. Verifies the authenticated email matches the manifest.
605
+ # Complete (or refresh) auth for an EXISTING account. Default: full Claude Code login
606
+ # (auto-refreshing .credentials.json, this machine). --token: portable setup-token
607
+ # (Mac + server). Verifies the signed-in email matches the manifest.
561
608
  require_manifest
562
- local id="${1:-}" force="${2:-}"
563
- [ -n "$id" ] || die "usage: claude-accounts login <acct-NN> [--force]"
609
+ local id="" force=0 token=0
610
+ while [ $# -gt 0 ]; do
611
+ case "$1" in
612
+ --force) force=1; shift ;;
613
+ --token) token=1; shift ;;
614
+ --*) die "unknown option: $1" ;;
615
+ *) if [ -z "$id" ]; then id="$1"; shift; else die "unexpected argument: $1"; fi ;;
616
+ esac
617
+ done
618
+ [ -n "$id" ] || die "usage: claude-accounts login <acct-NN> [--token] [--force]"
564
619
  valid_acct_id "$id" || die "not a valid account id: $id"
565
620
  account_ids | grep -qx "$id" || die "unknown account: $id"
566
621
  local d="$ACC_ROOT/$id" email got
@@ -574,17 +629,24 @@ for a in json.load(open(sys.argv[1])).get('accounts', []):
574
629
  PYEOF
575
630
  )"
576
631
  echo "Sign in as $email for $id."
577
- run_token_ceremony "$d" || die "sign-in failed or aborted — nothing changed"
578
- got="$(token_email "$CEREMONY_TOKEN")"
579
- if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "--force" ]; then
580
- die "you signed in as $got but $id is $email nothing saved (use --force to override)"
632
+ if [ "$token" = "1" ]; then
633
+ run_token_ceremony "$d" || die "sign-in failed or aborted — nothing changed"
634
+ got="$(token_email "$CEREMONY_TOKEN")"
635
+ if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "1" ]; then
636
+ die "you signed in as $got but $id is $email — nothing saved (use --force to override)"
637
+ fi
638
+ ( umask 077; printf '%s' "$CEREMONY_TOKEN" > "$d/server.token" )
639
+ chmod 600 "$d/server.token"
640
+ echo "$id token saved (portable — works on Mac and server)."
641
+ else
642
+ run_login_ceremony "$d" "$email" || die "login failed or aborted — nothing changed"
643
+ got="$(config_dir_email "$d")"
644
+ if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "1" ]; then
645
+ die "you signed in as $got but $id is $email — nothing saved (use --force to override)"
646
+ fi
647
+ echo "$id login saved (.credentials.json, this machine, auto-refreshing)."
581
648
  fi
582
- printf '%s' "$CEREMONY_TOKEN" > "$d/server.token"
583
- chmod 600 "$d/server.token"
584
- log_to ops.log "login $id verified=${got:-unverified}"
585
- echo "$id auth saved (token, works on Mac and server)."
586
- echo "Optional, for auto-refreshing OAuth creds on this machine instead:"
587
- echo " CLAUDE_ACCOUNT=$id claude # then /login"
649
+ log_to ops.log "login $id verified=${got:-unverified} mode=$([ "$token" = 1 ] && echo token || echo login)"
588
650
  auto_sync
589
651
  }
590
652
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Multi-account addon for Claude Code: every claude / claude -p 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": {
@@ -33,6 +33,13 @@ if [ "${1:-}" = "auth" ] && [ "${2:-}" = "status" ]; then
33
33
  printf '{"loggedIn": true, "email": "%s"}\n' "${FAKE_EMAIL:-fake@test}"
34
34
  exit 0
35
35
  fi
36
+ if [ "${1:-}" = "auth" ] && [ "${2:-}" = "login" ]; then
37
+ [ -n "${FAKE_LOGIN_FAIL:-}" ] && { echo "login aborted" >&2; exit 1; }
38
+ # simulate a completed full-scope login: write auto-refreshing creds to the config dir
39
+ printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-login","refreshToken":"r","expiresAt":9999999999999,"refreshTokenExpiresAt":9999999999999,"scopes":["user:inference"],"subscriptionType":"max"}}' > "${CLAUDE_CONFIG_DIR:-/dev/null}/.credentials.json"
40
+ echo "Logged in."
41
+ exit 0
42
+ fi
36
43
  if [ "${1:-}" = "setup-token" ]; then
37
44
  echo "Open this sign-in link: https://claude.ai/oauth/authorize?fake=1"
38
45
  [ -n "${FAKE_TOKEN_FAIL:-}" ] && { echo "sign-in aborted" >&2; exit 1; }
@@ -358,13 +365,21 @@ check "import refuses duplicate email" "already registered as acct-01" "$out"
358
365
  out="$(claude-accounts import a@test --id acct-01 --no-sync 2>&1)"
359
366
  check "import same-id update allowed" "Imported acct-01" "$out"
360
367
 
361
- # ---- 15c. login-first add (default link+code flow): registers after verified auth --
368
+ # ---- 15c. login-first add (DEFAULT full-login flow): registers after verified auth --
362
369
  out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=new@test claude-accounts add new@test 2>&1)"
363
- check "add shows sign-in link" "sign-in link" "$out"
364
- check "add registers after verified sign-in" "Registered acct-04 for new@test" "$out"
365
- [ -s "$ACC/acct-04/server.token" ] && t_ok "token landed in the new dir" || t_fail "add token" "missing"
370
+ check "add registers after verified login" "Registered acct-04 for new@test" "$out"
371
+ [ -f "$ACC/acct-04/.credentials.json" ] && t_ok "default add writes .credentials.json (full login)" || t_fail "add creds" "missing"
372
+ [ ! -f "$ACC/acct-04/server.token" ] && t_ok "default add does NOT mint a setup-token" || t_fail "add token" "unexpected server.token"
373
+ out="$(CLAUDE_ACCOUNT=acct-04 claude 2>&1)"
374
+ check "new account usable via pin (oauth creds)" "CFG=acct-04" "$out"
375
+ claude-accounts remove acct-04 --yes >/dev/null 2>&1
376
+
377
+ # ---- 15c1. add --token: portable setup-token instead of creds ------------------------
378
+ out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=tok@test claude-accounts add tok@test --token 2>&1)"
379
+ check "add --token registers via portable token" "Registered acct-04 for tok@test" "$out"
380
+ [ -s "$ACC/acct-04/server.token" ] && t_ok "add --token writes server.token" || t_fail "add --token" "missing"
366
381
  out="$(CLAUDE_ACCOUNT=acct-04 claude 2>&1)"
367
- check "new account usable via pin (token auth)" "TOK=sk-ant-oat01-FAKE" "$out"
382
+ check "token account exports CLAUDE_CODE_OAUTH_TOKEN" "TOK=sk-ant-oat01-FAKE" "$out"
368
383
  claude-accounts remove acct-04 --yes >/dev/null 2>&1
369
384
 
370
385
  # ---- 15c0. add with NO email: derives it from the verified sign-in -------------------
@@ -385,19 +400,24 @@ rc=$?
385
400
  check "email-less add needs a readable identity" "identity could not be read back" "$out"
386
401
  [ ! -d "$ACC/acct-04" ] && t_ok "unverifiable email-less add cleaned up" || t_fail "unverifiable cleanup" "dir left"
387
402
 
388
- # ---- 15c2. --tui variant registers via /login creds --------------------------------
389
- out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_DO_LOGIN=1 FAKE_EMAIL=tui@test claude-accounts add tui@test --tui 2>&1)"
390
- check "add --tui registers after verified login" "Registered acct-04 for tui@test" "$out"
391
- [ -f "$ACC/acct-04/.credentials.json" ] && t_ok "tui login creds landed in the new dir" || t_fail "tui creds" "missing"
403
+ # ---- 15c2. --tui is accepted as a no-op alias (full login is the default now) --------
404
+ out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=tui@test claude-accounts add tui@test --tui 2>&1)"
405
+ check "add --tui still works (alias for default login)" "Registered acct-04 for tui@test" "$out"
406
+ [ -f "$ACC/acct-04/.credentials.json" ] && t_ok "--tui writes creds like the default" || t_fail "tui creds" "missing"
392
407
  claude-accounts remove acct-04 --yes >/dev/null 2>&1
393
408
 
394
- # ---- 15d. aborted sign-in leaves zero traces ----------------------------------------
395
- out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_TOKEN_FAIL=1 claude-accounts add ghost@test 2>&1)"
409
+ # ---- 15d. aborted login leaves zero traces ------------------------------------------
410
+ out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_LOGIN_FAIL=1 claude-accounts add ghost@test 2>&1)"
396
411
  rc=$?
397
- check "aborted sign-in detected" "sign-in failed or aborted" "$out"
412
+ check "aborted login detected" "login failed or aborted" "$out"
398
413
  [ "$rc" != "0" ] && t_ok "aborted add exits nonzero" || t_fail "aborted add rc" "rc=0"
399
414
  [ ! -d "$ACC/acct-04" ] && t_ok "aborted add cleaned up its dir" || t_fail "aborted add cleanup" "dir left behind"
400
415
  claude-accounts list 2>&1 | grep -q ghost@test && t_fail "aborted add not in manifest" "ghost@test registered" || t_ok "aborted add not in manifest"
416
+ # aborted --token path too
417
+ out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_TOKEN_FAIL=1 claude-accounts add ghost2@test --token 2>&1)"
418
+ rc=$?
419
+ check "aborted --token add detected" "sign-in failed or aborted" "$out"
420
+ [ ! -d "$ACC/acct-04" ] && t_ok "aborted --token add cleaned up" || t_fail "aborted token cleanup" "dir left"
401
421
 
402
422
  # ---- 15e. sign-in as an already-registered email is rejected + cleaned ---------------
403
423
  out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=a@test claude-accounts add brand@test 2>&1)"
@@ -408,12 +428,16 @@ check "wrong-account sign-in rejected" "already registered as acct-01" "$out"
408
428
  # ---- 15f. login command completes auth for an existing auth-less account -------------
409
429
  claude-accounts import pending@test --id acct-08 --no-sync >/dev/null 2>&1
410
430
  out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=pending@test claude-accounts login acct-08 2>&1)"
411
- check "login completes existing account" "acct-08 auth saved" "$out"
412
- [ -s "$ACC/acct-08/server.token" ] && t_ok "login saved token" || t_fail "login token" "missing"
431
+ check "login completes existing account (full login)" "acct-08 login saved" "$out"
432
+ [ -f "$ACC/acct-08/.credentials.json" ] && t_ok "login writes .credentials.json" || t_fail "login creds" "missing"
413
433
  out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=other@test claude-accounts login acct-08 2>&1)"
414
434
  rc=$?
415
435
  check "login email mismatch refused" "nothing saved" "$out"
416
436
  [ "$rc" != "0" ] && t_ok "mismatched login exits nonzero" || t_fail "mismatched login rc" "rc=0"
437
+ # login --token writes a portable token for the same account
438
+ out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=pending@test claude-accounts login acct-08 --token 2>&1)"
439
+ check "login --token saves a portable token" "acct-08 token saved" "$out"
440
+ [ -s "$ACC/acct-08/server.token" ] && t_ok "login --token writes server.token" || t_fail "login token" "missing"
417
441
  claude-accounts remove acct-08 --yes >/dev/null 2>&1
418
442
 
419
443
  # ---- 16. CLI: limits marking via fixture endpoint ------------------------------