sidekick-shared 0.27.5 → 0.28.0

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 (314) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +93 -52
  3. package/dist/accountHealth.d.ts +14 -0
  4. package/dist/accountHealth.js +61 -14
  5. package/dist/accountLaunch.d.ts +6 -0
  6. package/dist/accountLaunch.js +61 -4
  7. package/dist/accountLease.d.ts +5 -0
  8. package/dist/accountLease.js +98 -0
  9. package/dist/accountManager.d.ts +7 -0
  10. package/dist/accountManager.js +226 -89
  11. package/dist/accountSync.d.ts +1 -5
  12. package/dist/accountSync.js +22 -6
  13. package/dist/accountSyncTypes.d.ts +9 -0
  14. package/dist/accounts.d.ts +75 -18
  15. package/dist/accounts.js +712 -189
  16. package/dist/browser.d.ts +13 -2
  17. package/dist/browser.js +30 -2
  18. package/dist/claudeCredentialDoctor.d.ts +50 -0
  19. package/dist/claudeCredentialDoctor.js +277 -0
  20. package/dist/claudeCredentialLedger.d.ts +15 -0
  21. package/dist/claudeCredentialLedger.js +130 -0
  22. package/dist/claudeCredentials.d.ts +20 -3
  23. package/dist/claudeCredentials.js +50 -3
  24. package/dist/claudeLiveGuard.d.ts +33 -0
  25. package/dist/claudeLiveGuard.js +46 -0
  26. package/dist/claudePaths.d.ts +11 -0
  27. package/dist/claudePaths.js +61 -0
  28. package/dist/claudeProfiles.d.ts +15 -8
  29. package/dist/claudeProfiles.js +30 -23
  30. package/dist/claudeRefreshLock.d.ts +15 -0
  31. package/dist/claudeRefreshLock.js +145 -0
  32. package/dist/codexAuth.d.ts +4 -0
  33. package/dist/codexAuth.js +18 -0
  34. package/dist/codexCredentialDoctor.d.ts +33 -0
  35. package/dist/codexCredentialDoctor.js +302 -0
  36. package/dist/codexCredentialStore.d.ts +29 -0
  37. package/dist/codexCredentialStore.js +213 -0
  38. package/dist/codexKeepAlive.d.ts +2 -0
  39. package/dist/codexKeepAlive.js +116 -0
  40. package/dist/codexPaths.d.ts +30 -2
  41. package/dist/codexPaths.js +118 -7
  42. package/dist/codexProfiles.d.ts +38 -35
  43. package/dist/codexProfiles.js +848 -715
  44. package/dist/credentialIO.d.ts +67 -7
  45. package/dist/credentialIO.js +211 -47
  46. package/dist/doctor.js +2 -2
  47. package/dist/ensureDefaultAccounts.js +1 -8
  48. package/dist/esm/accountChangeSignal.js +15 -0
  49. package/dist/esm/accountChanges.js +152 -0
  50. package/dist/esm/accountHealth.js +288 -0
  51. package/dist/esm/accountLaunch.js +145 -0
  52. package/dist/esm/accountLease.js +60 -0
  53. package/dist/esm/accountManager.js +606 -0
  54. package/dist/esm/accountRegistry.js +214 -0
  55. package/dist/esm/accountStatus.js +51 -0
  56. package/dist/esm/accountSwitch.js +74 -0
  57. package/dist/esm/accountSync.js +156 -0
  58. package/dist/esm/accountSyncTypes.js +4 -0
  59. package/dist/esm/accounts.js +1291 -0
  60. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  61. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  62. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  63. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  64. package/dist/esm/aggregation/costProvenance.js +31 -0
  65. package/dist/esm/aggregation/snapshot.js +161 -0
  66. package/dist/esm/aggregation/types.js +5 -0
  67. package/dist/esm/analytics/codeImpact.js +18 -0
  68. package/dist/esm/analytics/compactionLedger.js +37 -0
  69. package/dist/esm/analytics/qualityScore.js +112 -0
  70. package/dist/esm/autoSwitch.js +185 -0
  71. package/dist/esm/browser.js +34 -0
  72. package/dist/esm/claudeCredentialDoctor.js +239 -0
  73. package/dist/esm/claudeCredentialLedger.js +91 -0
  74. package/dist/esm/claudeCredentials.js +111 -0
  75. package/dist/esm/claudeLiveGuard.js +43 -0
  76. package/dist/esm/claudePaths.js +22 -0
  77. package/dist/esm/claudeProfiles.js +142 -0
  78. package/dist/esm/claudeRefreshLock.js +106 -0
  79. package/dist/esm/codexAuth.js +101 -0
  80. package/dist/esm/codexCredentialDoctor.js +265 -0
  81. package/dist/esm/codexCredentialStore.js +164 -0
  82. package/dist/esm/codexKeepAlive.js +112 -0
  83. package/dist/esm/codexPaths.js +172 -0
  84. package/dist/esm/codexProfiles.js +1251 -0
  85. package/dist/esm/codexQuota.js +564 -0
  86. package/dist/esm/codexQuotaWatcher.js +363 -0
  87. package/dist/esm/context/composer.js +71 -0
  88. package/dist/esm/context/sessionContext.js +546 -0
  89. package/dist/esm/credentialIO.js +283 -0
  90. package/dist/esm/credentials.js +49 -0
  91. package/dist/esm/doctor.js +305 -0
  92. package/dist/esm/ensureDefaultAccounts.js +62 -0
  93. package/dist/esm/errorHistory.js +56 -0
  94. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  95. package/dist/esm/extractors/gatherAssets.js +32 -0
  96. package/dist/esm/extractors/sessionAssets.js +176 -0
  97. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  98. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  99. package/dist/esm/extractors/tokenUsage.js +41 -0
  100. package/dist/esm/extractors/toolCall.js +142 -0
  101. package/dist/esm/failingTools.js +65 -0
  102. package/dist/esm/fileHead.js +27 -0
  103. package/dist/esm/formatters/eventHighlighter.js +332 -0
  104. package/dist/esm/formatters/noiseClassifier.js +209 -0
  105. package/dist/esm/formatters/sessionDump.js +328 -0
  106. package/dist/esm/formatters/toolSummary.js +226 -0
  107. package/dist/esm/formatting.js +82 -0
  108. package/dist/esm/handoffUrl.js +23 -0
  109. package/dist/esm/historicalStore.js +305 -0
  110. package/dist/esm/humanPrompt.js +88 -0
  111. package/dist/esm/index.js +213 -0
  112. package/dist/esm/keepAlive.js +189 -0
  113. package/dist/esm/liveCredentialGuard.js +33 -0
  114. package/dist/esm/modelAliases.js +51 -0
  115. package/dist/esm/modelCatalog.js +139 -0
  116. package/dist/esm/modelContext.js +280 -0
  117. package/dist/esm/modelInfo.js +832 -0
  118. package/dist/esm/multiProviderQuotaService.js +450 -0
  119. package/dist/esm/node.js +46 -0
  120. package/dist/esm/observedContextWindows.js +165 -0
  121. package/dist/esm/observedSessionCollector.js +939 -0
  122. package/dist/esm/parsers/changelogParser.js +71 -0
  123. package/dist/esm/parsers/codexHistory.js +89 -0
  124. package/dist/esm/parsers/codexParser.js +906 -0
  125. package/dist/esm/parsers/debugLogParser.js +126 -0
  126. package/dist/esm/parsers/jsonl.js +66 -0
  127. package/dist/esm/parsers/mcpToolName.js +28 -0
  128. package/dist/esm/parsers/openCodeParser.js +625 -0
  129. package/dist/esm/parsers/planExtractor.js +335 -0
  130. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  131. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  132. package/dist/esm/parsers/subagentScanner.js +360 -0
  133. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  134. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  135. package/dist/esm/paths.js +134 -0
  136. package/dist/esm/peakHours.js +194 -0
  137. package/dist/esm/phrases.js +1330 -0
  138. package/dist/esm/pricingCatalog.js +247 -0
  139. package/dist/esm/processDetection.js +200 -0
  140. package/dist/esm/projectMigration.js +59 -0
  141. package/dist/esm/promptHistory.js +1221 -0
  142. package/dist/esm/providerFailure.js +362 -0
  143. package/dist/esm/providerIds.js +6 -0
  144. package/dist/esm/providerQuota.js +1 -0
  145. package/dist/esm/providerServiceStatus.js +145 -0
  146. package/dist/esm/providerServiceStatusTypes.js +1 -0
  147. package/dist/esm/providerStatus.js +117 -0
  148. package/dist/esm/providers/claudeCode.js +575 -0
  149. package/dist/esm/providers/claudeListing.js +121 -0
  150. package/dist/esm/providers/codex.js +1034 -0
  151. package/dist/esm/providers/codexDatabase.js +297 -0
  152. package/dist/esm/providers/codexRollouts.js +215 -0
  153. package/dist/esm/providers/detect.js +185 -0
  154. package/dist/esm/providers/diagnostics.js +83 -0
  155. package/dist/esm/providers/directoryListingCache.js +206 -0
  156. package/dist/esm/providers/factory.js +54 -0
  157. package/dist/esm/providers/openCode.js +2008 -0
  158. package/dist/esm/providers/openCodeDatabase.js +419 -0
  159. package/dist/esm/providers/rolloutWalker.js +252 -0
  160. package/dist/esm/providers/types.js +12 -0
  161. package/dist/esm/quota.js +182 -0
  162. package/dist/esm/quotaHistory.js +364 -0
  163. package/dist/esm/quotaPoller.js +230 -0
  164. package/dist/esm/quotaPresentation.js +97 -0
  165. package/dist/esm/quotaResolver.js +231 -0
  166. package/dist/esm/quotaSnapshots.js +130 -0
  167. package/dist/esm/quotaThresholds.js +85 -0
  168. package/dist/esm/readers/decisions.js +24 -0
  169. package/dist/esm/readers/handoff.js +20 -0
  170. package/dist/esm/readers/helpers.js +27 -0
  171. package/dist/esm/readers/history.js +9 -0
  172. package/dist/esm/readers/notes.js +48 -0
  173. package/dist/esm/readers/plans.js +225 -0
  174. package/dist/esm/readers/tasks.js +19 -0
  175. package/dist/esm/report/htmlHelpers.js +280 -0
  176. package/dist/esm/report/htmlReportGenerator.js +704 -0
  177. package/dist/esm/report/index.js +7 -0
  178. package/dist/esm/report/logo.js +2 -0
  179. package/dist/esm/report/openBrowser.js +19 -0
  180. package/dist/esm/report/sessionReportInputs.js +33 -0
  181. package/dist/esm/report/transcriptParser.js +83 -0
  182. package/dist/esm/report/types.js +4 -0
  183. package/dist/esm/schemas/accountManager.js +204 -0
  184. package/dist/esm/schemas/accountStatus.js +25 -0
  185. package/dist/esm/schemas/assistantTurn.js +74 -0
  186. package/dist/esm/schemas/index.js +25 -0
  187. package/dist/esm/schemas/observedSessionV1.js +77 -0
  188. package/dist/esm/schemas/providerFailure.js +37 -0
  189. package/dist/esm/schemas/quota.js +132 -0
  190. package/dist/esm/schemas/quotaHistory.js +40 -0
  191. package/dist/esm/schemas/sessionEvent.js +216 -0
  192. package/dist/esm/schemas/stateFile.js +75 -0
  193. package/dist/esm/schemas/tokenEstimation.js +7 -0
  194. package/dist/esm/schemas/transcript.js +86 -0
  195. package/dist/esm/schemas/usageNormalization.js +38 -0
  196. package/dist/esm/search/advancedFilter.js +197 -0
  197. package/dist/esm/search/sessionSearch.js +66 -0
  198. package/dist/esm/sessionFingerprint.js +40 -0
  199. package/dist/esm/sessionMonitor.js +198 -0
  200. package/dist/esm/sessionPreviews.js +522 -0
  201. package/dist/esm/sessionPromptHistory.js +254 -0
  202. package/dist/esm/sessionPromptSignals.js +364 -0
  203. package/dist/esm/sessionStats.js +160 -0
  204. package/dist/esm/sessionTokenTotals.js +98 -0
  205. package/dist/esm/sessionTranscripts.js +49 -0
  206. package/dist/esm/stateFile.js +119 -0
  207. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  208. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  209. package/dist/esm/statusline/formatter.js +122 -0
  210. package/dist/esm/statusline/index.js +8 -0
  211. package/dist/esm/terminalSync.js +95 -0
  212. package/dist/esm/testing/credentialGuard.js +197 -0
  213. package/dist/esm/testing/index.js +12 -0
  214. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  215. package/dist/esm/tokenEstimation.js +67 -0
  216. package/dist/esm/tokenSummary.js +113 -0
  217. package/dist/esm/toolCategory.js +82 -0
  218. package/dist/esm/transcript.js +257 -0
  219. package/dist/esm/turns/assistantTurn.js +373 -0
  220. package/dist/esm/types/codex.js +23 -0
  221. package/dist/esm/types/decisionLog.js +5 -0
  222. package/dist/esm/types/historicalData.js +47 -0
  223. package/dist/esm/types/knowledgeNote.js +15 -0
  224. package/dist/esm/types/observedSessionV1.js +257 -0
  225. package/dist/esm/types/opencode.js +12 -0
  226. package/dist/esm/types/plan.js +7 -0
  227. package/dist/esm/types/sessionEvent.js +10 -0
  228. package/dist/esm/types/taskPersistence.js +12 -0
  229. package/dist/esm/usage/billingBlocks.js +148 -0
  230. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  231. package/dist/esm/usage/importSessionHistory.js +84 -0
  232. package/dist/esm/usage/subagentUsage.js +18 -0
  233. package/dist/esm/usage/usageEvents.js +402 -0
  234. package/dist/esm/usage/usageReports.js +202 -0
  235. package/dist/esm/usageNormalization.js +95 -0
  236. package/dist/esm/watchers/eventBridge.js +219 -0
  237. package/dist/esm/watchers/factory.js +63 -0
  238. package/dist/esm/watchers/jsonlTail.js +289 -0
  239. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  240. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  241. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  242. package/dist/esm/watchers/types.js +4 -0
  243. package/dist/esm/workspaceRoots.js +56 -0
  244. package/dist/esm/writers/atomic.js +375 -0
  245. package/dist/esm/writers/decisions.js +51 -0
  246. package/dist/esm/writers/notes.js +46 -0
  247. package/dist/esm/writers/tasks.js +63 -0
  248. package/dist/esm/zaiQuota.js +323 -0
  249. package/dist/esm/zaiQuotaApi.js +271 -0
  250. package/dist/esm/zaiQuotaWatcher.js +265 -0
  251. package/dist/fileHead.d.ts +22 -0
  252. package/dist/fileHead.js +33 -0
  253. package/dist/index.d.ts +30 -11
  254. package/dist/index.js +72 -15
  255. package/dist/keepAlive.d.ts +2 -1
  256. package/dist/keepAlive.js +116 -29
  257. package/dist/liveCredentialGuard.d.ts +28 -0
  258. package/dist/liveCredentialGuard.js +36 -0
  259. package/dist/node.d.ts +13 -1
  260. package/dist/node.js +43 -2
  261. package/dist/parsers/codexHistory.js +2 -2
  262. package/dist/parsers/codexParser.d.ts +12 -0
  263. package/dist/parsers/codexParser.js +34 -12
  264. package/dist/parsers/debugLogParser.js +2 -2
  265. package/dist/parsers/sessionPathResolver.js +5 -4
  266. package/dist/processDetection.d.ts +10 -3
  267. package/dist/processDetection.js +44 -11
  268. package/dist/promptHistory.d.ts +32 -0
  269. package/dist/promptHistory.js +115 -59
  270. package/dist/providerFailure.d.ts +12 -6
  271. package/dist/providerFailure.js +45 -0
  272. package/dist/providers/claudeCode.js +7 -99
  273. package/dist/providers/claudeListing.d.ts +22 -0
  274. package/dist/providers/claudeListing.js +158 -0
  275. package/dist/providers/codex.d.ts +3 -1
  276. package/dist/providers/codex.js +26 -227
  277. package/dist/providers/codexRollouts.d.ts +44 -0
  278. package/dist/providers/codexRollouts.js +261 -0
  279. package/dist/providers/detect.js +4 -4
  280. package/dist/readers/plans.js +3 -3
  281. package/dist/schemas/accountManager.d.ts +22 -0
  282. package/dist/schemas/accountManager.js +4 -0
  283. package/dist/schemas/assistantTurn.d.ts +2 -2
  284. package/dist/schemas/index.d.ts +2 -0
  285. package/dist/schemas/index.js +7 -2
  286. package/dist/schemas/providerFailure.d.ts +154 -0
  287. package/dist/schemas/providerFailure.js +40 -0
  288. package/dist/sessionPromptHistory.d.ts +12 -7
  289. package/dist/sessionPromptHistory.js +15 -3
  290. package/dist/sessionPromptSignals.d.ts +9 -0
  291. package/dist/sessionPromptSignals.js +13 -0
  292. package/dist/terminalSync.d.ts +12 -2
  293. package/dist/terminalSync.js +27 -2
  294. package/dist/testing/credentialGuard.d.ts +23 -0
  295. package/dist/testing/credentialGuard.js +233 -0
  296. package/dist/testing/index.d.ts +14 -0
  297. package/dist/testing/index.js +22 -0
  298. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  299. package/dist/testing/sessionLogFixtures.js +169 -0
  300. package/dist/tokenEstimation.d.ts +2 -0
  301. package/dist/tokenEstimation.js +21 -11
  302. package/dist/toolCategory.d.ts +18 -0
  303. package/dist/toolCategory.js +86 -0
  304. package/dist/types/codex.d.ts +4 -0
  305. package/dist/usage/usageEvents.d.ts +16 -0
  306. package/dist/usage/usageEvents.js +53 -9
  307. package/dist/watchers/jsonlTail.d.ts +26 -1
  308. package/dist/watchers/jsonlTail.js +136 -28
  309. package/dist/watchers/jsonlWatcher.js +7 -0
  310. package/dist/watchers/providerReaderWatcher.js +1 -0
  311. package/dist/watchers/types.d.ts +6 -0
  312. package/dist/workspaceRoots.d.ts +23 -0
  313. package/dist/workspaceRoots.js +93 -0
  314. package/package.json +22 -2
