Skip to content

feat(auth): multi-account registry store + migration (slice 3, PR 1/3)#115

Merged
stuffbucket merged 1 commit into
mainfrom
feat/account-registry-store
Jun 11, 2026
Merged

feat(auth): multi-account registry store + migration (slice 3, PR 1/3)#115
stuffbucket merged 1 commit into
mainfrom
feat/account-registry-store

Conversation

@stuffbucket

Copy link
Copy Markdown
Owner

What

PR 1 of 3 for slice 3 — multi-account persistence + quick-switch. This lays the persistence foundation with no user-visible behaviour change: a single account behaves exactly as today.

Store (github-token-store.ts)

  • AccountRegistry { schemaVersion: 2, activeKey, accounts: Record<"login@host", AccountRecord> }; AccountRecord { login, host, token, tokenType, addedVia, obtainedAt }. Identity key = login@host (gh's host format, so a maximal device-code account and the same gh-imported account collapse to one entry).
  • Pure ops: addAndActivate / setActive / removeAccount / getActiveRecord / listAccounts. Atomic temp+rename writes (a torn write would lose every account). RMW shorthands addAccountToDefaultRegistry / removeActiveFromDefaultRegistry.
  • migrateLegacyRecord: one-time lift of the legacy single-record file → registry, keyed login@host (resolveLogin injected; offline → unknown@host so the token is never lost). Gated on empty-registry; leaves the legacy file as a rollback fallback.
  • readDefaultRecord rewritten as a registry-aware back-compat wrapper (active record → legacy-file fallback), so boot / CLI reuse-check / setup-status / debug keep working unchanged.

Wiring

  • The three sign-in producers (device-code, CLI, gh-reuse) now persist a typed AccountRecord (addedVia device-code/gh-cli). Host comes from a new dependency-free github-host leaf — deliberately kept out of api-config so the token-store writers don't close an import cycle (api-config → state → get-models → error → auth-controller). deps:check cycle count stays at the baseline (23).
  • signOut drops the active account from the registry (behaviour-neutral for one account) + still removes the legacy file.
  • Boot runs the migration before reading the active account.
  • uninstall --purge removes the registry too; debug + setup-status read the active record.

Scope discipline

The contentious sign-out-keeps-copy lifecycle decision is deliberately not made here — it's only user-visible with the quick-switch UI (PR 3), so PR 1 stays behaviour-neutral and independently releasable. PR 2 = /accounts routes (list/switch/remove); PR 3 = the picker UI.

Testing

  • bun test — 840 pass / 0 fail (+15 registry + migration tests: pure ops, persistence round-trip, atomic 0600, corrupt→empty, migration login-keyed / offline-fallback / gated-noop / obtainedAt-preserved).
  • bun run check:fast clean; deps:check 0 errors (cycle-neutral); knip no new unused exports; shell tsc clean.
  • /simplify (2× parallel reviewers) — converged on "design sound, ship"; the one actionable item (a brittle path-replace in setup-status) was extracted into registryPathFor().

Introduces the schema-v2 account registry that backs quick-switch, with no
user-visible behaviour change yet — a single account behaves exactly as
before.

Store (github-token-store.ts):
- AccountRegistry { schemaVersion, activeKey, accounts: Record<login@host> }
  + AccountRecord { login, host, token, tokenType, addedVia, obtainedAt }.
- Pure ops: addAndActivate / setActive / removeAccount / getActiveRecord /
  listAccounts. Atomic temp+rename writes (a torn write would lose every
  account). RMW shorthands addAccountToDefaultRegistry /
  removeActiveFromDefaultRegistry.
- migrateLegacyRecord: one-time lift of the legacy single-record file into
  the registry, keyed login@host (resolveLogin injected; offline →
  unknown@host so the token is never lost). Gated + leaves the legacy file
  as a rollback fallback.
- readDefaultRecord rewritten as a registry-aware back-compat wrapper
  (active record → legacy-file fallback), so boot / CLI reuse-check /
  setup-status / debug keep working unchanged.

Wiring:
- The three sign-in producers (device-code, CLI, gh-reuse) now persist a
  typed AccountRecord (addedVia device-code/gh-cli) instead of the bare
  single record. Each resolves login best-effort; host = gh format via a new
  dependency-free github-host leaf (kept out of api-config to avoid an
  import cycle).
- signOut drops the active account from the registry (behaviour-neutral for
  one account) + still removes the legacy file.
- Boot runs the migration before reading the active account.
- uninstall --purge removes the registry too; debug + setup-status read the
  active record.

The contentious sign-out-keeps-copy lifecycle decision is deliberately NOT
made here — it is only user-visible with the quick-switch UI (PR 3). PR 1 is
behaviour-neutral. +15 store/migration tests; auth-controller DI seam gains
addAccount/removeActiveAccount.
): Promise<void> {
const json = `${JSON.stringify(value, null, 2)}\n`
const tmp = `${filePath}.tmp.${process.pid}`
await fs.writeFile(tmp, json, { mode: 0o600 })
@stuffbucket stuffbucket merged commit 2862b3c into main Jun 11, 2026
4 checks passed
@stuffbucket stuffbucket deleted the feat/account-registry-store branch June 11, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants