codex-multi-auth 0.1.5 → 0.1.8

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.
Files changed (75) hide show
  1. package/LICENSE +21 -1
  2. package/README.md +19 -6
  3. package/assets/readme-hero.svg +31 -31
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/codex-cli/state.js +4 -4
  9. package/dist/lib/codex-cli/state.js.map +1 -1
  10. package/dist/lib/codex-cli/sync.d.ts +9 -16
  11. package/dist/lib/codex-cli/sync.d.ts.map +1 -1
  12. package/dist/lib/codex-cli/sync.js +51 -284
  13. package/dist/lib/codex-cli/sync.js.map +1 -1
  14. package/dist/lib/codex-cli/writer.d.ts.map +1 -1
  15. package/dist/lib/codex-cli/writer.js +62 -5
  16. package/dist/lib/codex-cli/writer.js.map +1 -1
  17. package/dist/lib/codex-manager/settings-hub.d.ts +44 -2
  18. package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
  19. package/dist/lib/codex-manager/settings-hub.js +746 -117
  20. package/dist/lib/codex-manager/settings-hub.js.map +1 -1
  21. package/dist/lib/codex-manager.d.ts.map +1 -1
  22. package/dist/lib/codex-manager.js +3 -8
  23. package/dist/lib/codex-manager.js.map +1 -1
  24. package/dist/lib/context-overflow.js +10 -10
  25. package/dist/lib/named-backup-export.d.ts +69 -0
  26. package/dist/lib/named-backup-export.d.ts.map +1 -0
  27. package/dist/lib/named-backup-export.js +207 -0
  28. package/dist/lib/named-backup-export.js.map +1 -0
  29. package/dist/lib/oauth-success.html +66 -210
  30. package/dist/lib/oc-chatgpt-import-adapter.d.ts +64 -0
  31. package/dist/lib/oc-chatgpt-import-adapter.d.ts.map +1 -0
  32. package/dist/lib/oc-chatgpt-import-adapter.js +506 -0
  33. package/dist/lib/oc-chatgpt-import-adapter.js.map +1 -0
  34. package/dist/lib/oc-chatgpt-orchestrator.d.ts +80 -0
  35. package/dist/lib/oc-chatgpt-orchestrator.d.ts.map +1 -0
  36. package/dist/lib/oc-chatgpt-orchestrator.js +130 -0
  37. package/dist/lib/oc-chatgpt-orchestrator.js.map +1 -0
  38. package/dist/lib/oc-chatgpt-target-detection.d.ts +59 -0
  39. package/dist/lib/oc-chatgpt-target-detection.d.ts.map +1 -0
  40. package/dist/lib/oc-chatgpt-target-detection.js +327 -0
  41. package/dist/lib/oc-chatgpt-target-detection.js.map +1 -0
  42. package/dist/lib/prompts/codex.js +2 -2
  43. package/dist/lib/runtime-paths.d.ts.map +1 -1
  44. package/dist/lib/runtime-paths.js +44 -2
  45. package/dist/lib/runtime-paths.js.map +1 -1
  46. package/dist/lib/storage.d.ts +42 -3
  47. package/dist/lib/storage.d.ts.map +1 -1
  48. package/dist/lib/storage.js +634 -49
  49. package/dist/lib/storage.js.map +1 -1
  50. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  51. package/dist/lib/ui/auth-menu.js +13 -1
  52. package/dist/lib/ui/auth-menu.js.map +1 -1
  53. package/dist/lib/ui/copy.d.ts +15 -1
  54. package/dist/lib/ui/copy.d.ts.map +1 -1
  55. package/dist/lib/ui/copy.js +15 -1
  56. package/dist/lib/ui/copy.js.map +1 -1
  57. package/node_modules/@codex-ai/plugin/dist/index.d.ts +1 -1
  58. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +41 -41
  59. package/node_modules/@codex-ai/plugin/dist/tool.js +28 -28
  60. package/package.json +132 -133
  61. package/scripts/bench-format/hashline-v2.mjs +642 -642
  62. package/scripts/bench-format/render.mjs +496 -496
  63. package/scripts/bench-format/stats.mjs +54 -54
  64. package/scripts/bench-format/tasks.mjs +151 -151
  65. package/scripts/benchmark-render-dashboard.mjs +49 -49
  66. package/scripts/codex-multi-auth.js +12 -0
  67. package/scripts/codex.js +315 -2
  68. package/scripts/install-codex-auth.js +9 -9
  69. package/scripts/test-all-models.sh +7 -7
  70. package/scripts/test-model-matrix.js +247 -148
  71. package/scripts/validate-model-map.sh +7 -7
  72. package/vendor/codex-ai-plugin/dist/index.d.ts +1 -1
  73. package/vendor/codex-ai-plugin/dist/tool.d.ts +41 -41
  74. package/vendor/codex-ai-plugin/dist/tool.js +28 -28
  75. package/vendor/codex-ai-sdk/dist/index.d.ts +3 -3