@@ -33,8 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.CODEX_KEYRING_FIX_HINT = exports.getCodexCredentialStoreMode = exports.getSystemCodexHome = exports.getCodexProfileHome = exports.getCodexProfilesDir = void 0;
37
- exports.getCodexMonitoringHomes = getCodexMonitoringHomes;
36
+ exports.CODEX_KEYRING_FIX_HINT = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getCodexMonitoringHomes = exports.getCodexCredentialStoreMode = exports.getSystemCodexHome = exports.getCodexProfileHome = exports.getCodexProfilesDir = void 0;
37
+ exports.readStoredCodexAccount = readStoredCodexAccount;
38
+ exports.isLiveCodexProfile = isLiveCodexProfile;
39
+ exports.storeCodexProfileAuthUnlocked = storeCodexProfileAuthUnlocked;
38
40
  exports.storeCodexProfileAuth = storeCodexProfileAuth;
39
41
  exports.parseCodexLoginStatusOutput = parseCodexLoginStatusOutput;
40
42
  exports.getCodexLoginStatus = getCodexLoginStatus;
@@ -45,23 +47,29 @@ exports.isCodexProfileAuthenticated = isCodexProfileAuthenticated;
45
47
  exports.isCodexProfileAuthenticatedAsync = isCodexProfileAuthenticatedAsync;
46
48
  exports.listCodexAccounts = listCodexAccounts;
47
49
  exports.getActiveCodexAccount = getActiveCodexAccount;
48
- exports.resolveSidekickCodexHome = resolveSidekickCodexHome;
49
50
  exports.resolveActiveCodexAccount = resolveActiveCodexAccount;
50
- exports.getCodexExecutionEnv = getCodexExecutionEnv;
51
51
  exports.prepareCodexAccount = prepareCodexAccount;
52
52
  exports.prepareCodexAccountAsync = prepareCodexAccountAsync;
53
+ exports.discardCodexLogin = discardCodexLogin;
53
54
  exports.finalizeCodexAccount = finalizeCodexAccount;
54
55
  exports.finalizeCodexAccountAsync = finalizeCodexAccountAsync;
55
56
  exports.withCodexAuthSwapLock = withCodexAuthSwapLock;
56
57
  exports.dedupeCodexProfiles = dedupeCodexProfiles;
58
+ exports.hasPendingCodexRepair = hasPendingCodexRepair;
57
59
  exports.syncCodexLiveStateUnlocked = syncCodexLiveStateUnlocked;
58
60
  exports.syncCodexLiveState = syncCodexLiveState;
61
+ exports.syncCodexLiveStateAsync = syncCodexLiveStateAsync;
59
62
  exports.cleanupAbandonedCodexLogins = cleanupAbandonedCodexLogins;
60
63
  exports.switchToCodexAccount = switchToCodexAccount;
61
64
  exports.switchToCodexAccountAsync = switchToCodexAccountAsync;
62
65
  exports.reconcileCodexAuthState = reconcileCodexAuthState;
63
66
  exports.reconcileCodexAuthStateAsync = reconcileCodexAuthStateAsync;
67
+ exports.getCodexKeepAliveBlocker = getCodexKeepAliveBlocker;
68
+ exports.beginCodexKeepAlive = beginCodexKeepAlive;
69
+ exports.finishCodexKeepAlive = finishCodexKeepAlive;
70
+ exports.restoreCodexProfileFromBackup = restoreCodexProfileFromBackup;
64
71
  exports.removeCodexAccount = removeCodexAccount;
72
+ exports.cleanupAbandonedCodexLoginsAsync = cleanupAbandonedCodexLoginsAsync;
65
73
  const fs = __importStar(require("fs"));
66
74
  const path = __importStar(require("path"));
67
75
  const child_process_1 = require("child_process");
@@ -78,57 +86,30 @@ const accountSwitch_1 = require("./accountSwitch");
78
86
  const accountSyncTypes_1 = require("./accountSyncTypes");
79
87
  const processDetection_1 = require("./processDetection");
80
88
  const quotaSnapshots_1 = require("./quotaSnapshots");
89
+ const accountLease_1 = require("./accountLease");
90
+ const liveCredentialGuard_1 = require("./liveCredentialGuard");
91
+ const codexCredentialStore_1 = require("./codexCredentialStore");
81
92
  var codexPaths_2 = require("./codexPaths");
82
93
  Object.defineProperty(exports, "getCodexProfilesDir", { enumerable: true, get: function () { return codexPaths_2.getCodexProfilesDir; } });
83
94
  Object.defineProperty(exports, "getCodexProfileHome", { enumerable: true, get: function () { return codexPaths_2.getCodexProfileHome; } });
84
95
  Object.defineProperty(exports, "getSystemCodexHome", { enumerable: true, get: function () { return codexPaths_2.getSystemCodexHome; } });
85
96
  Object.defineProperty(exports, "getCodexCredentialStoreMode", { enumerable: true, get: function () { return codexPaths_2.getCodexCredentialStoreMode; } });
97
+ Object.defineProperty(exports, "getCodexMonitoringHomes", { enumerable: true, get: function () { return codexPaths_2.getCodexMonitoringHomes; } });
98
+ Object.defineProperty(exports, "resolveSidekickCodexHome", { enumerable: true, get: function () { return codexPaths_2.resolveSidekickCodexHome; } });
99
+ Object.defineProperty(exports, "getCodexExecutionEnv", { enumerable: true, get: function () { return codexPaths_2.getCodexExecutionEnv; } });
86
100
  exports.CODEX_KEYRING_FIX_HINT = 'Set `cli_auth_credentials_store = "file"` in ~/.codex/config.toml and run `codex login` again.';
87
- function dedupePaths(paths) {
88
- const seen = new Set();
89
- const unique = [];
90
- for (const candidate of paths) {
91
- const normalized = path.resolve(candidate);
92
- if (seen.has(normalized))
93
- continue;
94
- seen.add(normalized);
95
- unique.push(candidate);
96
- }
97
- return unique;
98
- }
99
- function getCodexMonitoringHomes() {
100
- const explicitHome = (0, codexPaths_1.getExplicitCodexHome)();
101
- if (explicitHome)
102
- return [explicitHome];
103
- // The system home is the single live home; profile homes only matter for
104
- // sessions recorded back when they doubled as live CODEX_HOMEs.
105
- const homes = [(0, codexPaths_1.getDefaultSystemCodexHome)()];
106
- for (const profile of listCodexAccounts()) {
107
- const profileHome = (0, codexPaths_1.getCodexProfileHome)(profile.id);
108
- if (fs.existsSync(path.join(profileHome, 'sessions'))) {
109
- homes.push(profileHome);
110
- }
111
- }
112
- return dedupePaths(homes);
113
- }
114
101
  function readPendingProfile(profileId) {
115
102
  try {
116
- return JSON.parse(fs.readFileSync((0, codexPaths_1.getCodexProfileStatePath)(profileId), 'utf8'));
103
+ return JSON.parse(fs.readFileSync((0, codexPaths_1.getCodexLoginStatePath)(profileId), 'utf8'));
117
104
  }
118
105
  catch {
119
106
  return null;
120
107
  }
121
108
  }
122
109
  function writePendingProfile(profileId, pending) {
123
- (0, codexPaths_1.ensureCodexProfileDirs)(profileId);
124
- (0, atomic_1.atomicWriteJsonSync)((0, codexPaths_1.getCodexProfileStatePath)(profileId), pending);
125
- }
126
- function copyIfExists(source, destination) {
127
- if (!fs.existsSync(source))
128
- return false;
129
- fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
130
- fs.copyFileSync(source, destination);
131
- return true;
110
+ const dir = (0, codexPaths_1.getCodexLoginDir)(profileId);
111
+ fs.mkdirSync(path.join(dir, 'codex-home'), { recursive: true, mode: 0o700 });
112
+ (0, atomic_1.atomicWriteJsonSync)(path.join(dir, 'profile.json'), pending);
132
113
  }
133
114
  /**
134
115
  * Copy the live `config.toml` into a profile home, forcing file-based
@@ -139,24 +120,143 @@ function copySourceCodexConfig(sourceHome, targetHome) {
139
120
  const source = (0, codexPaths_1.readFileOrNull)(path.join(sourceHome, 'config.toml')) ?? '';
140
121
  (0, atomic_1.atomicWriteFileSync)(path.join(targetHome, 'config.toml'), (0, codexPaths_1.forceFileCredentialStore)(source));
141
122
  }
142
- function importCurrentCodexAuth(sourceHome, targetHome) {
143
- const authCopied = copyIfExists(path.join(sourceHome, 'auth.json'), path.join(targetHome, 'auth.json'));
144
- const legacyCredsCopied = copyIfExists(path.join(sourceHome, '.credentials.json'), path.join(targetHome, '.credentials.json'));
145
- return authCopied || legacyCredsCopied;
123
+ /** Read the freshest saved login without creating a CLI-usable copy. */
124
+ function readStoredCodexAccount(profileId) {
125
+ const home = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(profileId));
126
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(profileId);
127
+ if (home.status === 'ok' && backup.status === 'ok') {
128
+ return (0, codexCredentialStore_1.codexLoginOnly)((0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, home.snapshot.authRaw) > 0
129
+ ? backup.snapshot
130
+ : home.snapshot);
131
+ }
132
+ return home.status === 'ok'
133
+ ? (0, codexCredentialStore_1.codexLoginOnly)(home.snapshot)
134
+ : backup.status === 'ok'
135
+ ? backup.snapshot
136
+ : null;
137
+ }
138
+ function profileMatchesSnapshot(profile, snapshot) {
139
+ const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(snapshot.authRaw);
140
+ if ((0, codexAuth_1.identitiesMatch)(identity, profileIdentity(profile)))
141
+ return true;
142
+ // API keys have no account claims. Match their complete credential bytes, never the active pointer.
143
+ if (identity?.authMode === 'api-key' ||
144
+ (!snapshot.authRaw && (0, codexCredentialStore_1.isLegacyCodexLogin)(snapshot.legacyRaw))) {
145
+ const saved = readStoredCodexAccount(profile.id);
146
+ return (saved !== null &&
147
+ (0, codexCredentialStore_1.codexSnapshotFingerprint)((0, codexCredentialStore_1.codexLoginOnly)(snapshot)) === (0, codexCredentialStore_1.codexSnapshotFingerprint)(saved));
148
+ }
149
+ return false;
150
+ }
151
+ function isLiveCodexProfile(profile) {
152
+ const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
153
+ return live.status === 'ok' && profileMatchesSnapshot(profile, live.snapshot);
154
+ }
155
+ /** Caller holds the swap lock. Live-account credentials only enter private storage. */
156
+ function storeCodexProfileAuthUnlocked(profileId, authRaw, legacyRaw, source) {
157
+ const candidate = (0, codexCredentialStore_1.codexLoginOnly)({ authRaw, legacyRaw });
158
+ const profile = listCodexAccounts().find((p) => p.id === profileId);
159
+ const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(authRaw);
160
+ if (profile &&
161
+ identity?.authMode === 'chatgpt' &&
162
+ (identity.email || identity.workspaceId) &&
163
+ (profile.email || profile.metadata?.workspaceId) &&
164
+ !(0, codexAuth_1.identitiesMatch)(identity, profileIdentity(profile))) {
165
+ throw new Error('The Codex credential belongs to a different saved account.');
166
+ }
167
+ const old = (0, codexCredentialStore_1.readCodexBackup)(profileId);
168
+ const chosen = old.status === 'ok' && (0, codexAuth_1.compareCodexAuthFreshness)(old.snapshot.authRaw, authRaw) > 0
169
+ ? old.snapshot
170
+ : candidate;
171
+ (0, codexCredentialStore_1.writeCodexBackup)(profileId, chosen);
172
+ const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
173
+ const sameLink = live.status === 'ok' &&
174
+ (0, codexAuth_1.codexRefreshFingerprint)(chosen.authRaw) !== null &&
175
+ (0, codexAuth_1.codexRefreshFingerprint)(chosen.authRaw) === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw);
176
+ const isLive = live.status === 'ok' &&
177
+ (profile
178
+ ? profileMatchesSnapshot(profile, live.snapshot)
179
+ : (0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(chosen.authRaw), (0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw)));
180
+ if (!(0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()) &&
181
+ live.status !== 'error' &&
182
+ live.status !== 'corrupt' &&
183
+ !isLive &&
184
+ !sameLink) {
185
+ (0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(profileId), chosen);
186
+ }
187
+ if (chosen.authRaw)
188
+ (0, accountHealth_1.writeCodexHealthSidecar)(profileId, chosen.authRaw, source);
146
189
  }
