SIGN IN SIGN UP

fix(accounts): heal corrupted CC rows and collapse duplicates on load

Anthropic accounts auto-imported from Claude Code credentials could end
up with `source="oauth"` and `identity={kind:"legacy",...}` after a
partial write from an older plugin version, even though the row's id
still proved its CC origin (`cc-cc-keychain-*` / `cc-cc-file-*`). Once
the kind diverged from `cc`, identity-based dedup stopped matching the
row against the live CC credential, and CC token rotation broke the
refresh-token fallback too — so every load created a fresh duplicate
row for the same upstream account.

Fix is a load-time repair pass plus targeted downgrade guards:

- src/accounts/repair.ts: new module. `repairCorruptedCCAccounts` heals
  rows whose id encodes a CC source by re-attaching the live CC
  credential's metadata, falling back to a partial source-restore when
  no credential matches, then collapses any rows that resolve to the
  same `{kind:"cc",...}` identity. Stats are NOT summed during merge —
  both rows represent the same upstream account. The storage-level
  `repairStoredCCAccounts` + `loadAccountsWithRepair` variant returns
  the dropped row ids so the next save can opt the disk-only union out
  of restoring them.

- src/accounts.ts: AccountManager.load runs the repair pass before the
  CC auto-import loop so auto-import sees a deduped list. addAccount's
  existing-row branch refuses to reshape source/identity/label/email of
  any row whose source is `cc-*` or whose id is CC-born when the caller
  passes no explicit CC options. saveToDisk threads `#pendingDroppedIds`
  through prepareStorageForSave and saveAccounts so the union honors
  the intentional removal.

- src/accounts/persistence.ts, src/storage.ts: prepareStorageForSave
  and unionAccountsWithDisk now accept an optional `droppedIds` filter.
  Disk rows whose id is in the set are not restored as "disk-only"
  accounts. Without this filter, the union safety net was re-adding
  every collapsed row on the next save, defeating the heal in a loop.

- src/accounts/matching.ts: updateManagedAccountFromStorage prefers an
  existing CC source when the disk row's source is not CC, protecting
  a healthy in-memory CC row from being downgraded by a malformed disk
  row during syncActiveIndexFromDisk.

- src/index.ts: OAuth re-auth callback skips any candidate whose id is
  CC-born so a CC-sourced row can never be reshaped to source="oauth".

- src/commands/oauth-flow.ts, src/cli/commands/auth.ts: direct-storage
  writers (slash-command login/reauth, CLI cmdLogin/cmdReauth) skip
  source/identity/email mutation when the matched row is CC-sourced and
  always set explicit source+identity on newly-created rows. cmdList
  and cmdStatus use loadAccountsWithRepair so the standalone CLI heals
  on every load.

- src/accounts/repair.test.ts: 6 regression tests covering full heal
  with a matching live credential, partial heal via single-credential
  fallback, duplicate collapse after repair, and the no-downgrade
  guards in addAccount with and without explicit cc options.

- src/accounts.test.ts, index.test.ts: existing saveAccounts call
  assertions extended with a second `expect.anything()` matcher to
  accommodate the new `{droppedIds}` options argument.

Verified: full suite 1097/1097 passing, lsp diagnostics clean on all
changed files.
V
Vacbo committed
0e7fd6e1d24ab210443fd472ec24adbee1383c53
Parent: deed641