@@ -1,12 +1,14 @@
1
- import { promises as fs, existsSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
3
2
  import { createHash } from "node:crypto";
3
+ import { existsSync, promises as fs } from "node:fs";
4
+ import { basename, dirname, join } from "node:path";
4
5
  import { ACCOUNT_LIMITS } from "./constants.js";
5
6
  import { createLogger } from "./logger.js";
7
+ import { exportNamedBackupFile, resolveNamedBackupPath, } from "./named-backup-export.js";
6
8
  import { MODEL_FAMILIES } from "./prompts/codex.js";
7
9
  import { AnyAccountStorageSchema, getValidationErrors } from "./schemas.js";
8
- import { getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, findProjectRoot, resolvePath, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
9
10
  import { migrateV1ToV3, } from "./storage/migrations.js";
11
+ import { findProjectRoot, getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, resolvePath, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
10
12
  const log = createLogger("storage");
11
13
  const ACCOUNTS_FILE_NAME = "openai-codex-accounts.json";
12
14
  const FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-flagged-accounts.json";
@@ -16,6 +18,7 @@ const ACCOUNTS_WAL_SUFFIX = ".wal";
16
18
  const ACCOUNTS_BACKUP_HISTORY_DEPTH = 3;
17
19
  const BACKUP_COPY_MAX_ATTEMPTS = 5;
18
20
  const BACKUP_COPY_BASE_DELAY_MS = 10;
21
+ const RESET_MARKER_SUFFIX = ".reset-intent";
19
22
  let storageBackupEnabled = true;
20
23
  let lastAccountsSaveTimestamp = 0;
21
24
  /**
@@ -59,6 +62,7 @@ export function formatStorageErrorHint(error, path) {
59
62
  }
60
63
  }
61
64
  let storageMutex = Promise.resolve();
65
+ const transactionSnapshotContext = new AsyncLocalStorage();
62
66
  function withStorageLock(fn) {
63
67
  const previousMutex = storageMutex;
64
68
  let releaseLock;
@@ -67,6 +71,32 @@ function withStorageLock(fn) {
67
71
  });
68
72
  return previousMutex.then(fn).finally(() => releaseLock());
69
73
  }
74
+ function looksLikeSyntheticFixtureAccount(account) {
75
+ const email = typeof account.email === "string" ? account.email.trim().toLowerCase() : "";
76
+ const refreshToken = typeof account.refreshToken === "string"
77
+ ? account.refreshToken.trim().toLowerCase()
78
+ : "";
79
+ const accountId = typeof account.accountId === "string"
80
+ ? account.accountId.trim().toLowerCase()
81
+ : "";
82
+ if (!/^account\d+@example\.com$/.test(email)) {
83
+ return false;
84
+ }
85
+ const hasSyntheticRefreshToken = refreshToken.startsWith("fake_refresh") ||
86
+ /^fake_refresh_token_\d+(_for_testing_only)?$/.test(refreshToken);
87
+ if (!hasSyntheticRefreshToken) {
88
+ return false;
89
+ }
90
+ if (accountId.length === 0) {
91
+ return true;
92
+ }
93
+ return /^acc(_|-)?\d+$/.test(accountId);
94
+ }
95
+ function looksLikeSyntheticFixtureStorage(storage) {
96
+ if (!storage || storage.accounts.length === 0)
97
+ return false;
98
+ return storage.accounts.every((account) => looksLikeSyntheticFixtureAccount(account));
99
+ }
70
100
  async function ensureGitignore(storagePath) {
71
101
  if (!currentStoragePath)
72
102
  return;
@@ -82,7 +112,10 @@ async function ensureGitignore(storagePath) {
82
112
  if (existsSync(gitignorePath)) {
83
113
  content = await fs.readFile(gitignorePath, "utf-8");
84
114
  const lines = content.split("\n").map((l) => l.trim());
85
- if (lines.includes(".codex") || lines.includes(".codex/") || lines.includes("/.codex") || lines.includes("/.codex/")) {
115
+ if (lines.includes(".codex") ||
116
+ lines.includes(".codex/") ||
117
+ lines.includes("/.codex") ||
118
+ lines.includes("/.codex/")) {
86
119
  return;
87
120
  }
88
121
  }
@@ -117,6 +150,47 @@ function getAccountsBackupRecoveryCandidates(path) {
117
150
  }
118
151
  return candidates;
119
152
  }
153
+ async function getAccountsBackupRecoveryCandidatesWithDiscovery(path) {
154
+ const knownCandidates = getAccountsBackupRecoveryCandidates(path);
155
+ const discoveredCandidates = new Set();
156
+ const candidatePrefix = `${basename(path)}.`;
157
+ const knownCandidateSet = new Set(knownCandidates);
158
+ const directoryPath = dirname(path);
159
+ try {
160
+ const entries = await fs.readdir(directoryPath, { withFileTypes: true });
161
+ for (const entry of entries) {
162
+ if (!entry.isFile())
163
+ continue;
164
+ if (!entry.name.startsWith(candidatePrefix))
165
+ continue;
166
+ if (isCacheLikeBackupArtifactName(entry.name))
167
+ continue;
168
+ if (entry.name.endsWith(RESET_MARKER_SUFFIX))
169
+ continue;
170
+ if (entry.name.endsWith(".tmp"))
171
+ continue;
172
+ if (entry.name.includes(".rotate."))
173
+ continue;
174
+ if (entry.name.endsWith(ACCOUNTS_WAL_SUFFIX))
175
+ continue;
176
+ const candidatePath = join(directoryPath, entry.name);
177
+ if (knownCandidateSet.has(candidatePath))
178
+ continue;
179
+ discoveredCandidates.add(candidatePath);
180
+ }
181
+ }
182
+ catch (error) {
183
+ const code = error.code;
184
+ if (code !== "ENOENT") {
185
+ log.warn("Failed to discover account backup candidates", {
186
+ path,
187
+ error: String(error),
188
+ });
189
+ }
190
+ }
191
+ const discoveredOrdered = Array.from(discoveredCandidates).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
192
+ return [...knownCandidates, ...discoveredOrdered];
193
+ }
120
194
  function getAccountsWalPath(path) {
121
195
  return `${path}${ACCOUNTS_WAL_SUFFIX}`;
122
196
  }
@@ -173,7 +247,9 @@ async function createRotatingAccountsBackup(path) {
173
247
  continue;
174
248
  }
175
249
  const stagedPath = buildStagedPath(currentPath, `slot-${i}`);
176
- await copyFileWithRetry(previousPath, stagedPath, { allowMissingSource: true });
250
+ await copyFileWithRetry(previousPath, stagedPath, {
251
+ allowMissingSource: true,
252
+ });
177
253
  if (existsSync(stagedPath)) {
178
254
  stagedWrites.push({ targetPath: currentPath, stagedPath });
179
255
  }
@@ -185,7 +261,10 @@ async function createRotatingAccountsBackup(path) {
185
261
  const latestStagedPath = buildStagedPath(latestBackupPath, "latest");
186
262
  await copyFileWithRetry(path, latestStagedPath);
187
263
  if (existsSync(latestStagedPath)) {
188
- stagedWrites.push({ targetPath: latestBackupPath, stagedPath: latestStagedPath });
264
+ stagedWrites.push({
265
+ targetPath: latestBackupPath,
266
+ stagedPath: latestStagedPath,
267
+ });
189
268
  }
190
269
  for (const stagedWrite of stagedWrites) {
191
270
  await renameFileWithRetry(stagedWrite.stagedPath, stagedWrite.targetPath);
@@ -207,7 +286,8 @@ async function createRotatingAccountsBackup(path) {
207
286
  }
208
287
  function isRotatingBackupTempArtifact(storagePath, candidatePath) {
209
288
  const backupPrefix = `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`;
210
- if (!candidatePath.startsWith(backupPrefix) || !candidatePath.endsWith(".tmp")) {
289
+ if (!candidatePath.startsWith(backupPrefix) ||
290
+ !candidatePath.endsWith(".tmp")) {
211
291
  return false;
212
292
  }
213
293
  const suffix = candidatePath.slice(backupPrefix.length);
@@ -224,7 +304,9 @@ function isRotatingBackupTempArtifact(storagePath, candidatePath) {
224
304
  async function cleanupStaleRotatingBackupArtifacts(path) {
225
305
  const directoryPath = dirname(path);
226
306
  try {
227
- const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
307
+ const directoryEntries = await fs.readdir(directoryPath, {
308
+ withFileTypes: true,
309
+ });
228
310
  const staleArtifacts = directoryEntries
229
311
  .filter((entry) => entry.isFile())
230
312
  .map((entry) => join(directoryPath, entry.name))
@@ -257,6 +339,198 @@ async function cleanupStaleRotatingBackupArtifacts(path) {
257
339
  function computeSha256(value) {
258
340
  return createHash("sha256").update(value).digest("hex");
259
341
  }
342
+ function getIntentionalResetMarkerPath(path) {
343
+ return `${path}${RESET_MARKER_SUFFIX}`;
344
+ }
345
+ function createEmptyStorageWithMetadata(restoreEligible, restoreReason) {
346
+ return {
347
+ version: 3,
348
+ accounts: [],
349
+ activeIndex: 0,
350
+ activeIndexByFamily: {},
351
+ restoreEligible,
352
+ restoreReason,
353
+ };
354
+ }
355
+ function withRestoreMetadata(storage, restoreEligible, restoreReason) {
356
+ return {
357
+ ...storage,
358
+ restoreEligible,
359
+ restoreReason,
360
+ };
361
+ }
362
+ function isCacheLikeBackupArtifactName(entryName) {
363
+ return entryName.toLowerCase().includes(".cache");
364
+ }
365
+ async function statSnapshot(path) {
366
+ try {
367
+ const stats = await fs.stat(path);
368
+ return { exists: true, bytes: stats.size, mtimeMs: stats.mtimeMs };
369
+ }
370
+ catch (error) {
371
+ const code = error.code;
372
+ if (code !== "ENOENT") {
373
+ log.warn("Failed to stat backup candidate", {
374
+ path,
375
+ error: String(error),
376
+ });
377
+ }
378
+ return { exists: false };
379
+ }
380
+ }
381
+ async function describeAccountSnapshot(path, kind, index) {
382
+ const stats = await statSnapshot(path);
383
+ if (!stats.exists) {
384
+ return { kind, path, index, exists: false, valid: false };
385
+ }
386
+ try {
387
+ const { normalized, schemaErrors, storedVersion } = await loadAccountsFromPath(path);
388
+ return {
389
+ kind,
390
+ path,
391
+ index,
392
+ exists: true,
393
+ valid: !!normalized,
394
+ bytes: stats.bytes,
395
+ mtimeMs: stats.mtimeMs,
396
+ version: typeof storedVersion === "number" ? storedVersion : undefined,
397
+ accountCount: normalized?.accounts.length,
398
+ schemaErrors: schemaErrors.length > 0 ? schemaErrors : undefined,
399
+ };
400
+ }
401
+ catch (error) {
402
+ const code = error.code;
403
+ if (code !== "ENOENT") {
404
+ log.warn("Failed to inspect account snapshot", {
405
+ path,
406
+ error: String(error),
407
+ });
408
+ }
409
+ return {
410
+ kind,
411
+ path,
412
+ index,
413
+ exists: true,
414
+ valid: false,
415
+ bytes: stats.bytes,
416
+ mtimeMs: stats.mtimeMs,
417
+ };
418
+ }
419
+ }
420
+ async function describeAccountsWalSnapshot(path) {
421
+ const stats = await statSnapshot(path);
422
+ if (!stats.exists) {
423
+ return { kind: "accounts-wal", path, exists: false, valid: false };
424
+ }
425
+ try {
426
+ const raw = await fs.readFile(path, "utf-8");
427
+ const parsed = JSON.parse(raw);
428
+ if (!isRecord(parsed)) {
429
+ return {
430
+ kind: "accounts-wal",
431
+ path,
432
+ exists: true,
433
+ valid: false,
434
+ bytes: stats.bytes,
435
+ mtimeMs: stats.mtimeMs,
436
+ };
437
+ }
438
+ const entry = parsed;
439
+ if (entry.version !== 1 ||
440
+ typeof entry.content !== "string" ||
441
+ typeof entry.checksum !== "string" ||
442
+ computeSha256(entry.content) !== entry.checksum) {
443
+ return {
444
+ kind: "accounts-wal",
445
+ path,
446
+ exists: true,
447
+ valid: false,
448
+ bytes: stats.bytes,
449
+ mtimeMs: stats.mtimeMs,
450
+ };
451
+ }
452
+ const { normalized, storedVersion, schemaErrors } = parseAndNormalizeStorage(JSON.parse(entry.content));
453
+ return {
454
+ kind: "accounts-wal",
455
+ path,
456
+ exists: true,
457
+ valid: !!normalized,
458
+ bytes: stats.bytes,
459
+ mtimeMs: stats.mtimeMs,
460
+ version: typeof storedVersion === "number" ? storedVersion : undefined,
461
+ accountCount: normalized?.accounts.length,
462
+ schemaErrors: schemaErrors.length > 0 ? schemaErrors : undefined,
463
+ };
464
+ }
465
+ catch {
466
+ return {
467
+ kind: "accounts-wal",
468
+ path,
469
+ exists: true,
470
+ valid: false,
471
+ bytes: stats.bytes,
472
+ mtimeMs: stats.mtimeMs,
473
+ };
474
+ }
475
+ }
476
+ async function loadFlaggedAccountsFromPath(path) {
477
+ const content = await fs.readFile(path, "utf-8");
478
+ const data = JSON.parse(content);
479
+ return normalizeFlaggedStorage(data);
480
+ }
481
+ async function describeFlaggedSnapshot(path, kind, index) {
482
+ const stats = await statSnapshot(path);
483
+ if (!stats.exists) {
484
+ return { kind, path, index, exists: false, valid: false };
485
+ }
486
+ try {
487
+ const storage = await loadFlaggedAccountsFromPath(path);
488
+ return {
489
+ kind,
490
+ path,
491
+ index,
492
+ exists: true,
493
+ valid: true,
494
+ bytes: stats.bytes,
495
+ mtimeMs: stats.mtimeMs,
496
+ version: storage.version,
497
+ flaggedCount: storage.accounts.length,
498
+ };
499
+ }
500
+ catch (error) {
501
+ const code = error.code;
502
+ if (code !== "ENOENT") {
503
+ log.warn("Failed to inspect flagged snapshot", {
504
+ path,
505
+ error: String(error),
506
+ });
507
+ }
508
+ return {
509
+ kind,
510
+ path,
511
+ index,
512
+ exists: true,
513
+ valid: false,
514
+ bytes: stats.bytes,
515
+ mtimeMs: stats.mtimeMs,
516
+ };
517
+ }
518
+ }
519
+ function latestValidSnapshot(snapshots) {
520
+ return snapshots
521
+ .filter((snapshot) => snapshot.valid)
522
+ .sort((left, right) => (right.mtimeMs ?? 0) - (left.mtimeMs ?? 0))[0];
523
+ }
524
+ function buildMetadataSection(storagePath, snapshots) {
525
+ const latestValid = latestValidSnapshot(snapshots);
526
+ return {
527
+ storagePath,
528
+ latestValidPath: latestValid?.path,
529
+ snapshotCount: snapshots.length,
530
+ validSnapshotCount: snapshots.filter((snapshot) => snapshot.valid).length,
531
+ snapshots,
532
+ };
533
+ }
260
534
  export function getLastAccountsSaveTimestamp() {
261
535
  return lastAccountsSaveTimestamp;
262
536
  }
@@ -276,7 +550,9 @@ export function setStoragePath(projectPath) {
276
550
  currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
277
551
  const previousWorktreeScopedPath = join(getProjectGlobalConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
278
552
  currentLegacyWorktreeStoragePath =
279
- previousWorktreeScopedPath !== currentStoragePath ? previousWorktreeScopedPath : null;
553
+ previousWorktreeScopedPath !== currentStoragePath
554
+ ? previousWorktreeScopedPath
555
+ : null;
280
556
  }
281
557
  else {
282
558
  currentStoragePath = null;
@@ -301,6 +577,15 @@ export function getStoragePath() {
301
577
  }
302
578
  return join(getConfigDir(), ACCOUNTS_FILE_NAME);
303
579
  }
580
+ export function buildNamedBackupPath(name) {
581
+ return resolveNamedBackupPath(name, getStoragePath());
582
+ }
583
+ export async function exportNamedBackup(name, options) {
584
+ return exportNamedBackupFile(name, {
585
+ getStoragePath,
586
+ exportAccounts,
587
+ }, options);
588
+ }
304
589
  export function getFlaggedAccountsPath() {
305
590
  return join(dirname(getStoragePath()), FLAGGED_ACCOUNTS_FILE_NAME);
306
591
  }
@@ -311,8 +596,13 @@ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
311
596
  if (!currentStoragePath) {
312
597
  return null;
313
598
  }
314
- const candidatePaths = [currentLegacyWorktreeStoragePath, currentLegacyProjectStoragePath]
315
- .filter((path) => typeof path === "string" && path.length > 0 && path !== currentStoragePath)
599
+ const candidatePaths = [
600
+ currentLegacyWorktreeStoragePath,
601
+ currentLegacyProjectStoragePath,
602
+ ]
603
+ .filter((path) => typeof path === "string" &&
604
+ path.length > 0 &&
605
+ path !== currentStoragePath)
316
606
  .filter((path, index, all) => all.indexOf(path) === index);
317
607
  if (candidatePaths.length === 0) {
318
608
  return null;
@@ -506,7 +796,8 @@ export function deduplicateAccountsByEmail(accounts) {
506
796
  const existingAddedAt = existing.addedAt || 0;
507
797
  const candidateAddedAt = account.addedAt || 0;
508
798
  const isNewer = candidateLastUsed > existingLastUsed ||
509
- (candidateLastUsed === existingLastUsed && candidateAddedAt > existingAddedAt);
799
+ (candidateLastUsed === existingLastUsed &&
800
+ candidateAddedAt > existingAddedAt);
510
801
  if (isNewer) {
511
802
  emailToNewestIndex.set(email, i);
512
803
  }
@@ -578,7 +869,9 @@ export function normalizeAccountStorage(data) {
578
869
  const baseStorage = fromVersion === 1
579
870
  ? migrateV1ToV3(data)
580
871
  : data;
581
- const validAccounts = rawAccounts.filter((account) => isRecord(account) && typeof account.refreshToken === "string" && !!account.refreshToken.trim());
872
+ const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
873
+ typeof account.refreshToken === "string" &&
874
+ !!account.refreshToken.trim());
582
875
  const deduplicatedAccounts = deduplicateAccountsByEmail(deduplicateAccountsByKey(validAccounts));
583
876
  const activeIndex = (() => {
584
877
  if (deduplicatedAccounts.length === 0)
@@ -625,10 +918,88 @@ export function normalizeAccountStorage(data) {
625
918
  export async function loadAccounts() {
626
919
  return loadAccountsInternal(saveAccounts);
627
920
  }
921
+ export async function getBackupMetadata() {
922
+ const storagePath = getStoragePath();
923
+ const walPath = getAccountsWalPath(storagePath);
924
+ const accountCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(storagePath);
925
+ const accountSnapshots = [
926
+ await describeAccountSnapshot(storagePath, "accounts-primary"),
927
+ await describeAccountsWalSnapshot(walPath),
928
+ ];
929
+ for (const [index, candidate] of accountCandidates.entries()) {
930
+ const kind = candidate === `${storagePath}.bak`
931
+ ? "accounts-backup"
932
+ : candidate.startsWith(`${storagePath}.bak.`)
933
+ ? "accounts-backup-history"
934
+ : "accounts-discovered-backup";
935
+ accountSnapshots.push(await describeAccountSnapshot(candidate, kind, index));
936
+ }
937
+ const flaggedPath = getFlaggedAccountsPath();
938
+ const flaggedCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(flaggedPath);
939
+ const flaggedSnapshots = [
940
+ await describeFlaggedSnapshot(flaggedPath, "flagged-primary"),
941
+ ];
942
+ for (const [index, candidate] of flaggedCandidates.entries()) {
943
+ const kind = candidate === `${flaggedPath}.bak`
944
+ ? "flagged-backup"
945
+ : candidate.startsWith(`${flaggedPath}.bak.`)
946
+ ? "flagged-backup-history"
947
+ : "flagged-discovered-backup";
948
+ flaggedSnapshots.push(await describeFlaggedSnapshot(candidate, kind, index));
949
+ }
950
+ return {
951
+ accounts: buildMetadataSection(storagePath, accountSnapshots),
952
+ flaggedAccounts: buildMetadataSection(flaggedPath, flaggedSnapshots),
953
+ };
954
+ }
955
+ export async function getRestoreAssessment() {
956
+ const storagePath = getStoragePath();
957
+ const resetMarkerPath = getIntentionalResetMarkerPath(storagePath);
958
+ const backupMetadata = await getBackupMetadata();
959
+ if (existsSync(resetMarkerPath)) {
960
+ return {
961
+ storagePath,
962
+ restoreEligible: false,
963
+ restoreReason: "intentional-reset",
964
+ backupMetadata,
965
+ };
966
+ }
967
+ const primarySnapshot = backupMetadata.accounts.snapshots.find((snapshot) => snapshot.kind === "accounts-primary");
968
+ if (!primarySnapshot?.exists) {
969
+ return {
970
+ storagePath,
971
+ restoreEligible: true,
972
+ restoreReason: "missing-storage",
973
+ latestSnapshot: backupMetadata.accounts.latestValidPath
974
+ ? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
975
+ : undefined,
976
+ backupMetadata,
977
+ };
978
+ }
979
+ if (primarySnapshot.valid && primarySnapshot.accountCount === 0) {
980
+ return {
981
+ storagePath,
982
+ restoreEligible: true,
983
+ restoreReason: "empty-storage",
984
+ latestSnapshot: primarySnapshot,
985
+ backupMetadata,
986
+ };
987
+ }
988
+ return {
989
+ storagePath,
990
+ restoreEligible: false,
991
+ latestSnapshot: backupMetadata.accounts.latestValidPath
992
+ ? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
993
+ : undefined,
994
+ backupMetadata,
995
+ };
996
+ }
628
997
  function parseAndNormalizeStorage(data) {
629
998
  const schemaErrors = getValidationErrors(AnyAccountStorageSchema, data);
630
999
  const normalized = normalizeAccountStorage(data);
631
- const storedVersion = isRecord(data) ? data.version : undefined;
1000
+ const storedVersion = isRecord(data)
1001
+ ? data.version
1002
+ : undefined;
632
1003
  return { normalized, storedVersion, schemaErrors };
633
1004
  }
634
1005
  async function loadAccountsFromPath(path) {
@@ -638,8 +1009,15 @@ async function loadAccountsFromPath(path) {
638
1009
  }
639
1010
  async function loadAccountsFromJournal(path) {
640
1011
  const walPath = getAccountsWalPath(path);
1012
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
1013
+ if (existsSync(resetMarkerPath)) {
1014
+ return null;
1015
+ }
641
1016
  try {
642
1017
  const raw = await fs.readFile(walPath, "utf-8");
1018
+ if (existsSync(resetMarkerPath)) {
1019
+ return null;
1020
+ }
643
1021
  const parsed = JSON.parse(raw);
644
1022
  if (!isRecord(parsed))
645
1023
  return null;
@@ -663,13 +1041,17 @@ async function loadAccountsFromJournal(path) {
663
1041
  catch (error) {
664
1042
  const code = error.code;
665
1043
  if (code !== "ENOENT") {
666
- log.warn("Failed to load account WAL journal", { path: walPath, error: String(error) });
1044
+ log.warn("Failed to load account WAL journal", {
1045
+ path: walPath,
1046
+ error: String(error),
1047
+ });
667
1048
  }
668
1049
  return null;
669
1050
  }
670
1051
  }
671
1052
  async function loadAccountsInternal(persistMigration) {
672
1053
  const path = getStoragePath();
1054
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
673
1055
  await cleanupStaleRotatingBackupArtifacts(path);
674
1056
  const migratedLegacyStorage = persistMigration
675
1057
  ? await migrateLegacyProjectStorageIfNeeded(persistMigration)
@@ -677,16 +1059,73 @@ async function loadAccountsInternal(persistMigration) {
677
1059
  try {
678
1060
  const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path);
679
1061
  if (schemaErrors.length > 0) {
680
- log.warn("Account storage schema validation warnings", { errors: schemaErrors.slice(0, 5) });
1062
+ log.warn("Account storage schema validation warnings", {
1063
+ errors: schemaErrors.slice(0, 5),
1064
+ });
681
1065
  }
682
1066
  if (normalized && storedVersion !== normalized.version) {
683
- log.info("Migrating account storage to v3", { from: storedVersion, to: normalized.version });
1067
+ log.info("Migrating account storage to v3", {
1068
+ from: storedVersion,
1069
+ to: normalized.version,
1070
+ });
684
1071
  if (persistMigration) {
685
1072
  try {
686
1073
  await persistMigration(normalized);
687
1074
  }
688
1075
  catch (saveError) {
689
- log.warn("Failed to persist migrated storage", { error: String(saveError) });
1076
+ log.warn("Failed to persist migrated storage", {
1077
+ error: String(saveError),
1078
+ });
1079
+ }
1080
+ }
1081
+ }
1082
+ if (existsSync(resetMarkerPath)) {
1083
+ return createEmptyStorageWithMetadata(false, "intentional-reset");
1084
+ }
1085
+ if (normalized && normalized.accounts.length === 0) {
1086
+ return withRestoreMetadata(normalized, true, "empty-storage");
1087
+ }
1088
+ const primaryLooksSynthetic = looksLikeSyntheticFixtureStorage(normalized);
1089
+ if (storageBackupEnabled && normalized && primaryLooksSynthetic) {
1090
+ const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
1091
+ for (const backupPath of backupCandidates) {
1092
+ if (backupPath === path)
1093
+ continue;
1094
+ try {
1095
+ const backup = await loadAccountsFromPath(backupPath);
1096
+ if (!backup.normalized)
1097
+ continue;
1098
+ if (looksLikeSyntheticFixtureStorage(backup.normalized))
1099
+ continue;
1100
+ if (backup.normalized.accounts.length <= 0)
1101
+ continue;
1102
+ log.warn("Detected synthetic primary account storage; promoting backup", {
1103
+ path,
1104
+ backupPath,
1105
+ primaryAccounts: normalized.accounts.length,
1106
+ backupAccounts: backup.normalized.accounts.length,
1107
+ });
1108
+ if (persistMigration) {
1109
+ try {
1110
+ await persistMigration(backup.normalized);
1111
+ }
1112
+ catch (persistError) {
1113
+ log.warn("Failed to persist promoted backup storage", {
1114
+ path,
1115
+ error: String(persistError),
1116
+ });
1117
+ }
1118
+ }
1119
+ return backup.normalized;
1120
+ }
1121
+ catch (backupError) {
1122
+ const backupCode = backupError.code;
1123
+ if (backupCode !== "ENOENT") {
1124
+ log.warn("Failed to load candidate backup for synthetic-primary promotion", {
1125
+ path: backupPath,
1126
+ error: String(backupError),
1127
+ });
1128
+ }
690
1129
  }
691
1130
  }
692
1131
  }
@@ -694,6 +1133,9 @@ async function loadAccountsInternal(persistMigration) {
694
1133
  }
695
1134
  catch (error) {
696
1135
  const code = error.code;
1136
+ if (existsSync(resetMarkerPath)) {
1137
+ return createEmptyStorageWithMetadata(false, "intentional-reset");
1138
+ }
697
1139
  if (code === "ENOENT" && migratedLegacyStorage) {
698
1140
  return migratedLegacyStorage;
699
1141
  }
@@ -712,8 +1154,11 @@ async function loadAccountsInternal(persistMigration) {
712
1154
  }
713
1155
  return recoveredFromWal;
714
1156
  }
1157
+ if (existsSync(resetMarkerPath)) {
1158
+ return createEmptyStorageWithMetadata(false, "intentional-reset");
1159
+ }
715
1160
  if (storageBackupEnabled) {
716
- const backupCandidates = getAccountsBackupRecoveryCandidates(path);
1161
+ const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
717
1162
  for (const backupPath of backupCandidates) {
718
1163
  try {
719
1164
  const backup = await loadAccountsFromPath(backupPath);
@@ -724,7 +1169,10 @@ async function loadAccountsInternal(persistMigration) {
724
1169
  });
725
1170
  }
726
1171
  if (backup.normalized) {
727
- log.warn("Recovered account storage from backup file", { path, backupPath });
1172
+ log.warn("Recovered account storage from backup file", {
1173
+ path,
1174
+ backupPath,
1175
+ });
728
1176
  if (persistMigration) {
729
1177
  try {
730
1178
  await persistMigration(backup.normalized);
@@ -753,17 +1201,37 @@ async function loadAccountsInternal(persistMigration) {
753
1201
  if (code !== "ENOENT") {
754
1202
  log.error("Failed to load account storage", { error: String(error) });
755
1203
  }
1204
+ if (code === "ENOENT") {
1205
+ return createEmptyStorageWithMetadata(true, "missing-storage");
1206
+ }
756
1207
  return null;
757
1208
  }
758
1209
  }
759
1210
  async function saveAccountsUnlocked(storage) {
760
1211
  const path = getStoragePath();
1212
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
761
1213
  const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
762
1214
  const tempPath = `${path}.${uniqueSuffix}.tmp`;
763
1215
  const walPath = getAccountsWalPath(path);
764
1216
  try {
765
1217
  await fs.mkdir(dirname(path), { recursive: true });
766
1218
  await ensureGitignore(path);
1219
+ if (looksLikeSyntheticFixtureStorage(storage)) {
1220
+ try {
1221
+ const existing = await loadNormalizedStorageFromPath(path, "existing account storage");
1222
+ if (existing &&
1223
+ existing.accounts.length > 0 &&
1224
+ !looksLikeSyntheticFixtureStorage(existing)) {
1225
+ throw new StorageError("Refusing to overwrite non-synthetic account storage with synthetic fixture payload", "EINVALID", path, "Detected synthetic fixture-like account payload. Use explicit account import/login commands instead.");
1226
+ }
1227
+ }
1228
+ catch (error) {
1229
+ if (error instanceof StorageError) {
1230
+ throw error;
1231
+ }
1232
+ // Ignore existing-file probe failures and continue with normal save flow.
1233
+ }
1234
+ }
767
1235
  if (storageBackupEnabled && existsSync(path)) {
768
1236
  try {
769
1237
  await createRotatingAccountsBackup(path);
@@ -799,6 +1267,12 @@ async function saveAccountsUnlocked(storage) {
799
1267
  for (let attempt = 0; attempt < 5; attempt++) {
800
1268
  try {
801
1269
  await fs.rename(tempPath, path);
1270
+ try {
1271
+ await fs.unlink(resetMarkerPath);
1272
+ }
1273
+ catch {
1274
+ // Best effort cleanup.
1275
+ }
802
1276
  lastAccountsSaveTimestamp = Date.now();
803
1277
  try {
804
1278
  await fs.unlink(walPath);
@@ -812,7 +1286,7 @@ async function saveAccountsUnlocked(storage) {
812
1286
  const code = renameError.code;
813
1287
  if (code === "EPERM" || code === "EBUSY") {
814
1288
  lastError = renameError;
815
- await new Promise(r => setTimeout(r, 10 * Math.pow(2, attempt)));
1289
+ await new Promise((r) => setTimeout(r, 10 * 2 ** attempt));
816
1290
  continue;
817
1291
  }
818
1292
  throw renameError;
@@ -842,8 +1316,16 @@ async function saveAccountsUnlocked(storage) {
842
1316
  }
843
1317
  export async function withAccountStorageTransaction(handler) {
844
1318
  return withStorageLock(async () => {
845
- const current = await loadAccountsInternal(saveAccountsUnlocked);
846
- return handler(current, saveAccountsUnlocked);
1319
+ const state = {
1320
+ snapshot: await loadAccountsInternal(saveAccountsUnlocked),
1321
+ active: true,
1322
+ };
1323
+ const current = state.snapshot;
1324
+ const persist = async (storage) => {
1325
+ await saveAccountsUnlocked(storage);
1326
+ state.snapshot = storage;
1327
+ };
1328
+ return transactionSnapshotContext.run(state, () => handler(current, persist));
847
1329
  });
848
1330
  }
849
1331
  /**
@@ -865,8 +1347,10 @@ export async function saveAccounts(storage) {
865
1347
  export async function clearAccounts() {
866
1348
  return withStorageLock(async () => {
867
1349
  const path = getStoragePath();
1350
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
868
1351
  const walPath = getAccountsWalPath(path);
869
- const backupPaths = getAccountsBackupRecoveryCandidates(path);
1352
+ const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
1353
+ await fs.writeFile(resetMarkerPath, JSON.stringify({ version: 1, createdAt: Date.now() }), { encoding: "utf-8", mode: 0o600 });
870
1354
  const clearPath = async (targetPath) => {
871
1355
  try {
872
1356
  await fs.unlink(targetPath);
@@ -882,7 +1366,11 @@ export async function clearAccounts() {
882
1366
  }
883
1367
  };
884
1368
  try {
885
- await Promise.all([clearPath(path), clearPath(walPath), ...backupPaths.map(clearPath)]);
1369
+ await Promise.all([
1370
+ clearPath(path),
1371
+ clearPath(walPath),
1372
+ ...backupPaths.map(clearPath),
1373
+ ]);
886
1374
  }
887
1375
  catch {
888
1376
  // Individual path cleanup is already best-effort with per-artifact logging.
@@ -897,13 +1385,22 @@ function normalizeFlaggedStorage(data) {
897
1385
  for (const rawAccount of data.accounts) {
898
1386
  if (!isRecord(rawAccount))
899
1387
  continue;
900
- const refreshToken = typeof rawAccount.refreshToken === "string" ? rawAccount.refreshToken.trim() : "";
1388
+ const refreshToken = typeof rawAccount.refreshToken === "string"
1389
+ ? rawAccount.refreshToken.trim()
1390
+ : "";
901
1391
  if (!refreshToken)
902
1392
  continue;
903
- const flaggedAt = typeof rawAccount.flaggedAt === "number" ? rawAccount.flaggedAt : Date.now();
904
- const isAccountIdSource = (value) => value === "token" || value === "id_token" || value === "org" || value === "manual";
1393
+ const flaggedAt = typeof rawAccount.flaggedAt === "number"
1394
+ ? rawAccount.flaggedAt
1395
+ : Date.now();
1396
+ const isAccountIdSource = (value) => value === "token" ||
1397
+ value === "id_token" ||
1398
+ value === "org" ||
1399
+ value === "manual";
905
1400
  const isSwitchReason = (value) => value === "rate-limit" || value === "initial" || value === "rotation";
906
- const isCooldownReason = (value) => value === "auth-failure" || value === "network-error" || value === "rate-limit";
1401
+ const isCooldownReason = (value) => value === "auth-failure" ||
1402
+ value === "network-error" ||
1403
+ value === "rate-limit";
907
1404
  let rateLimitResetTimes;
908
1405
  if (isRecord(rawAccount.rateLimitResetTimes)) {
909
1406
  const normalizedRateLimits = {};
@@ -928,19 +1425,33 @@ function normalizeFlaggedStorage(data) {
928
1425
  const normalized = {
929
1426
  refreshToken,
930
1427
  addedAt: typeof rawAccount.addedAt === "number" ? rawAccount.addedAt : flaggedAt,
931
- lastUsed: typeof rawAccount.lastUsed === "number" ? rawAccount.lastUsed : flaggedAt,
932
- accountId: typeof rawAccount.accountId === "string" ? rawAccount.accountId : undefined,
1428
+ lastUsed: typeof rawAccount.lastUsed === "number"
1429
+ ? rawAccount.lastUsed
1430
+ : flaggedAt,
1431
+ accountId: typeof rawAccount.accountId === "string"
1432
+ ? rawAccount.accountId
1433
+ : undefined,
933
1434
  accountIdSource,
934
- accountLabel: typeof rawAccount.accountLabel === "string" ? rawAccount.accountLabel : undefined,
1435
+ accountLabel: typeof rawAccount.accountLabel === "string"
1436
+ ? rawAccount.accountLabel
1437
+ : undefined,
935
1438
  email: typeof rawAccount.email === "string" ? rawAccount.email : undefined,
936
- enabled: typeof rawAccount.enabled === "boolean" ? rawAccount.enabled : undefined,
1439
+ enabled: typeof rawAccount.enabled === "boolean"
1440
+ ? rawAccount.enabled
1441
+ : undefined,
937
1442
  lastSwitchReason,
938
1443
  rateLimitResetTimes,
939
- coolingDownUntil: typeof rawAccount.coolingDownUntil === "number" ? rawAccount.coolingDownUntil : undefined,
1444
+ coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
1445
+ ? rawAccount.coolingDownUntil
1446
+ : undefined,
940
1447
  cooldownReason,
941
1448
  flaggedAt,
942
- flaggedReason: typeof rawAccount.flaggedReason === "string" ? rawAccount.flaggedReason : undefined,
943
- lastError: typeof rawAccount.lastError === "string" ? rawAccount.lastError : undefined,
1449
+ flaggedReason: typeof rawAccount.flaggedReason === "string"
1450
+ ? rawAccount.flaggedReason
1451
+ : undefined,
1452
+ lastError: typeof rawAccount.lastError === "string"
1453
+ ? rawAccount.lastError
1454
+ : undefined,
944
1455
  };
945
1456
  byRefreshToken.set(refreshToken, normalized);
946
1457
  }
@@ -951,16 +1462,24 @@ function normalizeFlaggedStorage(data) {
951
1462
  }
952
1463
  export async function loadFlaggedAccounts() {
953
1464
  const path = getFlaggedAccountsPath();
1465
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
954
1466
  const empty = { version: 1, accounts: [] };
955
1467
  try {
956
1468
  const content = await fs.readFile(path, "utf-8");
957
1469
  const data = JSON.parse(content);
958
- return normalizeFlaggedStorage(data);
1470
+ const loaded = normalizeFlaggedStorage(data);
1471
+ if (existsSync(resetMarkerPath)) {
1472
+ return empty;
1473
+ }
1474
+ return loaded;
959
1475
  }
960
1476
  catch (error) {
961
1477
  const code = error.code;
962
1478
  if (code !== "ENOENT") {
963
- log.error("Failed to load flagged account storage", { path, error: String(error) });
1479
+ log.error("Failed to load flagged account storage", {
1480
+ path,
1481
+ error: String(error),
1482
+ });
964
1483
  return empty;
965
1484
  }
966
1485
  }
@@ -1000,13 +1519,31 @@ export async function loadFlaggedAccounts() {
1000
1519
  export async function saveFlaggedAccounts(storage) {
1001
1520
  return withStorageLock(async () => {
1002
1521
  const path = getFlaggedAccountsPath();
1522
+ const markerPath = getIntentionalResetMarkerPath(path);
1003
1523
  const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
1004
1524
  const tempPath = `${path}.${uniqueSuffix}.tmp`;
1005
1525
  try {
1006
1526
  await fs.mkdir(dirname(path), { recursive: true });
1527
+ if (existsSync(path)) {
1528
+ try {
1529
+ await fs.copyFile(path, `${path}.bak`);
1530
+ }
1531
+ catch (backupError) {
1532
+ log.warn("Failed to create flagged backup snapshot", {
1533
+ path,
1534
+ error: String(backupError),
1535
+ });
1536
+ }
1537
+ }
1007
1538
  const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
1008
1539
  await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
1009
1540
  await fs.rename(tempPath, path);
1541
+ try {
1542
+ await fs.unlink(markerPath);
1543
+ }
1544
+ catch {
1545
+ // Best effort cleanup.
1546
+ }
1010
1547
  }
1011
1548
  catch (error) {
1012
1549
  try {
@@ -1015,20 +1552,48 @@ export async function saveFlaggedAccounts(storage) {
1015
1552
  catch {
1016
1553
  // Ignore cleanup failures.
1017
1554
  }
1018
- log.error("Failed to save flagged account storage", { path, error: String(error) });
1555
+ log.error("Failed to save flagged account storage", {
1556
+ path,
1557
+ error: String(error),
1558
+ });
1019
1559
  throw error;
1020
1560
  }
1021
1561
  });
1022
1562
  }
1023
1563
  export async function clearFlaggedAccounts() {
1024
1564
  return withStorageLock(async () => {
1565
+ const path = getFlaggedAccountsPath();
1566
+ const markerPath = getIntentionalResetMarkerPath(path);
1025
1567
  try {
1026
- await fs.unlink(getFlaggedAccountsPath());
1568
+ await fs.writeFile(markerPath, "reset", {
1569
+ encoding: "utf-8",
1570
+ mode: 0o600,
1571
+ });
1027
1572
  }
1028
1573
  catch (error) {
1029
- const code = error.code;
1030
- if (code !== "ENOENT") {
1031
- log.error("Failed to clear flagged account storage", { error: String(error) });
1574
+ log.error("Failed to write flagged reset marker", {
1575
+ path,
1576
+ markerPath,
1577
+ error: String(error),
1578
+ });
1579
+ throw error;
1580
+ }
1581
+ const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
1582
+ for (const candidate of [path, ...backupPaths, markerPath]) {
1583
+ try {
1584
+ await fs.unlink(candidate);
1585
+ }
1586
+ catch (error) {
1587
+ const code = error.code;
1588
+ if (code !== "ENOENT") {
1589
+ log.error("Failed to clear flagged account storage", {
1590
+ path: candidate,
1591
+ error: String(error),
1592
+ });
1593
+ if (candidate === path) {
1594
+ throw error;
1595
+ }
1596
+ }
1032
1597
  }
1033
1598
  }
1034
1599
  });
@@ -1039,19 +1604,34 @@ export async function clearFlaggedAccounts() {
1039
1604
  * @param force - If true, overwrite existing file (default: true)
1040
1605
  * @throws Error if file exists and force is false, or if no accounts to export
1041
1606
  */
1042
- export async function exportAccounts(filePath, force = true) {
1607
+ export async function exportAccounts(filePath, force = true, beforeCommit) {
1043
1608
  const resolvedPath = resolvePath(filePath);
1044
1609
  if (!force && existsSync(resolvedPath)) {
1045
1610
  throw new Error(`File already exists: ${resolvedPath}`);
1046
1611
  }
1047
- const storage = await withAccountStorageTransaction((current) => Promise.resolve(current));
1612
+ const transactionState = transactionSnapshotContext.getStore();
1613
+ const storage = transactionState?.active
1614
+ ? transactionState.snapshot
1615
+ : await withAccountStorageTransaction((current) => Promise.resolve(current));
1048
1616
  if (!storage || storage.accounts.length === 0) {
1049
1617
  throw new Error("No accounts to export");
1050
1618
  }
1051
1619
  await fs.mkdir(dirname(resolvedPath), { recursive: true });
1052
- const content = JSON.stringify(storage, null, 2);
1620
+ await beforeCommit?.(resolvedPath);
1621
+ if (!force && existsSync(resolvedPath)) {
1622
+ throw new Error(`File already exists: ${resolvedPath}`);
1623
+ }
1624
+ const content = JSON.stringify({
1625
+ version: storage.version,
1626
+ accounts: storage.accounts,
1627
+ activeIndex: storage.activeIndex,
1628
+ activeIndexByFamily: storage.activeIndexByFamily,
1629
+ }, null, 2);
1053
1630
  await fs.writeFile(resolvedPath, content, { encoding: "utf-8", mode: 0o600 });
1054
- log.info("Exported accounts", { path: resolvedPath, count: storage.accounts.length });
1631
+ log.info("Exported accounts", {
1632
+ path: resolvedPath,
1633
+ count: storage.accounts.length,
1634
+ });
1055
1635
  }
1056
1636
  /**
1057
1637
  * Imports accounts from a JSON file, merging with existing accounts.
@@ -1077,7 +1657,7 @@ export async function importAccounts(filePath) {
1077
1657
  if (!normalized) {
1078
1658
  throw new Error("Invalid account storage format");
1079
1659
  }
1080
- const { imported: importedCount, total, skipped: skippedCount } = await withAccountStorageTransaction(async (existing, persist) => {
1660
+ const { imported: importedCount, total, skipped: skippedCount, } = await withAccountStorageTransaction(async (existing, persist) => {
1081
1661
  const existingAccounts = existing?.accounts ?? [];
1082
1662
  const existingActiveIndex = existing?.activeIndex ?? 0;
1083
1663
  const merged = [...existingAccounts, ...normalized.accounts];
@@ -1099,7 +1679,12 @@ export async function importAccounts(filePath) {
1099
1679
  const skipped = normalized.accounts.length - imported;
1100
1680
  return { imported, total: deduplicatedAccounts.length, skipped };
1101
1681
  });
1102
- log.info("Imported accounts", { path: resolvedPath, imported: importedCount, skipped: skippedCount, total });
1682
+ log.info("Imported accounts", {
1683
+ path: resolvedPath,
1684
+ imported: importedCount,
1685
+ skipped: skippedCount,
1686
+ total,
1687
+ });
1103
1688
  return { imported: importedCount, total, skipped: skippedCount };
1104
1689
  }
1105
1690
  //# sourceMappingURL=storage.js.map