147
- /**
148
- * The single chokepoint for storing a Codex credential in a profile: the raw
149
- * `auth.json` bytes (never re-serialized), the legacy `.credentials.json`
150
- * when present, and the secret-free health sidecar.
151
- */
152
190
  function storeCodexProfileAuth(profileId, authRaw, legacyRaw, source) {
153
- const profileHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
154
- if (authRaw)
155
- (0, atomic_1.atomicWriteFileSync)(path.join(profileHome, 'auth.json'), authRaw);
156
- if (legacyRaw)
157
- (0, atomic_1.atomicWriteFileSync)(path.join(profileHome, '.credentials.json'), legacyRaw);
158
- if (authRaw)
159
- (0, accountHealth_1.writeCodexHealthSidecar)(profileId, authRaw, source);
191
+ withCodexAuthSwapLock(() => storeCodexProfileAuthUnlocked(profileId, authRaw, legacyRaw, source));
192
+ }
193
+ function consumerBlocker(scan) {
194
+ if (!scan?.ok)
195
+ return 'Could not verify that Codex consumers are stopped; retry after closing Codex.';
196
+ if (scan.consumers.length)
197
+ return 'Close running Codex CLI, app, and IDE sessions before changing credentials. --force cannot bypass this check.';
198
+ return null;
199
+ }
200
+ /** The sole installer of live Codex credentials, including rollback and repair. */
201
+ function installIntoLiveCodexStore(candidate, accountId, intent, context) {
202
+ if ((0, accountLease_1.isAccountLeaseHeld)('codex', accountId))
203
+ return { ok: false, reason: 'Keep-alive holds this account; retry when it finishes.' };
204
+ const blocker = consumerBlocker(context.scan);
205
+ if (blocker)
206
+ return { ok: false, reason: blocker };
207
+ const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
208
+ if (mode !== 'file' && mode !== 'unknown')
209
+ return { ok: false, reason: exports.CODEX_KEYRING_FIX_HINT };
210
+ if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
211
+ return { ok: false, reason: 'Unset CODEX_HOME; it points at a Sidekick-owned home.' };
212
+ const current = (0, codexCredentialStore_1.readCodexCredentialStore)();
213
+ if (context.before.status === 'error' || !(0, codexCredentialStore_1.codexStoreMatches)(current, context.before.snapshot)) {
214
+ return { ok: false, reason: 'The live Codex credential changed; nothing was overwritten.' };
215
+ }
216
+ const raw = current.status === 'ok' ? current.snapshot.authRaw : null;
217
+ const fp = (0, codexAuth_1.codexRefreshFingerprint)(raw);
218
+ const candidateIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(candidate.authRaw);
219
+ const candidateProfile = listCodexAccounts().find((p) => p.id === accountId);
220
+ const validIdentity = candidateIdentity?.authMode !== 'chatgpt' ||
221
+ (candidateProfile !== undefined &&
222
+ (0, codexAuth_1.identitiesMatch)(candidateIdentity, profileIdentity(candidateProfile)));
223
+ const decision = (0, liveCredentialGuard_1.decideLiveCredentialWrite)({
224
+ provider: 'Codex',
225
+ intent,
226
+ liveStatus: current.status,
227
+ candidateValid: validIdentity && ((0, codexCredentialStore_1.isCodexLogin)(candidate.authRaw) || (0, codexCredentialStore_1.isLegacyCodexLogin)(candidate.legacyRaw)),
228
+ liveHasCredential: current.status === 'ok',
229
+ liveAccountId: context.liveAccountId,
230
+ candidateAccountId: accountId,
231
+ sameLink: fp !== null && fp === (0, codexAuth_1.codexRefreshFingerprint)(candidate.authRaw),
232
+ strictlyNewer: (0, codexAuth_1.compareCodexAuthFreshness)(candidate.authRaw, raw) > 0,
233
+ capturedOutgoing: context.capturedOutgoing,
234
+ matchesWritten: context.written !== undefined && (0, codexCredentialStore_1.codexStoreMatches)(current, context.written),
235
+ rejectCorrupt: true,
236
+ });
237
+ if (!decision.allow)
238
+ return { ok: false, reason: decision.reason };
239
+ if (current.status === 'error')
240
+ return { ok: false, reason: 'The live Codex login could not be read.' };
241
+ let expected;
242
+ try {
243
+ expected = (0, codexCredentialStore_1.codexInstalledSnapshot)(current.snapshot, candidate);
244
+ }
245
+ catch {
246
+ return {
247
+ ok: false,
248
+ reason: 'Legacy credentials would replace unrelated MCP data; sign in again.',
249
+ };
250
+ }
251
+ try {
252
+ (0, codexCredentialStore_1.writeLiveCodexCredentialStoreUnguarded)(expected);
253
+ if (!(0, codexCredentialStore_1.codexStoreMatches)((0, codexCredentialStore_1.readCodexCredentialStore)(), expected))
254
+ throw new Error('verification failed');
255
+ return { ok: true, installed: expected };
256
+ }
257
+ catch {
258
+ return { ok: false, reason: 'The live Codex write did not verify.', attempted: expected };
259
+ }
160
260
  }
161
261
  function readMetadataFromAuthJson(codexHome) {
162
262
  const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
@@ -212,7 +312,7 @@ function getCodexLoginStatus(codexHome) {
212
312
  timeout: PROBE_TIMEOUT_MS,
213
313
  killSignal: 'SIGKILL',
214
314
  });
215
- return parseCodexLoginStatusOutput(result.status, String(result.stdout ?? ''));
315
+ return parseCodexLoginStatusOutput(result.status, `${String(result.stdout ?? '')}\n${String(result.stderr ?? '')}`);
216
316
  }
217
317
  catch {
218
318
  // Ignore missing CLI or spawn errors.
@@ -223,9 +323,11 @@ function getCodexLoginStatusAsync(codexHome) {
223
323
  return new Promise((resolve) => {
224
324
  try {
225
325
  const env = { ...process.env, CODEX_HOME: codexHome };
226
- (0, child_process_1.execFile)('codex', ['login', 'status'], { encoding: 'utf8', env, timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error, stdout) => {
326
+ (0, child_process_1.execFile)('codex', ['login', 'status'], { encoding: 'utf8', env, timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error, stdout, stderr) => {
227
327
  // A missing CLI, non-zero exit, or timeout all mean "not logged in".
228
- resolve(error ? { loggedIn: false } : parseCodexLoginStatusOutput(0, String(stdout ?? '')));
328
+ resolve(error
329
+ ? { loggedIn: false }
330
+ : parseCodexLoginStatusOutput(0, `${String(stdout ?? '')}\n${String(stderr ?? '')}`));
229
331
  });
230
332
  }
231
333
  catch {
@@ -258,15 +360,13 @@ async function readCodexAccountMetadataAsync(codexHome) {
258
360
  metadataFromLoginStatus(await getCodexLoginStatusAsync(codexHome)));
259
361
  }
260
362
  function hasCodexCredentialFiles(codexHome) {
261
- return (fs.existsSync(path.join(codexHome, 'auth.json')) ||
262
- fs.existsSync(path.join(codexHome, '.credentials.json')));
363
+ return (0, codexCredentialStore_1.readCodexCredentialStore)(codexHome).status === 'ok';
263
364
  }
264
365
  function isCodexProfileAuthenticated(codexHome) {
265
- return hasCodexCredentialFiles(codexHome) || getCodexLoginStatus(codexHome).loggedIn;
366
+ return hasCodexCredentialFiles(codexHome);
266
367
  }
267
- /** {@link isCodexProfileAuthenticated} without blocking the event loop on the CLI probe. */
268
368
  async function isCodexProfileAuthenticatedAsync(codexHome) {
269
- return hasCodexCredentialFiles(codexHome) || (await getCodexLoginStatusAsync(codexHome)).loggedIn;
369
+ return isCodexProfileAuthenticated(codexHome);
270
370
  }
271
371
  function ensureUniqueCodexLabel(label, excludeId) {
272
372
  const normalized = label.trim().toLowerCase();
@@ -279,11 +379,6 @@ function listCodexAccounts() {
279
379
  function getActiveCodexAccount() {
280
380
  return (0, accountRegistry_1.getActiveSavedAccount)('codex');
281
381
  }
282
- function resolveSidekickCodexHome() {
283
- // Account switching swaps auth.json inside the system home, so the system
284
- // home (or an explicit CODEX_HOME) is always the single live home.
285
- return (0, codexPaths_1.getSystemCodexHome)();
286
- }
287
382
  function profileIdentity(profile) {
288
383
  return {
289
384
  email: profile.email ?? profile.metadata?.email,
@@ -305,7 +400,7 @@ function profileIdentity(profile) {
305
400
  */
306
401
  function resolveActiveCodexAccount(options = {}) {
307
402
  const selfHeal = options.selfHeal ?? true;
308
- const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(resolveSidekickCodexHome(), 'auth.json')));
403
+ const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.resolveSidekickCodexHome)(), 'auth.json')));
309
404
  if (identity && (identity.email || identity.workspaceId)) {
310
405
  const match = findProfileForIdentity(identity);
311
406
  if (match) {
@@ -344,226 +439,190 @@ function resolveActiveCodexAccount(options = {}) {
344
439
  }
345
440
  return { source: 'none' };
346
441
  }
347
- function getCodexExecutionEnv(baseEnv = process.env) {
348
- return {
349
- ...baseEnv,
350
- CODEX_HOME: resolveSidekickCodexHome(),
351
- };
352
- }
442
+ // ── Prepare / finalize (add account) ─────────────────────────────────────
353
443
  function keyringRefusal(mode) {
354
444
  return {
355
445
  success: false,
356
446
  needsLogin: false,
357
- error: `Codex stores credentials in the OS keyring (cli_auth_credentials_store = "${mode}"); sidekick can only switch file-based logins. ${exports.CODEX_KEYRING_FIX_HINT}`,
447
+ error: `Codex credential storage (${mode}) is not file-based. ${exports.CODEX_KEYRING_FIX_HINT}`,
358
448
  };
359
449
  }
360
- function prepareCodexAccountCore(label) {
450
+ function prepareCodexAccountCore(label, importCurrent, scan) {
361
451
  const trimmedLabel = label.trim();
362
- if (!trimmedLabel) {
363
- return { result: { success: false, error: 'Codex accounts require a non-empty label.' } };
364
- }
452
+ if (!trimmedLabel)
453
+ return { success: false, error: 'Codex accounts require a non-empty label.' };
365
454
  const labelError = ensureUniqueCodexLabel(trimmedLabel);
366
- if (labelError) {
367
- return { result: { success: false, error: labelError } };
368
- }
455
+ if (labelError)
456
+ return { success: false, error: labelError };
369
457
  const sourceHome = (0, codexPaths_1.getSystemCodexHome)();
370
- const storeMode = (0, codexPaths_1.getCodexCredentialStoreMode)(sourceHome);
371
- const liveAuthPresent = hasCodexCredentialFiles(sourceHome);
372
- if (storeMode === 'keyring' || (storeMode === 'auto' && !liveAuthPresent)) {
373
- return { result: keyringRefusal(storeMode) };
374
- }
375
- const profileId = (0, crypto_1.randomUUID)();
376
- const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
377
- (0, codexPaths_1.ensureCodexProfileDirs)(profileId);
378
- writePendingProfile(profileId, {
379
- label: trimmedLabel,
380
- addedAt: new Date().toISOString(),
381
- });
382
- copySourceCodexConfig(sourceHome, codexHome);
383
- const imported = importCurrentCodexAuth(sourceHome, codexHome);
384
- if (imported) {
385
- // The live login may already be saved under another profile: fold into it
386
- // instead of creating a duplicate seat.
387
- const identity = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
388
- const existing = identity
389
- ? findProfileForIdentity(identity, { fallbackToActive: false })
390
- : null;
391
- if (existing) {
392
- try {
393
- const liveRefresh = (0, codexAuth_1.readLastRefresh)((0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')));
394
- const storedRefresh = (0, codexAuth_1.readLastRefresh)((0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.getCodexProfileHome)(existing.id), 'auth.json')));
395
- const storedIsNewer = storedRefresh !== null && liveRefresh !== null && storedRefresh > liveRefresh;
396
- if (!storedIsNewer) {
397
- storeCodexProfileAuth(existing.id, (0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json')), (0, codexPaths_1.readFileOrNull)(path.join(codexHome, '.credentials.json')), 'login');
398
- }
399
- }
400
- catch {
401
- /* the existing backup stays as it was */
402
- }
403
- fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
404
- const relabel = existing.metadata?.origin === 'live-sync' && existing.label === existing.email
405
- ? {
406
- ...existing,
407
- label: trimmedLabel,
408
- metadata: { ...existing.metadata, origin: 'manual' },
409
- }
410
- : existing;
411
- if (relabel !== existing)
412
- (0, accountRegistry_1.upsertSavedAccountProfile)(relabel);
458
+ const mode = (0, codexPaths_1.getCodexCredentialStoreMode)(sourceHome);
459
+ if (mode !== 'file' && mode !== 'unknown')
460
+ return keyringRefusal(mode);
461
+ if ((0, codexCredentialStore_1.isSidekickCodexHome)(sourceHome))
462
+ return {
463
+ success: false,
464
+ error: 'Unset CODEX_HOME before adding an account; it names a Sidekick-owned home.',
465
+ };
466
+ const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
467
+ if (importCurrent && live.status === 'ok') {
468
+ const sync = syncCodexLiveStateUnlocked('live-sync', { scan });
469
+ const profile = listCodexAccounts().find((p) => profileMatchesSnapshot(p, live.snapshot));
470
+ if (!profile)
413
471
  return {
414
- result: {
415
- success: true,
416
- profileId: existing.id,
417
- codexHome: (0, codexPaths_1.getCodexProfileHome)(existing.id),
418
- needsLogin: false,
419
- warning: `This Codex login is already saved as "${relabel.label ?? existing.email ?? existing.id}".`,
420
- },
472
+ success: false,
473
+ error: sync.warnings[0] ?? 'The live Codex login has no identifiable account.',
421
474
  };
422
- }
423
- return { finalize: { profileId, codexHome } };
475
+ const manual = profile.metadata?.origin === 'live-sync' || profile.label === profile.email;
476
+ if (manual)
477
+ (0, accountRegistry_1.upsertSavedAccountProfile)({
478
+ ...profile,
479
+ label: trimmedLabel,
480
+ metadata: { ...profile.metadata, origin: 'manual' },
481
+ });
482
+ return {
483
+ success: true,
484
+ profileId: profile.id,
485
+ codexHome: sourceHome,
486
+ needsLogin: false,
487
+ warning: sync.warnings.join(' ') ||
488
+ (!sync.registered
489
+ ? `This Codex login is already saved as ${profile.label ?? profile.id}.`
490
+ : undefined),
491
+ };
492
+ }
493
+ if (importCurrent && (live.status === 'corrupt' || live.status === 'error')) {
494
+ return {
495
+ success: false,
496
+ error: 'The live Codex credential could not be read; retry after Codex finishes writing it.',
497
+ };
498
+ }
499
+ const profileId = (0, crypto_1.randomUUID)();
500
+ try {
501
+ writePendingProfile(profileId, { label: trimmedLabel, addedAt: new Date().toISOString() });
502
+ const codexHome = (0, codexPaths_1.getCodexLoginHome)(profileId);
503
+ copySourceCodexConfig(sourceHome, codexHome);
504
+ return { success: true, profileId, codexHome, needsLogin: true };
505
+ }
506
+ catch {
507
+ discardCodexLogin(profileId);
508
+ return { success: false, error: 'Could not prepare the isolated Codex login.' };
424
509
  }
425
- return { result: { success: true, profileId, codexHome, needsLogin: true } };
426
510
  }
427
- function prepareCodexAccount(label) {
428
- const step = prepareCodexAccountCore(label);
429
- if ('finalize' in step) {
430
- const finalized = finalizeCodexAccount(step.finalize.profileId);
431
- return { ...finalized, ...step.finalize, needsLogin: false };
511
+ /** Legacy callers can import current; explicit sign-in passes importCurrent: false. */
512
+ function prepareCodexAccount(label, options = {}) {
513
+ const scan = options.importCurrent === false ? undefined : (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
514
+ try {
515
+ return withCodexAuthSwapLock(() => prepareCodexAccountCore(label, options.importCurrent ?? true, scan));
516
+ }
517
+ catch {
518
+ return { success: false, error: 'Could not acquire the Codex account-switch lock.' };
519
+ }
520
+ }
521
+ async function prepareCodexAccountAsync(label, options = {}) {
522
+ const scan = options.importCurrent === false ? undefined : await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
523
+ try {
524
+ return withCodexAuthSwapLock(() => prepareCodexAccountCore(label, options.importCurrent ?? true, scan));
525
+ }
526
+ catch {
527
+ return { success: false, error: 'Could not acquire the Codex account-switch lock.' };
432
528
  }
433
- return step.result;
434
529
  }
435
- /** {@link prepareCodexAccount} without blocking the event loop on CLI probes. */
436
- async function prepareCodexAccountAsync(label) {
437
- const step = prepareCodexAccountCore(label);
438
- if ('finalize' in step) {
439
- const finalized = await finalizeCodexAccountAsync(step.finalize.profileId);
440
- return { ...finalized, ...step.finalize, needsLogin: false };
441
- }
442
- return step.result;
530
+ /** Removes only a pending login, never a registered canonical profile. */
531
+ function discardCodexLogin(id) {
532
+ fs.rmSync((0, codexPaths_1.getCodexLoginDir)(id), { recursive: true, force: true });
533
+ if (!listCodexAccounts().some((p) => p.id === id) &&
534
+ fs.existsSync((0, codexPaths_1.getCodexProfileStatePath)(id))) {
535
+ fs.rmSync((0, codexPaths_1.getCodexProfileDir)(id), { recursive: true, force: true });
536
+ }
443
537
  }
444
- function finalizeCodexAccountCore(profileId, opts, probes) {
538
+ function finalizeCodexAccountCore(profileId, opts, scan) {
445
539
  const pending = readPendingProfile(profileId);
446
540
  if (!pending) {
447
- // Already finalized: `prepareCodexAccount` folds a live login that is
448
- // already saved into its profile and reports `needsLogin: false`, and the
449
- // login flow then finalizes that profile id. Activate it if asked.
450
- const saved = listCodexAccounts().find((profile) => profile.id === profileId);
451
- if (!saved) {
452
- return { result: { success: false, error: `Codex profile ${profileId} was not prepared.` } };
453
- }
454
- if (opts.activate === false)
455
- return { result: { success: true, profileId } };
456
- return { swap: saved };
457
- }
458
- const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
459
- if (!probes.authenticated) {
460
- return { result: { success: false, error: 'Codex profile is not authenticated yet.' } };
461
- }
462
- // A fresh isolated login for an account that is already saved: fold into the
463
- // existing profile rather than registering a second seat.
464
- const authRaw = (0, codexPaths_1.readFileOrNull)(path.join(codexHome, 'auth.json'));
465
- const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(authRaw);
466
- const existing = identity ? findProfileForIdentity(identity, { fallbackToActive: false }) : null;
467
- if (existing && existing.id !== profileId) {
468
- storeCodexProfileAuth(existing.id, authRaw, (0, codexPaths_1.readFileOrNull)(path.join(codexHome, '.credentials.json')), 'login');
469
- fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
470
- const merged = {
471
- ...existing,
472
- label: existing.metadata?.origin === 'live-sync' ? pending.label : existing.label,
473
- email: probes.metadata.email ?? existing.email,
474
- metadata: { ...existing.metadata, ...probes.metadata, origin: 'login' },
541
+ const saved = listCodexAccounts().find((p) => p.id === profileId);
542
+ if (!saved)
543
+ return { success: false, error: `Codex profile ${profileId} was not prepared.` };
544
+ return opts.activate === false
545
+ ? { success: true, profileId }
546
+ : performCodexAuthSwapCore(saved, {}, { scan });
547
+ }
548
+ const home = (0, codexPaths_1.getCodexLoginHome)(profileId);
549
+ const read = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
550
+ if (read.status !== 'ok')
551
+ return {
552
+ success: false,
553
+ error: 'Codex profile is not authenticated with readable file credentials yet.',
475
554
  };
476
- (0, accountRegistry_1.upsertSavedAccountProfile)(merged);
477
- if (opts.activate === false)
478
- return { result: { success: true, profileId: existing.id } };
479
- return { swap: merged };
480
- }
555
+ const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(read.snapshot.authRaw);
556
+ const metadata = readCodexAccountMetadataFromFiles(home) ?? {};
557
+ const existing = identity ? findProfileForIdentity(identity, { fallbackToActive: false }) : null;
558
+ // Legacy pending homes must not become a saved profile before their credential is moved out.
559
+ const id = existing?.id ?? (home === (0, codexPaths_1.getCodexProfileHome)(profileId) ? (0, crypto_1.randomUUID)() : profileId);
481
560
  const profile = {
482
- id: profileId,
561
+ ...existing,
562
+ id,
483
563
  providerId: 'codex',
484
- label: pending.label,
485
- email: probes.metadata.email,
486
- addedAt: pending.addedAt,
487
- metadata: { ...probes.metadata, origin: 'login' },
564
+ label: existing?.label ?? pending.label,
565
+ email: metadata.email ?? existing?.email,
566
+ addedAt: existing?.addedAt ?? pending.addedAt,
567
+ metadata: { ...existing?.metadata, ...metadata, origin: 'login' },
488
568
  };
489
- (0, accountRegistry_1.upsertSavedAccountProfile)(profile);
490
- if (authRaw)
491
- (0, accountHealth_1.writeCodexHealthSidecar)(profileId, authRaw, 'login');
492
569
  try {
493
- fs.rmSync((0, codexPaths_1.getCodexProfileStatePath)(profileId), { force: true });
570
+ // Preserve first. No successful sign-in is lost if activation is blocked.
571
+ const previous = (0, codexCredentialStore_1.readCodexBackup)(id);
572
+ (0, codexCredentialStore_1.writeCodexBackup)(id, previous.status === 'ok' &&
573
+ (0, codexAuth_1.compareCodexAuthFreshness)(previous.snapshot.authRaw, read.snapshot.authRaw) > 0
574
+ ? previous.snapshot
575
+ : read.snapshot);
576
+ (0, codexCredentialStore_1.removeCodexProfileCredentials)(home, read.snapshot);
577
+ (0, accountRegistry_1.upsertSavedAccountProfile)(profile);
578
+ (0, codexPaths_1.ensureCodexProfileDirs)(id);
579
+ copySourceCodexConfig(home, (0, codexPaths_1.getCodexProfileHome)(id));
580
+ storeCodexProfileAuthUnlocked(id, read.snapshot.authRaw, read.snapshot.legacyRaw, 'login');
494
581
  }
495
582
  catch {
496
- /* the pending marker is advisory */
497
- }
498
- if (opts.activate === false) {
499
- return { result: { success: true, profileId } };
500
- }
501
- if (!hasCodexCredentialFiles(codexHome)) {
502
- // Authenticated via the OS keyring — there are no credential files to
503
- // swap, so the registry pointer is all we can update.
504
- (0, accountRegistry_1.setActiveSavedAccount)('codex', profileId);
505
583
  return {
506
- result: {
507
- success: true,
508
- profileId,
509
- warning: `Codex stores credentials in the OS keyring; sidekick cannot swap them per account, so \`codex\` keeps using the keyring credentials. ${exports.CODEX_KEYRING_FIX_HINT}`,
510
- },
584
+ success: false,
585
+ profileId: id,
586
+ error: 'Could not finish saving the Codex login; any verified private backup was kept.',
511
587
  };
512
588
  }
513
- return { swap: profile };
589
+ finally {
590
+ discardCodexLogin(profileId);
591
+ }
592
+ if (opts.activate === false)
593
+ return { success: true, profileId: id };
594
+ const result = performCodexAuthSwapCore(profile, {}, { scan });
595
+ return result.success
596
+ ? { ...result, profileId: id }
597
+ : {
598
+ ...result,
599
+ profileId: id,
600
+ needsLogin: false,
601
+ error: `The login was saved, but could not be activated: ${result.error}`,
602
+ };
514
603
  }
515
604
  function finalizeCodexAccount(profileId, opts = {}) {
516
- if (!readPendingProfile(profileId)) {
517
- // Already finalized: the core activates a saved profile without probing.
518
- const step = finalizeCodexAccountCore(profileId, opts, { authenticated: false, metadata: {} });
519
- return 'swap' in step ? performCodexAuthSwap(step.swap, {}) : step.result;
520
- }
521
- const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
522
- const authenticated = isCodexProfileAuthenticated(codexHome);
523
- const step = finalizeCodexAccountCore(profileId, opts, {
524
- authenticated,
525
- metadata: authenticated ? readCodexAccountMetadata(codexHome) : {},
526
- });
527
- return 'swap' in step ? performCodexAuthSwap(step.swap, {}) : step.result;
605
+ const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
606
+ try {
607
+ return withCodexAuthSwapLock(() => finalizeCodexAccountCore(profileId, opts, scan));
608
+ }
609
+ catch {
610
+ return { success: false, error: 'Could not finalize the Codex login.' };
611
+ }
528
612
  }
529
- /** {@link finalizeCodexAccount} without blocking the event loop on CLI probes. */
530
613
  async function finalizeCodexAccountAsync(profileId, opts = {}) {
531
- if (!readPendingProfile(profileId)) {
532
- // Already finalized: the core activates a saved profile without probing.
533
- const step = finalizeCodexAccountCore(profileId, opts, { authenticated: false, metadata: {} });
534
- return 'swap' in step ? performCodexAuthSwapAsync(step.swap, {}) : step.result;
535
- }
536
- const codexHome = (0, codexPaths_1.getCodexProfileHome)(profileId);
537
- const authenticated = await isCodexProfileAuthenticatedAsync(codexHome);
538
- const step = finalizeCodexAccountCore(profileId, opts, {
539
- authenticated,
540
- metadata: authenticated ? await readCodexAccountMetadataAsync(codexHome) : {},
541
- });
542
- return 'swap' in step ? performCodexAuthSwapAsync(step.swap, {}) : step.result;
543
- }
544
- // ── Stash / identity matching ────────────────────────────────────────────
545
- function getCodexStashDir() {
546
- return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
547
- }
548
- function stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
614
+ const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
549
615
  try {
550
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
551
- let stashPath = null;
552
- if (liveAuthRaw) {
553
- stashPath = path.join(getCodexStashDir(), `auth-${stamp}.json`);
554
- (0, atomic_1.atomicWriteFileSync)(stashPath, liveAuthRaw);
555
- }
556
- if (liveLegacyRaw) {
557
- const legacyStashPath = path.join(getCodexStashDir(), `credentials-${stamp}.json`);
558
- (0, atomic_1.atomicWriteFileSync)(legacyStashPath, liveLegacyRaw);
559
- stashPath = stashPath ?? legacyStashPath;
560
- }
561
- return stashPath;
616
+ return withCodexAuthSwapLock(() => finalizeCodexAccountCore(profileId, opts, scan));
562
617
  }
563
618
  catch {
564
- return null;
619
+ return { success: false, error: 'Could not finalize the Codex login.' };
565
620
  }
566
621
  }
622
+ // ── Stash / identity matching ────────────────────────────────────────────
623
+ function getCodexStashDir() {
624
+ return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
625
+ }
567
626
  function findProfileForIdentity(identity, options = {}) {
568
627
  const profiles = listCodexAccounts();
569
628
  if (identity?.workspaceId) {
@@ -572,7 +631,8 @@ function findProfileForIdentity(identity, options = {}) {
572
631
  return byWorkspace;
573
632
  }
574
633
  if (identity?.email) {
575
- const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email);
634
+ const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email &&
635
+ (!identity.workspaceId || !profile.metadata?.workspaceId));
576
636
  if (byEmail)
577
637
  return byEmail;
578
638
  }
@@ -597,468 +657,485 @@ function withCodexAuthSwapLock(operation) {
597
657
  fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
598
658
  return (0, atomic_1.withFileLockSync)(path.join(lockDir, 'auth-swap.lock'), operation);
599
659
  }
600
- /**
601
- * Merge duplicate Codex profiles (same workspace, else same email). The oldest
602
- * profile keeps its id and label, receives the freshest `auth.json` and the
603
- * quota snapshot, and the active pointer; losers are stashed, never deleted.
604
- * Returns the removed ids. Caller holds the swap lock.
605
- */
606
- function dedupeCodexProfiles() {
660
+ /** Merge owned profiles only after preserving their newest login in a verified backup. */
661
+ function dedupeCodexProfiles(scan) {
662
+ if (consumerBlocker(scan))
663
+ return [];
607
664
  const removed = [];
608
- const profiles = listCodexAccounts();
609
665
  const groups = new Map();
610
- for (const profile of profiles) {
666
+ for (const profile of listCodexAccounts()) {
611
667
  const key = profile.metadata?.workspaceId
612
668
  ? `ws:${profile.metadata.workspaceId}`
613
- : (profile.email ?? profile.metadata?.email)
614
- ? `email:${profile.email ?? profile.metadata?.email}`
669
+ : profile.email
670
+ ? `email:${profile.email}`
615
671
  : `id:${profile.id}`;
616
672
  groups.set(key, [...(groups.get(key) ?? []), profile]);
617
673
  }
618
674
  for (const group of groups.values()) {
619
- if (group.length < 2)
675
+ if (group.length < 2 || group.some((p) => (0, accountLease_1.isAccountLeaseHeld)('codex', p.id)))
620
676
  continue;
621
677
  const sorted = [...group].sort((a, b) => Date.parse(a.addedAt) - Date.parse(b.addedAt));
622
678
  const keeper = sorted[0];
623
- const keeperAuthPath = path.join((0, codexPaths_1.getCodexProfileHome)(keeper.id), 'auth.json');
624
- let freshestRaw = (0, codexPaths_1.readFileOrNull)(keeperAuthPath);
625
- let freshestAt = (0, codexAuth_1.readLastRefresh)(freshestRaw, keeperAuthPath) ?? -1;
626
- const activeId = (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id;
679
+ let freshest = readStoredCodexAccount(keeper.id);
680
+ for (const p of sorted.slice(1)) {
681
+ const candidate = readStoredCodexAccount(p.id);
682
+ if (candidate &&
683
+ (!freshest || (0, codexAuth_1.compareCodexAuthFreshness)(candidate.authRaw, freshest.authRaw) > 0))
684
+ freshest = candidate;
685
+ }
686
+ if (!freshest)
687
+ continue;
688
+ (0, codexCredentialStore_1.writeCodexBackup)(keeper.id, freshest);
627
689
  for (const loser of sorted.slice(1)) {
628
- const loserAuthPath = path.join((0, codexPaths_1.getCodexProfileHome)(loser.id), 'auth.json');
629
- const loserRaw = (0, codexPaths_1.readFileOrNull)(loserAuthPath);
630
- const loserAt = (0, codexAuth_1.readLastRefresh)(loserRaw, loserAuthPath) ?? -1;
631
- if (loserRaw && loserAt > freshestAt) {
632
- freshestRaw = loserRaw;
633
- freshestAt = loserAt;
634
- }
635
- try {
636
- if (!(0, quotaSnapshots_1.readQuotaSnapshot)('codex', keeper.id)) {
637
- const snapshot = (0, quotaSnapshots_1.readQuotaSnapshot)('codex', loser.id);
638
- if (snapshot)
639
- (0, quotaSnapshots_1.writeQuotaSnapshot)('codex', keeper.id, snapshot);
640
- }
641
- }
642
- catch {
643
- /* quota re-key is best-effort */
644
- }
645
- try {
646
- const stashDir = path.join(getCodexStashDir(), `dup-${loser.id}-${new Date().toISOString().replace(/[:.]/g, '-')}`);
647
- fs.mkdirSync(path.dirname(stashDir), { recursive: true, mode: 0o700 });
648
- fs.renameSync((0, codexPaths_1.getCodexProfileDir)(loser.id), stashDir);
649
- }
650
- catch {
651
- fs.rmSync((0, codexPaths_1.getCodexProfileDir)(loser.id), { recursive: true, force: true });
652
- }
690
+ const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(loser.id));
691
+ if (store.status === 'error' || store.status === 'corrupt')
692
+ continue;
693
+ if (store.status === 'ok')
694
+ (0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(loser.id), store.snapshot);
695
+ const snapshot = (0, quotaSnapshots_1.readQuotaSnapshot)('codex', loser.id);
696
+ if (snapshot && !(0, quotaSnapshots_1.readQuotaSnapshot)('codex', keeper.id))
697
+ (0, quotaSnapshots_1.writeQuotaSnapshot)('codex', keeper.id, snapshot);
698
+ const archive = path.join(getCodexStashDir(), `dup-${loser.id}`);
699
+ fs.mkdirSync(path.dirname(archive), { recursive: true, mode: 0o700 });
700
+ if (fs.existsSync((0, codexPaths_1.getCodexProfileDir)(loser.id)))
701
+ fs.renameSync((0, codexPaths_1.getCodexProfileDir)(loser.id), archive);
702
+ if ((0, accountRegistry_1.getActiveSavedAccount)('codex')?.id === loser.id)
703
+ (0, accountRegistry_1.setActiveSavedAccount)('codex', keeper.id, { silent: true });
653
704
  (0, accountRegistry_1.removeSavedAccountProfile)('codex', loser.id);
705
+ fs.rmSync((0, codexPaths_1.getCodexBackupPath)(loser.id), { force: true });
706
+ fs.rmSync((0, codexPaths_1.getCodexBackupPath)(loser.id, true), { force: true });
654
707
  removed.push(loser.id);
655
- if (activeId === loser.id)
656
- (0, accountRegistry_1.setActiveSavedAccount)('codex', keeper.id, { silent: true });
657
708
  }
658
- if (freshestRaw && freshestRaw !== (0, codexPaths_1.readFileOrNull)(keeperAuthPath)) {
659
- storeCodexProfileAuth(keeper.id, freshestRaw, null, 'migration');
660
- }
661
- const metadata = readCodexAccountMetadataFromFiles((0, codexPaths_1.getCodexProfileHome)(keeper.id)) ?? {};
662
- (0, accountRegistry_1.upsertSavedAccountProfile)({
663
- ...keeper,
664
- email: metadata.email ?? keeper.email,
665
- metadata: { ...keeper.metadata, ...metadata },
666
- });
709
+ storeCodexProfileAuthUnlocked(keeper.id, freshest.authRaw, freshest.legacyRaw, 'migration');
667
710
  }
668
711
  return removed;
669
712
  }
670
- /**
671
- * Fold the live `auth.json` into the saved profiles: register an unknown login
672
- * (label = email), refresh the matching profile's backup when the live file is
673
- * newer, and re-point the active pointer at it. Caller holds the swap lock.
674
- * Never throws; problems land in `warnings`.
675
- */
713
+ function codexJournalPath() {
714
+ return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'switch-journal.json');
715
+ }
716
+ function loginHash(snapshot) {
717
+ return (0, codexCredentialStore_1.codexSnapshotFingerprint)((0, codexCredentialStore_1.codexLoginOnly)(snapshot));
718
+ }
719
+ function clearCodexJournal() {
720
+ fs.rmSync(codexJournalPath(), { force: true });
721
+ }
722
+ function hasPendingCodexRepair() {
723
+ if (fs.existsSync(codexJournalPath()))
724
+ return true;
725
+ const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
726
+ if (live.status === 'error' || live.status === 'corrupt')
727
+ return true;
728
+ if (live.status !== 'ok')
729
+ return false;
730
+ return listCodexAccounts().some((p) => {
731
+ if (!profileMatchesSnapshot(p, live.snapshot))
732
+ return false;
733
+ const home = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(p.id));
734
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(p.id);
735
+ return (home.status !== 'absent' ||
736
+ (backup.status === 'ok' &&
737
+ (0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, live.snapshot.authRaw) > 0));
738
+ });
739
+ }
740
+ function resolveCodexJournal(report, scan) {
741
+ if (!fs.existsSync(codexJournalPath()))
742
+ return;
743
+ let journal;
744
+ try {
745
+ journal = JSON.parse(fs.readFileSync(codexJournalPath(), 'utf8'));
746
+ if (!journal.to || !journal.fromHash || !journal.toHash)
747
+ throw new Error();
748
+ }
749
+ catch {
750
+ report.warnings.push('The Codex switch journal is unreadable; run accounts doctor.');
751
+ return;
752
+ }
753
+ const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
754
+ if (live.status === 'error' || live.status === 'corrupt') {
755
+ report.warnings.push('An interrupted Codex switch is pending; the live login could not be read.');
756
+ return;
757
+ }
758
+ const blocker = consumerBlocker(scan);
759
+ if (blocker) {
760
+ report.warnings.push(blocker);
761
+ return;
762
+ }
763
+ const hash = loginHash(live.snapshot);
764
+ const to = listCodexAccounts().find((p) => p.id === journal.to);
765
+ if (hash === journal.toHash ||
766
+ (live.status === 'ok' && to && profileMatchesSnapshot(to, live.snapshot))) {
767
+ if (live.status !== 'ok')
768
+ return;
769
+ (0, codexCredentialStore_1.writeCodexBackup)(journal.to, live.snapshot);
770
+ const target = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to));
771
+ if (target.status === 'error' || target.status === 'corrupt')
772
+ throw new Error('The interrupted switch target is unreadable.');
773
+ if (target.status === 'ok')
774
+ (0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(journal.to), target.snapshot);
775
+ if (journal.from) {
776
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(journal.from);
777
+ if (backup.status === 'ok' && loginHash(backup.snapshot) === journal.fromHash) {
778
+ if ((0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from)).status === 'absent')
779
+ (0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from), backup.snapshot);
780
+ (0, accountHealth_1.writeCodexHealthSidecar)(journal.from, backup.snapshot.authRaw, 'switch');
781
+ }
782
+ }
783
+ (0, accountRegistry_1.setActiveSavedAccount)('codex', journal.to, { silent: true });
784
+ report.journal = 'finished';
785
+ }
786
+ else if (hash === journal.fromHash) {
787
+ const target = (0, codexCredentialStore_1.readCodexBackup)(journal.to);
788
+ if (target.status === 'ok' && loginHash(target.snapshot) === journal.toHash) {
789
+ if ((0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to)).status === 'absent')
790
+ (0, codexCredentialStore_1.writeInactiveCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.to), target.snapshot);
791
+ }
792
+ if (journal.from) {
793
+ const outgoing = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(journal.from));
794
+ if (outgoing.status === 'ok')
795
+ (0, codexCredentialStore_1.removeCodexProfileCredentials)((0, codexPaths_1.getCodexProfileHome)(journal.from), outgoing.snapshot);
796
+ }
797
+ (0, accountRegistry_1.setActiveSavedAccount)('codex', journal.from, { silent: true });
798
+ report.journal = 'rolled-back';
799
+ }
800
+ else {
801
+ report.journal = 'cleared';
802
+ report.warnings.push('The live Codex login matches neither side of the interrupted switch; it was kept. Private backups are available for inspection.');
803
+ }
804
+ clearCodexJournal();
805
+ }
806
+ /** No subprocesses inside this function: hosts resolve process scans before taking the lock. */
676
807
  function syncCodexLiveStateUnlocked(source = 'live-sync', probes = {}) {
677
808
  const report = (0, accountSyncTypes_1.emptyProviderSyncReport)();
678
809
  try {
679
- const merged = dedupeCodexProfiles();
810
+ const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
811
+ if (mode !== 'file' && mode !== 'unknown') {
812
+ report.skipped = 'keyring';
813
+ report.warnings.push(exports.CODEX_KEYRING_FIX_HINT);
814
+ return report;
815
+ }
816
+ if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)())) {
817
+ report.skipped = 'managed-home';
818
+ report.warnings.push('Unset CODEX_HOME before syncing; it points at a Sidekick-owned home.');
819
+ return report;
820
+ }
821
+ resolveCodexJournal(report, probes.scan);
822
+ let live = (0, codexCredentialStore_1.readCodexCredentialStore)();
823
+ if (live.status === 'error' || live.status === 'corrupt') {
824
+ report.skipped = 'unreadable';
825
+ report.warnings.push('The live Codex login could not be read; nothing was replaced.');
826
+ return report;
827
+ }
828
+ if (live.status === 'absent') {
829
+ report.skipped = 'logged-out';
830
+ return report;
831
+ }
832
+ const merged = dedupeCodexProfiles(probes.scan);
680
833
  if (merged.length)
681
834
  report.merged = merged;
682
- const systemHome = (0, codexPaths_1.getSystemCodexHome)();
683
- const liveAuthPath = path.join(systemHome, 'auth.json');
684
- const liveAuthRaw = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
685
- const liveLegacyRaw = (0, codexPaths_1.readFileOrNull)(path.join(systemHome, '.credentials.json'));
686
- if (!liveAuthRaw && !liveLegacyRaw) {
687
- const mode = (0, codexPaths_1.getCodexCredentialStoreMode)(systemHome);
688
- if (probes.systemKeyringLoggedIn || mode === 'keyring' || mode === 'auto') {
689
- report.skipped = 'keyring';
690
- report.warnings.push(`Codex keeps its login in the OS keyring; sidekick cannot switch it. ${exports.CODEX_KEYRING_FIX_HINT}`);
691
- }
692
- else {
693
- report.skipped = 'logged-out';
835
+ const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(live.snapshot.authRaw);
836
+ const initialSnapshot = live.snapshot;
837
+ let profile = listCodexAccounts().find((p) => profileMatchesSnapshot(p, initialSnapshot));
838
+ if (!profile) {
839
+ if (!identity?.email &&
840
+ !identity?.workspaceId &&
841
+ identity?.authMode !== 'api-key' &&
842
+ !(0, codexCredentialStore_1.isLegacyCodexLogin)(live.snapshot.legacyRaw)) {
843
+ report.skipped = 'no-identity';
844
+ return report;
694
845
  }
695
- return report;
696
- }
697
- const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(liveAuthRaw);
698
- let profile = findProfileForIdentity(identity, { fallbackToActive: false });
699
- if (!profile && identity && (identity.email || identity.workspaceId)) {
700
- const profileId = (0, crypto_1.randomUUID)();
701
846
  profile = {
702
- id: profileId,
847
+ id: (0, crypto_1.randomUUID)(),
703
848
  providerId: 'codex',
704
- label: identity.email ?? identity.workspaceId,
705
- email: identity.email,
849
+ label: identity?.email ?? identity?.workspaceId ?? 'Codex API key',
850
+ email: identity?.email,
706
851
  addedAt: new Date().toISOString(),
707
- metadata: {
708
- email: identity.email,
709
- workspaceId: identity.workspaceId,
710
- planType: identity.planType,
711
- authMode: identity.authMode,
712
- origin: 'live-sync',
713
- },
852
+ metadata: { ...identity, authMode: identity?.authMode ?? 'api-key', origin: 'live-sync' },
714
853
  };
715
- (0, codexPaths_1.ensureCodexProfileDirs)(profileId);
716
- copySourceCodexConfig(systemHome, (0, codexPaths_1.getCodexProfileHome)(profileId));
854
+ // API-key identities require their verified bytes before registry matching can work.
855
+ (0, codexCredentialStore_1.writeCodexBackup)(profile.id, live.snapshot);
717
856
  (0, accountRegistry_1.upsertSavedAccountProfile)(profile);
718
- report.registered = { id: profileId, email: identity.email };
857
+ report.registered = { id: profile.id, email: profile.email };
719
858
  }
720
- else if (!profile) {
721
- // API-key auth or an unparseable token: fold into the active profile
722
- // only when it is itself an api-key login; otherwise stash.
723
- const active = getActiveCodexAccount();
724
- if (active && active.metadata?.authMode === 'api-key') {
725
- profile = active;
859
+ const home = (0, codexPaths_1.getCodexProfileHome)(profile.id);
860
+ (0, codexPaths_1.ensureCodexProfileDirs)(profile.id);
861
+ if (!fs.existsSync(path.join(home, 'config.toml')))
862
+ copySourceCodexConfig((0, codexPaths_1.getSystemCodexHome)(), home);
863
+ const duplicate = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
864
+ const stored = readStoredCodexAccount(profile.id);
865
+ if (stored && (0, codexAuth_1.compareCodexAuthFreshness)(stored.authRaw, live.snapshot.authRaw) > 0) {
866
+ const install = installIntoLiveCodexStore(stored, profile.id, 'repair', {
867
+ before: live,
868
+ liveAccountId: profile.id,
869
+ scan: probes.scan,
870
+ });
871
+ if (install.ok) {
872
+ live = { status: 'ok', snapshot: install.installed };
873
+ report.repaired = profile.id;
726
874
  }
727
875
  else {
728
- report.skipped = 'no-identity';
729
- return report;
876
+ report.warnings.push(`A newer Codex login is saved privately; repair is pending. ${install.reason}`);
730
877
  }
731
878
  }
732
- // Codex rotates the refresh token in place, so for the same identity the
733
- // live file is the freshest copy unless the profile was refreshed on its
734
- // own (keep-alive, isolated launch) and both files say when.
735
- const profileAuthPath = path.join((0, codexPaths_1.getCodexProfileHome)(profile.id), 'auth.json');
736
- const storedRaw = (0, codexPaths_1.readFileOrNull)(profileAuthPath);
737
- const storedRefresh = (0, codexAuth_1.readLastRefresh)(storedRaw);
738
- const liveRefresh = (0, codexAuth_1.readLastRefresh)(liveAuthRaw);
739
- const storedIsNewer = storedRefresh !== null && liveRefresh !== null && storedRefresh > liveRefresh;
740
- const newer = liveAuthRaw !== storedRaw && !storedIsNewer;
741
- if (newer) {
742
- try {
743
- storeCodexProfileAuth(profile.id, liveAuthRaw, liveLegacyRaw, source);
744
- const metadata = readCodexAccountMetadataFromFiles((0, codexPaths_1.getCodexProfileHome)(profile.id)) ?? {};
745
- (0, accountRegistry_1.upsertSavedAccountProfile)({
746
- ...profile,
747
- email: metadata.email ?? profile.email,
748
- metadata: { ...profile.metadata, ...metadata },
749
- });
750
- report.folded = profile.id;
751
- }
752
- catch (err) {
753
- report.warnings.push(`Could not back up the live Codex credentials: ${err}`);
879
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(profile.id);
880
+ if (backup.status !== 'ok' ||
881
+ (0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, live.snapshot.authRaw) <= 0) {
882
+ (0, codexCredentialStore_1.writeCodexBackup)(profile.id, live.snapshot);
883
+ report.folded = profile.id;
884
+ }
885
+ (0, accountHealth_1.writeCodexHealthSidecar)(profile.id, live.snapshot.authRaw, source);
886
+ if (duplicate.status === 'ok') {
887
+ const blocker = consumerBlocker(probes.scan);
888
+ // Preserve a newer copy even when its promotion must wait.
889
+ if ((0, codexAuth_1.compareCodexAuthFreshness)(duplicate.snapshot.authRaw, live.snapshot.authRaw) > 0)
890
+ (0, codexCredentialStore_1.writeCodexBackup)(profile.id, duplicate.snapshot);
891
+ if (blocker || (0, accountLease_1.isAccountLeaseHeld)('codex', profile.id))
892
+ report.warnings.push(blocker ?? 'The profile is leased; duplicate cleanup is pending.');
893
+ else {
894
+ (0, codexCredentialStore_1.removeCodexProfileCredentials)(home, duplicate.snapshot);
895
+ report.demoted = profile.id;
754
896
  }
755
897
  }
756
- const active = (0, accountRegistry_1.getActiveSavedAccount)('codex');
757
- if (!active || active.id !== profile.id) {
898
+ else if (duplicate.status === 'corrupt' || duplicate.status === 'error') {
899
+ report.warnings.push('The live account has an unreadable profile credential; run accounts doctor.');
900
+ }
901
+ if ((0, accountRegistry_1.getActiveSavedAccount)('codex')?.id !== profile.id) {
758
902
  (0, accountRegistry_1.setActiveSavedAccount)('codex', profile.id, { silent: true });
759
903
  report.repointed = profile.id;
760
904
  }
761
905
  }
762
- catch (err) {
763
- report.warnings.push(`Codex account sync failed: ${err}`);
906
+ catch {
907
+ report.warnings.push('Codex account sync could not finish; verified credentials were preserved for the next retry.');
764
908
  }
765
909
  return report;
766
910
  }
767
- /** {@link syncCodexLiveStateUnlocked} under the swap lock. */
768
911
  function syncCodexLiveState(source = 'live-sync') {
912
+ const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
769
913
  try {
770
- return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source));
914
+ return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }));
771
915
  }
772
- catch (err) {
773
- return { warnings: [`Could not acquire the account-switch lock: ${err}`] };
916
+ catch {
917
+ return { warnings: ['Could not acquire the Codex account-switch lock.'] };
774
918
  }
775
919
  }
776
- /**
777
- * Remove isolated login homes that never authenticated once they are older
778
- * than `olderThanMs`. Returns the removed profile ids.
779
- */
780
- function cleanupAbandonedCodexLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Date.now()) {
781
- const removed = [];
782
- let entries = [];
920
+ async function syncCodexLiveStateAsync(source = 'live-sync') {
921
+ const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
783
922
  try {
784
- entries = fs.readdirSync((0, codexPaths_1.getCodexProfilesDir)());
923
+ return withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }));
785
924
  }
786
925
  catch {
787
- return removed;
926
+ return { warnings: ['Could not acquire the Codex account-switch lock.'] };
788
927
  }
789
- const registered = new Set(listCodexAccounts().map((p) => p.id));
790
- for (const profileId of entries) {
791
- if (registered.has(profileId))
792
- continue;
793
- const pending = readPendingProfile(profileId);
794
- if (!pending)
795
- continue;
796
- if (hasCodexCredentialFiles((0, codexPaths_1.getCodexProfileHome)(profileId)))
797
- continue;
798
- const addedAt = Date.parse(pending.addedAt);
799
- const age = Number.isNaN(addedAt) ? Number.POSITIVE_INFINITY : now - addedAt;
800
- if (age < olderThanMs)
801
- continue;
802
- try {
803
- fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
804
- removed.push(profileId);
805
- }
806
- catch {
807
- /* best effort */
928
+ }
929
+ /** Only abandoned Sidekick-owned login homes; authenticated copies are preserved privately first. */
930
+ function cleanupAbandonedCodexLogins(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now(), scan) {
931
+ const removed = [];
932
+ if (consumerBlocker(scan ?? (0, processDetection_1.inspectRunningAccountConsumersSync)('codex')))
933
+ return removed;
934
+ withCodexAuthSwapLock(() => {
935
+ for (const root of [(0, codexPaths_1.getCodexLoginsDir)(), (0, codexPaths_1.getCodexProfilesDir)()]) {
936
+ if (!fs.existsSync(root))
937
+ continue;
938
+ for (const id of fs.readdirSync(root)) {
939
+ if (listCodexAccounts().some((p) => p.id === id))
940
+ continue;
941
+ const pending = readPendingProfile(id);
942
+ if (!pending || now - Date.parse(pending.addedAt) < olderThanMs)
943
+ continue;
944
+ const home = (0, codexPaths_1.getCodexLoginHome)(id);
945
+ const read = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
946
+ if (read.status === 'error' || read.status === 'corrupt')
947
+ continue;
948
+ if (read.status === 'ok') {
949
+ const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(read.snapshot.authRaw);
950
+ const owner = identity
951
+ ? findProfileForIdentity(identity, { fallbackToActive: false })
952
+ : null;
953
+ if (!owner)
954
+ continue; // An unregistered authenticated login needs a human decision.
955
+ const saved = readStoredCodexAccount(owner.id);
956
+ (0, codexCredentialStore_1.writeCodexBackup)(owner.id, saved && (0, codexAuth_1.compareCodexAuthFreshness)(saved.authRaw, read.snapshot.authRaw) >= 0
957
+ ? saved
958
+ : read.snapshot);
959
+ }
960
+ discardCodexLogin(id);
961
+ removed.push(id);
962
+ }
808
963
  }
809
- }
964
+ });
810
965
  return removed;
811
966
  }
812
- /**
813
- * CLI probes are resolved before the auth-swap lock is taken — never spawn a
814
- * child process while holding it. The keyring probe is skipped (false) when
815
- * live credential files exist, because the swap only consults it in their
816
- * absence.
817
- */
818
- function resolveCodexSwapProbesSync() {
819
- const systemHome = (0, codexPaths_1.getSystemCodexHome)();
820
- return {
821
- consumers: (0, processDetection_1.detectRunningAccountConsumersSync)('codex'),
822
- systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
823
- ? false
824
- : getCodexLoginStatus(systemHome).loggedIn,
825
- };
826
- }
827
- async function resolveCodexSwapProbesAsync() {
828
- const systemHome = (0, codexPaths_1.getSystemCodexHome)();
829
- return {
830
- consumers: await (0, processDetection_1.detectRunningAccountConsumers)('codex'),
831
- systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
832
- ? false
833
- : (await getCodexLoginStatusAsync(systemHome)).loggedIn,
834
- };
835
- }
836
- function performCodexAuthSwap(target, options) {
837
- const probes = resolveCodexSwapProbesSync();
838
- try {
839
- return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, probes));
840
- }
841
- catch (err) {
842
- // Keep the non-throwing result contract when the lock cannot be acquired.
843
- return (0, accountSwitch_1.switchFailure)('codex', target.id, `Could not acquire the account-switch lock: ${err}`);
844
- }
845
- }
846
- async function performCodexAuthSwapAsync(target, options) {
847
- const probes = await resolveCodexSwapProbesAsync();
848
- let result;
849
- try {
850
- // The sync lock is fine here: probes are pre-resolved, so the critical
851
- // section is only fast local-filesystem work.
852
- result = withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, probes));
853
- }
854
- catch (err) {
855
- return (0, accountSwitch_1.switchFailure)('codex', target.id, `Could not acquire the account-switch lock: ${err}`);
856
- }
857
- if (result.success && options.verifyWithCli && !result.alreadyActive) {
858
- const status = await getCodexLoginStatusAsync((0, codexPaths_1.getSystemCodexHome)());
859
- result = status.loggedIn
860
- ? { ...result, verification: 'cli' }
861
- : (0, accountSwitch_1.finishSwitchResult)({
862
- ...result,
863
- warnings: [
864
- ...result.warnings,
865
- '`codex login status` reports "Not logged in" after the switch; the stored token may have been revoked. Sign in again if codex prompts you.',
866
- ],
867
- });
868
- }
869
- return result;
870
- }
871
967
  function performCodexAuthSwapCore(target, options, probes) {
872
- const systemHome = (0, codexPaths_1.getSystemCodexHome)();
873
- const liveAuthPath = path.join(systemHome, 'auth.json');
874
- const liveLegacyPath = path.join(systemHome, '.credentials.json');
875
- const targetName = target.label ?? target.email ?? target.id;
876
- let previousAccountId = (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null;
877
968
  const base = {
878
969
  success: true,
879
970
  provider: 'codex',
880
971
  accountId: target.id,
881
- previousAccountId,
972
+ previousAccountId: (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null,
882
973
  verified: false,
883
974
  verification: 'none',
884
975
  warnings: [],
885
976
  hints: [],
886
- runningConsumers: probes.consumers,
977
+ runningConsumers: probes.scan.consumers,
887
978
  email: target.email ?? target.metadata?.email,
888
979
  };
889
- const profilesDir = path.resolve((0, codexPaths_1.getCodexProfilesDir)());
890
- const resolvedHome = path.resolve(systemHome);
891
- if (resolvedHome === profilesDir || resolvedHome.startsWith(profilesDir + path.sep)) {
892
- return (0, accountSwitch_1.finishSwitchResult)({
893
- ...base,
894
- success: false,
895
- error: `CODEX_HOME points at a sidekick profile home (${systemHome}); unset it before switching accounts.`,
896
- });
897
- }
898
- const liveBefore = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
899
- const liveLegacyBefore = (0, codexPaths_1.readFileOrNull)(liveLegacyPath);
900
- if (!liveBefore && !liveLegacyBefore && probes.systemKeyringLoggedIn) {
901
- return (0, accountSwitch_1.finishSwitchResult)({
902
- ...base,
903
- success: false,
904
- error: `Codex stores credentials in the OS keyring; file-based account switching is not supported. ${exports.CODEX_KEYRING_FIX_HINT}`,
905
- });
906
- }
907
- // Phase 1: fold the live (freshest, rotated) credentials back into their
908
- // profile, registering the login if sidekick has never seen it.
909
- const sync = syncCodexLiveStateUnlocked('switch', {
910
- systemKeyringLoggedIn: probes.systemKeyringLoggedIn,
911
- });
912
- base.warnings.push(...sync.warnings);
913
- // Only now is the "previous" account known: the sync may have re-pointed or
914
- // registered the login that was really live, and undo must return to it.
915
- previousAccountId = (0, accountRegistry_1.getActiveSavedAccount)('codex')?.id ?? null;
916
- base.previousAccountId = previousAccountId;
917
- if (sync.skipped === 'no-identity' && liveBefore) {
918
- const stashPath = stashLiveCodexAuth(liveBefore, liveLegacyBefore);
919
- base.warnings.push(stashPath
920
- ? `Live Codex credentials did not match any saved account; stashed at ${stashPath}.`
921
- : 'Live Codex credentials did not match any saved account and could not be stashed.');
922
- }
923
- // Re-read after the sync: the target may have received the live file.
924
- const liveAuthRaw = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
925
- const liveLegacyRaw = (0, codexPaths_1.readFileOrNull)(liveLegacyPath);
926
- const profileHome = (0, codexPaths_1.getCodexProfileHome)(target.id);
927
- const targetAuthPath = path.join(profileHome, 'auth.json');
928
- const targetAuthRaw = (0, codexPaths_1.readFileOrNull)(targetAuthPath);
929
- const targetLegacyRaw = (0, codexPaths_1.readFileOrNull)(path.join(profileHome, '.credentials.json'));
930
- if (!targetAuthRaw && !targetLegacyRaw) {
931
- return (0, accountSwitch_1.finishSwitchResult)({
932
- ...base,
933
- success: false,
934
- needsLogin: true,
935
- error: `No stored credentials for "${targetName}". Sign in again to this account.`,
936
- });
937
- }
938
- if (targetAuthRaw && !(0, codexAuth_1.parseAuthJson)(targetAuthRaw)) {
939
- return (0, accountSwitch_1.finishSwitchResult)({
940
- ...base,
941
- success: false,
942
- needsLogin: true,
943
- error: `Stored credentials for "${targetName}" are corrupted. Sign in again to this account.`,
944
- });
945
- }
946
- // If the live file already belongs to the target account it is the freshest
947
- // copy (rotated refresh token included) — never replace it with a staler
948
- // backup, which would permanently invalidate the login.
949
- const liveIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(liveAuthRaw);
950
- const liveMatchesTarget = Boolean((0, codexAuth_1.identitiesMatch)(liveIdentity, {
951
- ...profileIdentity(target),
952
- ...((0, codexAuth_1.readAuthIdentityFromRaw)(targetAuthRaw) ?? {}),
953
- }) ||
954
- (liveAuthRaw !== null && liveAuthRaw === targetAuthRaw) ||
955
- (!liveAuthRaw && !targetAuthRaw && liveLegacyRaw !== null && liveLegacyRaw === targetLegacyRaw));
956
- if (liveMatchesTarget) {
957
- if (previousAccountId !== target.id)
958
- (0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
980
+ const fail = (error, extra = {}) => (0, accountSwitch_1.finishSwitchResult)({ ...base, success: false, error, ...extra });
981
+ const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
982
+ if (mode !== 'file' && mode !== 'unknown')
983
+ return fail(exports.CODEX_KEYRING_FIX_HINT);
984
+ if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
985
+ return fail('CODEX_HOME points at a Sidekick-owned home; unset it before switching.');
986
+ let before = (0, codexCredentialStore_1.readCodexCredentialStore)();
987
+ if (before.status === 'error' || before.status === 'corrupt')
988
+ return fail('The live Codex login could not be read; it was left untouched.');
989
+ const saved = readStoredCodexAccount(target.id);
990
+ if (before.status === 'ok' && profileMatchesSnapshot(target, before.snapshot)) {
991
+ if (saved && (0, codexAuth_1.compareCodexAuthFreshness)(saved.authRaw, before.snapshot.authRaw) > 0) {
992
+ const result = installIntoLiveCodexStore(saved, target.id, 'relogin', {
993
+ before,
994
+ liveAccountId: target.id,
995
+ scan: probes.scan,
996
+ });
997
+ if (!result.ok)
998
+ return fail(result.reason);
999
+ }
1000
+ const sync = syncCodexLiveStateUnlocked('switch', probes);
959
1001
  return (0, accountSwitch_1.finishSwitchResult)({
960
1002
  ...base,
961
1003
  alreadyActive: true,
962
1004
  verified: true,
963
1005
  verification: 'store',
1006
+ warnings: sync.warnings,
964
1007
  health: (0, accountHealth_1.getAccountHealth)('codex', target.id),
965
- hints: [`${targetName} is already the active Codex account.`],
1008
+ hints: ['That account is already the live Codex login.'],
966
1009
  });
967
1010
  }
968
- // Preflight: warn about stale backups; refuse ones known to be dead.
1011
+ const blocker = consumerBlocker(probes.scan);
1012
+ if (blocker)
1013
+ return fail(blocker);
1014
+ if ((0, accountLease_1.isAccountLeaseHeld)('codex', target.id))
1015
+ return fail('Keep-alive is refreshing this account; retry after it finishes.');
1016
+ const sync = syncCodexLiveStateUnlocked('switch', probes);
1017
+ base.warnings.push(...sync.warnings);
1018
+ if (fs.existsSync(codexJournalPath()))
1019
+ return fail('An interrupted Codex switch still needs recovery; run accounts doctor.');
1020
+ before = (0, codexCredentialStore_1.readCodexCredentialStore)();
1021
+ if (before.status === 'error' || before.status === 'corrupt')
1022
+ return fail('The live Codex credential could not be read.');
1023
+ const beforeSnapshot = before.snapshot;
1024
+ const outgoing = before.status === 'ok'
1025
+ ? listCodexAccounts().find((p) => profileMatchesSnapshot(p, beforeSnapshot))
1026
+ : null;
1027
+ if (before.status === 'ok' && !outgoing)
1028
+ return fail('The live login has no unambiguous account identity to save it under; sign in with Codex and retry.');
1029
+ base.previousAccountId = outgoing?.id ?? null;
1030
+ if (outgoing?.id === target.id)
1031
+ return fail('The live account changed during the switch; retry.');
1032
+ const targetHome = (0, codexPaths_1.getCodexProfileHome)(target.id);
1033
+ const targetBefore = (0, codexCredentialStore_1.readCodexCredentialStore)(targetHome);
1034
+ if (targetBefore.status === 'error' || targetBefore.status === 'corrupt')
1035
+ return fail('The target profile is unreadable; the switch was deferred.');
1036
+ const candidate = readStoredCodexAccount(target.id);
1037
+ if (!candidate)
1038
+ return fail('No readable stored credentials for this account; sign in again.', {
1039
+ needsLogin: true,
1040
+ });
1041
+ const candidateIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(candidate.authRaw);
1042
+ if (candidateIdentity?.authMode === 'chatgpt' &&
1043
+ !(0, codexAuth_1.identitiesMatch)(candidateIdentity, profileIdentity(target))) {
1044
+ return fail('The saved Codex credential does not belong to the selected account.');
1045
+ }
1046
+ const liveFp = (0, codexAuth_1.codexRefreshFingerprint)(beforeSnapshot.authRaw);
1047
+ if (liveFp && liveFp === (0, codexAuth_1.codexRefreshFingerprint)(candidate.authRaw))
1048
+ return fail('The target is another copy of the live refresh token; sign in again.');
969
1049
  const health = (0, accountHealth_1.getAccountHealth)('codex', target.id, { probe: 'store' });
970
1050
  base.health = health;
971
- if ((health.state === 'expired' || health.state === 'missing') && !options.force) {
972
- return (0, accountSwitch_1.finishSwitchResult)({
973
- ...base,
974
- success: false,
1051
+ if ((health.state === 'expired' || health.state === 'missing') && !options.force)
1052
+ return fail('Stored credentials have expired or are missing; sign in again.', {
975
1053
  needsLogin: true,
976
- error: `Stored credentials for "${targetName}" have expired; sign in again.`,
977
1054
  });
1055
+ const refreshed = (0, codexAuth_1.readLastRefresh)(candidate.authRaw);
1056
+ if (refreshed !== null && Date.now() - refreshed > codexAuth_1.STALE_AUTH_THRESHOLD_MS)
1057
+ base.warnings.push('The stored Codex login has not been refreshed in over 8 days.');
1058
+ // Capture and verify both sides before removing either usable copy. Mandatory even with force.
1059
+ try {
1060
+ if (outgoing)
1061
+ (0, codexCredentialStore_1.writeCodexBackup)(outgoing.id, beforeSnapshot);
1062
+ (0, codexCredentialStore_1.writeCodexBackup)(target.id, candidate);
1063
+ (0, atomic_1.atomicWriteJsonSync)(codexJournalPath(), {
1064
+ from: outgoing?.id ?? null,
1065
+ to: target.id,
1066
+ fromHash: loginHash(beforeSnapshot),
1067
+ toHash: loginHash(candidate),
1068
+ at: new Date().toISOString(),
1069
+ });
1070
+ }
1071
+ catch {
1072
+ return fail('The outgoing login or switch journal could not be saved and verified; live was left untouched.');
1073
+ }
1074
+ if (!(0, codexCredentialStore_1.codexStoreMatches)((0, codexCredentialStore_1.readCodexCredentialStore)(), beforeSnapshot))
1075
+ return fail('The live login changed after capture; the switch was deferred.');
1076
+ const profileStore = (0, codexCredentialStore_1.readCodexCredentialStore)(targetHome);
1077
+ if (!(0, codexCredentialStore_1.codexStoreMatches)(profileStore, targetBefore.snapshot))
1078
+ return fail('The target credential changed after selection; the switch was deferred.');
1079
+ if (profileStore.status === 'error' || profileStore.status === 'corrupt')
1080
+ return fail('The target profile is unreadable; the switch was deferred.');
1081
+ try {
1082
+ if (profileStore.status === 'ok')
1083
+ (0, codexCredentialStore_1.removeCodexProfileCredentials)(targetHome, profileStore.snapshot);
978
1084
  }
979
- const targetLastRefresh = (0, codexAuth_1.readLastRefresh)(targetAuthRaw, targetAuthPath);
980
- if (targetLastRefresh !== null && Date.now() - targetLastRefresh > codexAuth_1.STALE_AUTH_THRESHOLD_MS) {
981
- base.warnings.push(`Stored credentials for "${targetName}" have not been refreshed in over 8 days; codex may ask you to log in again.`);
1085
+ catch {
1086
+ return fail('The target copy could not be removed safely; the switch was deferred.');
982
1087
  }
983
- const restoreLiveFiles = () => {
984
- try {
985
- if (liveAuthRaw)
986
- (0, atomic_1.atomicWriteFileSync)(liveAuthPath, liveAuthRaw);
987
- else
988
- fs.rmSync(liveAuthPath, { force: true });
989
- if (liveLegacyRaw)
990
- (0, atomic_1.atomicWriteFileSync)(liveLegacyPath, liveLegacyRaw);
991
- else
992
- fs.rmSync(liveLegacyPath, { force: true });
993
- }
994
- catch {
995
- /* rollback is best-effort */
1088
+ const install = installIntoLiveCodexStore(candidate, target.id, 'switch', {
1089
+ before,
1090
+ liveAccountId: outgoing?.id ?? null,
1091
+ capturedOutgoing: Boolean(outgoing),
1092
+ scan: probes.scan,
1093
+ });
1094
+ const recover = (written) => {
1095
+ if (written) {
1096
+ installIntoLiveCodexStore(beforeSnapshot, outgoing?.id ?? target.id, 'rollback', {
1097
+ before: (0, codexCredentialStore_1.readCodexCredentialStore)(),
1098
+ liveAccountId: target.id,
1099
+ written,
1100
+ scan: probes.scan,
1101
+ });
996
1102
  }
1103
+ // Journal reconciliation never overwrites live and never restores a copy of its identity.
1104
+ resolveCodexJournal((0, accountSyncTypes_1.emptyProviderSyncReport)(), probes.scan);
1105
+ };
1106
+ if (!install.ok) {
997
1107
  try {
998
- (0, accountRegistry_1.setActiveSavedAccount)('codex', previousAccountId, { silent: true });
1108
+ recover(install.attempted);
999
1109
  }
1000
1110
  catch {
1001
- /* rollback is best-effort */
1111
+ /* journal and private backups survive */
1002
1112
  }
1003
- };
1004
- // Install.
1113
+ return fail(`${install.reason} Verified private backups were kept; run accounts doctor if recovery is pending.`, { verification: 'failed' });
1114
+ }
1005
1115
  try {
1006
- if (targetAuthRaw) {
1007
- (0, atomic_1.atomicWriteFileSync)(liveAuthPath, targetAuthRaw);
1008
- if (targetLegacyRaw)
1009
- (0, atomic_1.atomicWriteFileSync)(liveLegacyPath, targetLegacyRaw);
1010
- else if (liveLegacyRaw)
1011
- fs.rmSync(liveLegacyPath, { force: true });
1116
+ (0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
1117
+ }
1118
+ catch {
1119
+ try {
1120
+ recover(install.installed);
1012
1121
  }
1013
- else {
1014
- (0, atomic_1.atomicWriteFileSync)(liveLegacyPath, targetLegacyRaw);
1015
- if (liveAuthRaw)
1016
- fs.rmSync(liveAuthPath, { force: true });
1122
+ catch {
1123
+ /* journal survives */
1017
1124
  }
1125
+ return fail('The account registry could not be updated; recovery retained the verified backups.');
1018
1126
  }
1019
- catch (err) {
1020
- restoreLiveFiles();
1021
- return (0, accountSwitch_1.finishSwitchResult)({
1022
- ...base,
1023
- success: false,
1024
- error: `Failed to write Codex credentials: ${err}`,
1025
- });
1026
- }
1027
- // Verify by re-reading the live file.
1028
- const installed = (0, codexPaths_1.readFileOrNull)(targetAuthRaw ? liveAuthPath : liveLegacyPath);
1029
- const expected = targetAuthRaw ?? targetLegacyRaw;
1030
- const installedIdentity = targetAuthRaw ? (0, codexAuth_1.readAuthIdentityFromRaw)(installed) : null;
1031
- const verified = installed === expected &&
1032
- (!targetAuthRaw ||
1033
- (0, codexAuth_1.identitiesMatch)(installedIdentity, {
1034
- ...profileIdentity(target),
1035
- ...((0, codexAuth_1.readAuthIdentityFromRaw)(targetAuthRaw) ?? {}),
1036
- }) ||
1037
- installedIdentity?.authMode === 'api-key');
1038
- if (!verified) {
1039
- restoreLiveFiles();
1040
- return (0, accountSwitch_1.finishSwitchResult)({
1041
- ...base,
1042
- success: false,
1043
- verification: 'failed',
1044
- error: `The live Codex credential file did not reflect the switch to "${targetName}"; the previous login was restored.`,
1045
- });
1046
- }
1047
- // Pointer last.
1048
1127
  try {
1049
- (0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
1128
+ if (outgoing)
1129
+ storeCodexProfileAuthUnlocked(outgoing.id, beforeSnapshot.authRaw, beforeSnapshot.legacyRaw, 'switch');
1130
+ (0, accountHealth_1.writeCodexHealthSidecar)(target.id, candidate.authRaw, 'switch');
1131
+ clearCodexJournal();
1050
1132
  }
1051
- catch (err) {
1052
- restoreLiveFiles();
1053
- return (0, accountSwitch_1.finishSwitchResult)({
1054
- ...base,
1055
- success: false,
1056
- error: `Failed to update account registry: ${err}`,
1057
- });
1133
+ catch {
1134
+ base.warnings.push('The live switch succeeded; profile cleanup is pending and will retry on sync.');
1058
1135
  }
1059
1136
  let undoToken;
1060
1137
  try {
1061
- undoToken = (0, accountSwitch_1.writeLastSwitch)('codex', previousAccountId, target.id).token;
1138
+ undoToken = (0, accountSwitch_1.writeLastSwitch)('codex', outgoing?.id ?? null, target.id).token;
1062
1139
  }
1063
1140
  catch {
1064
1141
  base.warnings.push('The switch succeeded but could not be recorded for undo.');
@@ -1068,123 +1145,174 @@ function performCodexAuthSwapCore(target, options, probes) {
1068
1145
  verified: true,
1069
1146
  verification: 'store',
1070
1147
  undoToken,
1071
- hints: [`New codex sessions use ${target.email ?? targetName}.`],
1148
+ hints: [`New Codex sessions use ${target.label ?? target.email ?? target.id}.`],
1072
1149
  });
1073
1150
  }
1074
1151
  function switchToCodexAccount(profileId, options = {}) {
1075
- const target = listCodexAccounts().find((account) => account.id === profileId);
1076
- if (!target) {
1152
+ const target = listCodexAccounts().find((p) => p.id === profileId);
1153
+ if (!target)
1077
1154
  return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
1155
+ const scan = (0, processDetection_1.inspectRunningAccountConsumersSync)('codex');
1156
+ try {
1157
+ return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, { scan }));
1158
+ }
1159
+ catch {
1160
+ return (0, accountSwitch_1.switchFailure)('codex', profileId, 'The Codex switch could not finish; private backups and any pending journal were kept.');
1078
1161
  }
1079
- return performCodexAuthSwap(target, options);
1080
1162
  }
1081
- /** {@link switchToCodexAccount} without blocking the event loop on CLI probes. */
1082
1163
  async function switchToCodexAccountAsync(profileId, options = {}) {
1083
- const target = listCodexAccounts().find((account) => account.id === profileId);
1084
- if (!target) {
1164
+ const target = listCodexAccounts().find((p) => p.id === profileId);
1165
+ if (!target)
1085
1166
  return (0, accountSwitch_1.switchFailure)('codex', profileId, `Codex account ${profileId} not found.`);
1167
+ const scan = await (0, processDetection_1.inspectRunningAccountConsumers)('codex');
1168
+ let result;
1169
+ try {
1170
+ result = withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, options, { scan }));
1086
1171
  }
1087
- return performCodexAuthSwapAsync(target, options);
1088
- }
1089
- // ── One-time migration ───────────────────────────────────────────────────
1090
- /**
1091
- * The reconcile flow consults the keyring probe only when the migration has
1092
- * not run yet and the system home has no auth.json; skip the spawn otherwise.
1093
- */
1094
- function needsReconcileKeyringProbe() {
1095
- const markerPath = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', '.live-auth-migrated-v1');
1096
- if (fs.existsSync(markerPath))
1097
- return false;
1098
- return !fs.existsSync(path.join((0, codexPaths_1.getSystemCodexHome)(), 'auth.json'));
1172
+ catch {
1173
+ return (0, accountSwitch_1.switchFailure)('codex', profileId, 'The Codex switch could not finish; private backups and any pending journal were kept.');
1174
+ }
1175
+ if (result.success && options.verifyWithCli && !result.alreadyActive) {
1176
+ const status = await getCodexLoginStatusAsync((0, codexPaths_1.getSystemCodexHome)());
1177
+ if (status.loggedIn)
1178
+ result.verification = 'cli';
1179
+ else
1180
+ result = (0, accountSwitch_1.finishSwitchResult)({
1181
+ ...result,
1182
+ warnings: [
1183
+ ...result.warnings,
1184
+ 'Codex could not read the installed login; sign in again if prompted.',
1185
+ ],
1186
+ });
1187
+ }
1188
+ return result;
1099
1189
  }
1100
- // One-time migration for installs created when profile homes doubled as live
1101
- // CODEX_HOMEs: the active profile's auth.json may hold a fresher rotated
1102
- // refresh token than the system home. Best-effort: never throws.
1190
+ /** Legacy migration entry points now use the same guarded reconciliation as ordinary sync. */
1103
1191
  function reconcileCodexAuthState() {
1192
+ syncCodexLiveState('migration');
1193
+ }
1194
+ async function reconcileCodexAuthStateAsync() {
1195
+ await syncCodexLiveStateAsync('migration');
1196
+ }
1197
+ function codexKeepAliveBlockerUnlocked(id) {
1198
+ if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
1199
+ return 'unset CODEX_HOME before managing accounts; it names a Sidekick-owned home';
1200
+ const mode = (0, codexPaths_1.getCodexCredentialStoreMode)();
1201
+ if (mode !== 'file' && mode !== 'unknown')
1202
+ return 'live credential storage is not readable file mode';
1203
+ const profile = listCodexAccounts().find((p) => p.id === id);
1204
+ if (!profile)
1205
+ return 'account not found';
1206
+ const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
1207
+ if (live.status === 'error' || live.status === 'corrupt')
1208
+ return 'the live login could not be read';
1209
+ if (live.status === 'ok' && profileMatchesSnapshot(profile, live.snapshot))
1210
+ return 'live account; the CLI refreshes it in normal use';
1211
+ const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(id));
1212
+ if (store.status !== 'ok')
1213
+ return 'no readable profile copy to refresh (keep-alive never restores backups)';
1214
+ const fp = (0, codexAuth_1.codexRefreshFingerprint)(store.snapshot.authRaw);
1215
+ if (!fp)
1216
+ return 'no refreshable ChatGPT login';
1217
+ if (live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
1218
+ return 'same refresh token as the live login';
1219
+ if ((0, accountHealth_1.readHealthSidecar)('codex', id)?.source === 'live-sync')
1220
+ return 'copied from the live login; waits for a switch away from it';
1221
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
1222
+ if (backup.status === 'error' || backup.status === 'corrupt')
1223
+ return 'its private backup could not be read';
1224
+ if (backup.status === 'ok' &&
1225
+ (0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
1226
+ return 'its saved backup is newer than the profile copy';
1227
+ return null;
1228
+ }
1229
+ function getCodexKeepAliveBlocker(id) {
1104
1230
  try {
1105
- const systemKeyringLoggedIn = needsReconcileKeyringProbe()
1106
- ? getCodexLoginStatus((0, codexPaths_1.getSystemCodexHome)()).loggedIn
1107
- : false;
1108
- withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
1231
+ return withCodexAuthSwapLock(() => codexKeepAliveBlockerUnlocked(id));
1109
1232
  }
1110
1233
  catch {
1111
- // Reconciliation must never break startup — including a lock timeout.
1234
+ return 'could not acquire the account-switch lock';
1112
1235
  }
1113
1236
  }
1114
- /** {@link reconcileCodexAuthState} without blocking the event loop on CLI probes. */
1115
- async function reconcileCodexAuthStateAsync() {
1237
+ function beginCodexKeepAlive(id, leaseMs) {
1116
1238
  try {
1117
- const systemKeyringLoggedIn = needsReconcileKeyringProbe()
1118
- ? (await getCodexLoginStatusAsync((0, codexPaths_1.getSystemCodexHome)())).loggedIn
1119
- : false;
1120
- withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
1239
+ return withCodexAuthSwapLock(() => {
1240
+ const reason = codexKeepAliveBlockerUnlocked(id);
1241
+ if (reason)
1242
+ return { ok: false, reason };
1243
+ return (0, accountLease_1.acquireAccountLease)('codex', id, leaseMs)
1244
+ ? { ok: true }
1245
+ : { ok: false, reason: 'another keep-alive run holds this account' };
1246
+ });
1121
1247
  }
1122
1248
  catch {
1123
- // Reconciliation must never break startup — including a lock timeout.
1249
+ return { ok: false, reason: 'could not acquire the account-switch lock' };
1124
1250
  }
1125
1251
  }
1126
- function reconcileCodexAuthStateCore(probes) {
1252
+ function finishCodexKeepAlive(id) {
1127
1253
  try {
1128
- const markerPath = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', '.live-auth-migrated-v1');
1129
- if (fs.existsSync(markerPath))
1130
- return;
1131
- const writeMarker = () => (0, atomic_1.atomicWriteFileSync)(markerPath, new Date().toISOString() + '\n');
1132
- const active = getActiveCodexAccount();
1133
- if (!active) {
1134
- writeMarker();
1135
- return;
1136
- }
1137
- const profileHome = (0, codexPaths_1.getCodexProfileHome)(active.id);
1138
- const profileAuthPath = path.join(profileHome, 'auth.json');
1139
- const profileAuthRaw = (0, codexPaths_1.readFileOrNull)(profileAuthPath);
1140
- if (!profileAuthRaw) {
1141
- writeMarker();
1142
- return;
1143
- }
1144
- const systemHome = (0, codexPaths_1.getSystemCodexHome)();
1145
- const liveAuthPath = path.join(systemHome, 'auth.json');
1146
- const liveAuthRaw = (0, codexPaths_1.readFileOrNull)(liveAuthPath);
1147
- if (!liveAuthRaw) {
1148
- // No live credentials (account was added via isolated login and never
1149
- // promoted). Promote the active profile's copy unless codex is logged
1150
- // in through the OS keyring. The probe was resolved before the swap
1151
- // lock was taken — never spawn while holding it.
1152
- if (!probes.systemKeyringLoggedIn) {
1153
- (0, atomic_1.atomicWriteFileSync)(liveAuthPath, profileAuthRaw);
1154
- }
1155
- writeMarker();
1156
- return;
1157
- }
1158
- const liveIdentity = (0, codexAuth_1.readAuthIdentityFromRaw)(liveAuthRaw);
1159
- const profileIdentityValue = (0, codexAuth_1.readAuthIdentityFromRaw)(profileAuthRaw);
1160
- const sameIdentity = (0, codexAuth_1.identitiesMatch)(liveIdentity, profileIdentityValue);
1161
- if (sameIdentity) {
1162
- const liveRefresh = (0, codexAuth_1.readLastRefresh)(liveAuthRaw, liveAuthPath);
1163
- const profileRefresh = (0, codexAuth_1.readLastRefresh)(profileAuthRaw, profileAuthPath);
1164
- if (profileRefresh !== null && (liveRefresh === null || profileRefresh > liveRefresh)) {
1165
- // The profile copy was the live home under the old model and holds
1166
- // the valid rotated refresh token — promote it.
1167
- stashLiveCodexAuth(liveAuthRaw, null);
1168
- (0, atomic_1.atomicWriteFileSync)(liveAuthPath, profileAuthRaw);
1169
- }
1170
- else {
1171
- storeCodexProfileAuth(active.id, liveAuthRaw, null, 'migration');
1172
- }
1173
- }
1174
- else {
1175
- // The live credentials belong to a different account; the live state
1176
- // wins — point the registry at the matching saved profile if there is
1177
- // one, and refresh its backup.
1178
- const matching = findProfileForIdentity(liveIdentity);
1179
- if (matching && matching.id !== active.id) {
1180
- storeCodexProfileAuth(matching.id, liveAuthRaw, null, 'migration');
1181
- (0, accountRegistry_1.setActiveSavedAccount)('codex', matching.id);
1254
+ withCodexAuthSwapLock(() => {
1255
+ const profile = listCodexAccounts().find((p) => p.id === id);
1256
+ const store = (0, codexCredentialStore_1.readCodexCredentialStore)((0, codexPaths_1.getCodexProfileHome)(id));
1257
+ if (!profile || isLiveCodexProfile(profile) || store.status !== 'ok')
1258
+ return;
1259
+ if (!(0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw), profileIdentity(profile)))
1260
+ return;
1261
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
1262
+ if (backup.status === 'ok' &&
1263
+ (0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
1264
+ return;
1265
+ (0, codexCredentialStore_1.writeCodexBackup)(id, store.snapshot);
1266
+ (0, accountHealth_1.writeCodexHealthSidecar)(id, store.snapshot.authRaw, 'keepalive');
1267
+ });
1268
+ }
1269
+ finally {
1270
+ (0, accountLease_1.releaseAccountLease)('codex', id);
1271
+ }
1272
+ }
1273
+ function restoreCodexProfileFromBackup(id, options = {}) {
1274
+ try {
1275
+ return withCodexAuthSwapLock(() => {
1276
+ if ((0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)()))
1277
+ return 'none';
1278
+ const profile = listCodexAccounts().find((p) => p.id === id);
1279
+ if (!profile)
1280
+ return 'none';
1281
+ const live = (0, codexCredentialStore_1.readCodexCredentialStore)();
1282
+ if (live.status === 'error' || live.status === 'corrupt')
1283
+ return 'none';
1284
+ if (isLiveCodexProfile(profile))
1285
+ return 'live';
1286
+ const home = (0, codexPaths_1.getCodexProfileHome)(id);
1287
+ const store = (0, codexCredentialStore_1.readCodexCredentialStore)(home);
1288
+ if (store.status === 'ok') {
1289
+ const fp = (0, codexAuth_1.codexRefreshFingerprint)(store.snapshot.authRaw);
1290
+ if (fp && live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
1291
+ return 'none';
1292
+ if (!(0, codexAuth_1.identitiesMatch)((0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw), profileIdentity(profile)) &&
1293
+ (0, codexAuth_1.readAuthIdentityFromRaw)(store.snapshot.authRaw)?.authMode !== 'api-key' &&
1294
+ !(0, codexCredentialStore_1.isLegacyCodexLogin)(store.snapshot.legacyRaw))
1295
+ return 'none';
1296
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
1297
+ if (backup.status === 'ok' &&
1298
+ (0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, store.snapshot.authRaw) > 0)
1299
+ return 'none';
1300
+ return 'present';
1182
1301
  }
1183
- }
1184
- writeMarker();
1302
+ if (store.status !== 'absent' || options.restore === false)
1303
+ return 'none';
1304
+ const backup = (0, codexCredentialStore_1.readCodexBackup)(id);
1305
+ if (backup.status !== 'ok')
1306
+ return 'none';
1307
+ const fp = (0, codexAuth_1.codexRefreshFingerprint)(backup.snapshot.authRaw);
1308
+ if (fp && live.status === 'ok' && fp === (0, codexAuth_1.codexRefreshFingerprint)(live.snapshot.authRaw))
1309
+ return 'none';
1310
+ (0, codexCredentialStore_1.writeInactiveCodexCredentialStore)(home, backup.snapshot);
1311
+ return 'restored';
1312
+ });
1185
1313
  }
1186
1314
  catch {
1187
- // Reconciliation must never break startup.
1315
+ return 'none';
1188
1316
  }
1189
1317
  }
1190
1318
  function removeCodexAccount(profileId) {
@@ -1193,5 +1321,10 @@ function removeCodexAccount(profileId) {
1193
1321
  return { success: false, error: `Codex account ${profileId} not found.` };
1194
1322
  }
1195
1323
  fs.rmSync((0, codexPaths_1.getCodexProfileDir)(profileId), { recursive: true, force: true });
1324
+ fs.rmSync((0, codexPaths_1.getCodexBackupPath)(profileId), { force: true });
1325
+ fs.rmSync((0, codexPaths_1.getCodexBackupPath)(profileId, true), { force: true });
1196
1326
  return { success: true };
1197
1327
  }
1328
+ async function cleanupAbandonedCodexLoginsAsync(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now()) {
1329
+ return cleanupAbandonedCodexLogins(olderThanMs, now, await (0, processDetection_1.inspectRunningAccountConsumers)('codex'));
1330
+ }