codex-multi-auth 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +223 -98
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +30 -87
- package/config/{opencode-legacy.json → codex-legacy.json} +571 -571
- package/config/{opencode-modern.json → codex-modern.json} +241 -239
- package/config/{minimal-opencode.json → minimal-codex.json} +15 -13
- package/config/schema/config.schema.json +21 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -20
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/auth/auth.d.ts +1 -1
- package/dist/lib/auth/auth.js +1 -1
- package/dist/lib/cli.d.ts +1 -1
- package/dist/lib/cli.js +4 -4
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/codex-manager/settings-hub.d.ts +6 -0
- package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -0
- package/dist/lib/codex-manager/settings-hub.js +1786 -0
- package/dist/lib/codex-manager/settings-hub.js.map +1 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +5 -1637
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +20 -11
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-overflow.d.ts +1 -1
- package/dist/lib/context-overflow.js +11 -11
- package/dist/lib/oauth-success.html +1 -1
- package/dist/lib/prompts/codex-host-bridge.d.ts +19 -0
- package/dist/lib/prompts/codex-host-bridge.d.ts.map +1 -0
- package/dist/lib/prompts/{codex-opencode-bridge.js → codex-host-bridge.js} +143 -143
- package/dist/lib/prompts/codex-host-bridge.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +2 -2
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +3 -3
- package/dist/lib/prompts/host-codex-prompt.d.ts +25 -0
- package/dist/lib/prompts/host-codex-prompt.d.ts.map +1 -0
- package/dist/lib/prompts/{opencode-codex.js → host-codex-prompt.js} +136 -47
- package/dist/lib/prompts/host-codex-prompt.js.map +1 -0
- package/dist/lib/recovery/storage.d.ts +2 -2
- package/dist/lib/recovery/storage.js +2 -2
- package/dist/lib/recovery/types.d.ts +1 -1
- package/dist/lib/recovery/types.js +1 -1
- package/dist/lib/recovery.d.ts +1 -1
- package/dist/lib/recovery.d.ts.map +1 -1
- package/dist/lib/recovery.js +1 -4
- package/dist/lib/recovery.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +3 -3
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +6 -2
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/input-utils.d.ts +2 -2
- package/dist/lib/request/helpers/input-utils.d.ts.map +1 -1
- package/dist/lib/request/helpers/input-utils.js +14 -14
- package/dist/lib/request/helpers/input-utils.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts +2 -2
- package/dist/lib/request/helpers/model-map.js +2 -2
- package/dist/lib/request/request-transformer.d.ts +12 -12
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +23 -24
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/runtime-paths.d.ts +4 -4
- package/dist/lib/runtime-paths.d.ts.map +1 -1
- package/dist/lib/runtime-paths.js +57 -13
- package/dist/lib/runtime-paths.js.map +1 -1
- package/dist/lib/storage/paths.d.ts +11 -0
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +167 -5
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage.d.ts +1 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +290 -81
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/tools/hashline-tools.d.ts +1 -1
- package/dist/lib/tools/hashline-tools.d.ts.map +1 -1
- package/dist/lib/tools/hashline-tools.js +1 -1
- package/dist/lib/tools/hashline-tools.js.map +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/ui/copy.d.ts +6 -6
- package/dist/lib/ui/copy.js +6 -6
- package/dist/lib/ui/copy.js.map +1 -1
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -0
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -0
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -0
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -0
- package/node_modules/@codex-ai/plugin/package.json +9 -0
- package/package.json +30 -16
- package/scripts/audit-dev-allowlist.js +128 -128
- package/scripts/bench-format/{opencode.mjs → codex-host.mjs} +206 -205
- package/scripts/bench-format/hashline-v2.mjs +642 -642
- package/scripts/bench-format/models.mjs +111 -105
- package/scripts/bench-format/render.mjs +496 -496
- package/scripts/bench-format/stats.mjs +54 -54
- package/scripts/bench-format/tasks.mjs +151 -151
- package/scripts/benchmark-edit-formats.mjs +1162 -1161
- package/scripts/benchmark-render-dashboard.mjs +49 -49
- package/scripts/codex.js +40 -2
- package/scripts/copy-oauth-success.js +37 -37
- package/scripts/install-codex-auth-utils.js +49 -0
- package/scripts/{install-opencode-codex-auth.js → install-codex-auth.js} +220 -193
- package/scripts/repo-hygiene.js +320 -0
- package/scripts/test-all-models.sh +7 -7
- package/scripts/test-model-matrix.js +475 -423
- package/scripts/validate-model-map.sh +7 -7
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -0
- package/vendor/codex-ai-plugin/dist/index.js +2 -0
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -0
- package/vendor/codex-ai-plugin/dist/tool.js +29 -0
- package/vendor/codex-ai-plugin/package.json +9 -0
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -0
- package/vendor/codex-ai-sdk/dist/index.js +2 -0
- package/vendor/codex-ai-sdk/package.json +8 -0
- package/assets/opencode-logo-ornate-dark.svg +0 -18
- package/dist/lib/prompts/codex-opencode-bridge.d.ts +0 -19
- package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +0 -1
- package/dist/lib/prompts/codex-opencode-bridge.js.map +0 -1
- package/dist/lib/prompts/opencode-codex.d.ts +0 -25
- package/dist/lib/prompts/opencode-codex.d.ts.map +0 -1
- package/dist/lib/prompts/opencode-codex.js.map +0 -1
package/dist/lib/storage.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ACCOUNT_LIMITS } from "./constants.js";
|
|
|
5
5
|
import { createLogger } from "./logger.js";
|
|
6
6
|
import { MODEL_FAMILIES } from "./prompts/codex.js";
|
|
7
7
|
import { AnyAccountStorageSchema, getValidationErrors } from "./schemas.js";
|
|
8
|
-
import { getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, findProjectRoot, resolvePath } from "./storage/paths.js";
|
|
8
|
+
import { getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, findProjectRoot, resolvePath, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
|
|
9
9
|
import { migrateV1ToV3, } from "./storage/migrations.js";
|
|
10
10
|
const log = createLogger("storage");
|
|
11
11
|
const ACCOUNTS_FILE_NAME = "openai-codex-accounts.json";
|
|
@@ -13,6 +13,7 @@ const FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-flagged-accounts.json";
|
|
|
13
13
|
const LEGACY_FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-blocked-accounts.json";
|
|
14
14
|
const ACCOUNTS_BACKUP_SUFFIX = ".bak";
|
|
15
15
|
const ACCOUNTS_WAL_SUFFIX = ".wal";
|
|
16
|
+
const ACCOUNTS_BACKUP_HISTORY_DEPTH = 3;
|
|
16
17
|
const BACKUP_COPY_MAX_ATTEMPTS = 5;
|
|
17
18
|
const BACKUP_COPY_BASE_DELAY_MS = 10;
|
|
18
19
|
let storageBackupEnabled = true;
|
|
@@ -95,6 +96,7 @@ async function ensureGitignore(storagePath) {
|
|
|
95
96
|
}
|
|
96
97
|
let currentStoragePath = null;
|
|
97
98
|
let currentLegacyProjectStoragePath = null;
|
|
99
|
+
let currentLegacyWorktreeStoragePath = null;
|
|
98
100
|
let currentProjectRoot = null;
|
|
99
101
|
export function setStorageBackupEnabled(enabled) {
|
|
100
102
|
storageBackupEnabled = enabled;
|
|
@@ -102,9 +104,156 @@ export function setStorageBackupEnabled(enabled) {
|
|
|
102
104
|
function getAccountsBackupPath(path) {
|
|
103
105
|
return `${path}${ACCOUNTS_BACKUP_SUFFIX}`;
|
|
104
106
|
}
|
|
107
|
+
function getAccountsBackupPathAtIndex(path, index) {
|
|
108
|
+
if (index <= 0) {
|
|
109
|
+
return getAccountsBackupPath(path);
|
|
110
|
+
}
|
|
111
|
+
return `${path}${ACCOUNTS_BACKUP_SUFFIX}.${index}`;
|
|
112
|
+
}
|
|
113
|
+
function getAccountsBackupRecoveryCandidates(path) {
|
|
114
|
+
const candidates = [];
|
|
115
|
+
for (let i = 0; i < ACCOUNTS_BACKUP_HISTORY_DEPTH; i += 1) {
|
|
116
|
+
candidates.push(getAccountsBackupPathAtIndex(path, i));
|
|
117
|
+
}
|
|
118
|
+
return candidates;
|
|
119
|
+
}
|
|
105
120
|
function getAccountsWalPath(path) {
|
|
106
121
|
return `${path}${ACCOUNTS_WAL_SUFFIX}`;
|
|
107
122
|
}
|
|
123
|
+
async function copyFileWithRetry(sourcePath, destinationPath, options) {
|
|
124
|
+
const allowMissingSource = options?.allowMissingSource ?? false;
|
|
125
|
+
for (let attempt = 0; attempt < BACKUP_COPY_MAX_ATTEMPTS; attempt += 1) {
|
|
126
|
+
try {
|
|
127
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const code = error.code;
|
|
132
|
+
if (allowMissingSource && code === "ENOENT") {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const canRetry = (code === "EPERM" || code === "EBUSY") &&
|
|
136
|
+
attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS;
|
|
137
|
+
if (canRetry) {
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, BACKUP_COPY_BASE_DELAY_MS * 2 ** attempt));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function renameFileWithRetry(sourcePath, destinationPath) {
|
|
146
|
+
for (let attempt = 0; attempt < BACKUP_COPY_MAX_ATTEMPTS; attempt += 1) {
|
|
147
|
+
try {
|
|
148
|
+
await fs.rename(sourcePath, destinationPath);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const code = error.code;
|
|
153
|
+
const canRetry = (code === "EPERM" || code === "EBUSY" || code === "EAGAIN") &&
|
|
154
|
+
attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS;
|
|
155
|
+
if (!canRetry) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
const jitterMs = Math.floor(Math.random() * BACKUP_COPY_BASE_DELAY_MS);
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, BACKUP_COPY_BASE_DELAY_MS * 2 ** attempt + jitterMs));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function createRotatingAccountsBackup(path) {
|
|
164
|
+
const candidates = getAccountsBackupRecoveryCandidates(path);
|
|
165
|
+
const rotationNonce = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
166
|
+
const stagedWrites = [];
|
|
167
|
+
const buildStagedPath = (targetPath, label) => `${targetPath}.rotate.${rotationNonce}.${label}.tmp`;
|
|
168
|
+
try {
|
|
169
|
+
for (let i = candidates.length - 1; i > 0; i -= 1) {
|
|
170
|
+
const previousPath = candidates[i - 1];
|
|
171
|
+
const currentPath = candidates[i];
|
|
172
|
+
if (!previousPath || !currentPath || !existsSync(previousPath)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const stagedPath = buildStagedPath(currentPath, `slot-${i}`);
|
|
176
|
+
await copyFileWithRetry(previousPath, stagedPath, { allowMissingSource: true });
|
|
177
|
+
if (existsSync(stagedPath)) {
|
|
178
|
+
stagedWrites.push({ targetPath: currentPath, stagedPath });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const latestBackupPath = candidates[0];
|
|
182
|
+
if (!latestBackupPath) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const latestStagedPath = buildStagedPath(latestBackupPath, "latest");
|
|
186
|
+
await copyFileWithRetry(path, latestStagedPath);
|
|
187
|
+
if (existsSync(latestStagedPath)) {
|
|
188
|
+
stagedWrites.push({ targetPath: latestBackupPath, stagedPath: latestStagedPath });
|
|
189
|
+
}
|
|
190
|
+
for (const stagedWrite of stagedWrites) {
|
|
191
|
+
await renameFileWithRetry(stagedWrite.stagedPath, stagedWrite.targetPath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
for (const stagedWrite of stagedWrites) {
|
|
196
|
+
if (!existsSync(stagedWrite.stagedPath)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
await fs.unlink(stagedWrite.stagedPath);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Best effort cleanup for staged rotation artifacts.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function isRotatingBackupTempArtifact(storagePath, candidatePath) {
|
|
209
|
+
const backupPrefix = `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`;
|
|
210
|
+
if (!candidatePath.startsWith(backupPrefix) || !candidatePath.endsWith(".tmp")) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const suffix = candidatePath.slice(backupPrefix.length);
|
|
214
|
+
const rotateSeparatorIndex = suffix.indexOf(".rotate.");
|
|
215
|
+
if (rotateSeparatorIndex === -1) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const backupIndexSuffix = suffix.slice(0, rotateSeparatorIndex);
|
|
219
|
+
if (backupIndexSuffix.length > 0 && !/^\.\d+$/.test(backupIndexSuffix)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
async function cleanupStaleRotatingBackupArtifacts(path) {
|
|
225
|
+
const directoryPath = dirname(path);
|
|
226
|
+
try {
|
|
227
|
+
const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
228
|
+
const staleArtifacts = directoryEntries
|
|
229
|
+
.filter((entry) => entry.isFile())
|
|
230
|
+
.map((entry) => join(directoryPath, entry.name))
|
|
231
|
+
.filter((entryPath) => isRotatingBackupTempArtifact(path, entryPath));
|
|
232
|
+
for (const staleArtifactPath of staleArtifacts) {
|
|
233
|
+
try {
|
|
234
|
+
await fs.unlink(staleArtifactPath);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const code = error.code;
|
|
238
|
+
if (code !== "ENOENT") {
|
|
239
|
+
log.warn("Failed to remove stale rotating backup artifact", {
|
|
240
|
+
path: staleArtifactPath,
|
|
241
|
+
error: String(error),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const code = error.code;
|
|
249
|
+
if (code !== "ENOENT") {
|
|
250
|
+
log.warn("Failed to scan for stale rotating backup artifacts", {
|
|
251
|
+
path,
|
|
252
|
+
error: String(error),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
108
257
|
function computeSha256(value) {
|
|
109
258
|
return createHash("sha256").update(value).digest("hex");
|
|
110
259
|
}
|
|
@@ -115,24 +264,31 @@ export function setStoragePath(projectPath) {
|
|
|
115
264
|
if (!projectPath) {
|
|
116
265
|
currentStoragePath = null;
|
|
117
266
|
currentLegacyProjectStoragePath = null;
|
|
267
|
+
currentLegacyWorktreeStoragePath = null;
|
|
118
268
|
currentProjectRoot = null;
|
|
119
269
|
return;
|
|
120
270
|
}
|
|
121
271
|
const projectRoot = findProjectRoot(projectPath);
|
|
122
272
|
if (projectRoot) {
|
|
123
273
|
currentProjectRoot = projectRoot;
|
|
124
|
-
|
|
274
|
+
const identityRoot = resolveProjectStorageIdentityRoot(projectRoot);
|
|
275
|
+
currentStoragePath = join(getProjectGlobalConfigDir(identityRoot), ACCOUNTS_FILE_NAME);
|
|
125
276
|
currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
|
|
277
|
+
const previousWorktreeScopedPath = join(getProjectGlobalConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
|
|
278
|
+
currentLegacyWorktreeStoragePath =
|
|
279
|
+
previousWorktreeScopedPath !== currentStoragePath ? previousWorktreeScopedPath : null;
|
|
126
280
|
}
|
|
127
281
|
else {
|
|
128
282
|
currentStoragePath = null;
|
|
129
283
|
currentLegacyProjectStoragePath = null;
|
|
284
|
+
currentLegacyWorktreeStoragePath = null;
|
|
130
285
|
currentProjectRoot = null;
|
|
131
286
|
}
|
|
132
287
|
}
|
|
133
288
|
export function setStoragePathDirect(path) {
|
|
134
289
|
currentStoragePath = path;
|
|
135
290
|
currentLegacyProjectStoragePath = null;
|
|
291
|
+
currentLegacyWorktreeStoragePath = null;
|
|
136
292
|
currentProjectRoot = null;
|
|
137
293
|
}
|
|
138
294
|
/**
|
|
@@ -152,50 +308,108 @@ function getLegacyFlaggedAccountsPath() {
|
|
|
152
308
|
return join(dirname(getStoragePath()), LEGACY_FLAGGED_ACCOUNTS_FILE_NAME);
|
|
153
309
|
}
|
|
154
310
|
async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
|
|
155
|
-
if (!currentStoragePath
|
|
156
|
-
!currentLegacyProjectStoragePath ||
|
|
157
|
-
currentLegacyProjectStoragePath === currentStoragePath ||
|
|
158
|
-
!existsSync(currentLegacyProjectStoragePath)) {
|
|
311
|
+
if (!currentStoragePath) {
|
|
159
312
|
return null;
|
|
160
313
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
314
|
+
const candidatePaths = [currentLegacyWorktreeStoragePath, currentLegacyProjectStoragePath]
|
|
315
|
+
.filter((path) => typeof path === "string" && path.length > 0 && path !== currentStoragePath)
|
|
316
|
+
.filter((path, index, all) => all.indexOf(path) === index);
|
|
317
|
+
if (candidatePaths.length === 0) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const existingCandidatePaths = candidatePaths.filter((legacyPath) => existsSync(legacyPath));
|
|
321
|
+
if (existingCandidatePaths.length === 0) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
let targetStorage = await loadNormalizedStorageFromPath(currentStoragePath, "current account storage");
|
|
325
|
+
let migrated = false;
|
|
326
|
+
for (const legacyPath of existingCandidatePaths) {
|
|
327
|
+
const legacyStorage = await loadNormalizedStorageFromPath(legacyPath, "legacy account storage");
|
|
328
|
+
if (!legacyStorage) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const mergedStorage = mergeStorageForMigration(targetStorage, legacyStorage);
|
|
332
|
+
const fallbackStorage = targetStorage ?? legacyStorage;
|
|
333
|
+
try {
|
|
334
|
+
await persist(mergedStorage);
|
|
335
|
+
targetStorage = mergedStorage;
|
|
336
|
+
migrated = true;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
targetStorage = fallbackStorage;
|
|
340
|
+
log.warn("Failed to persist migrated account storage", {
|
|
341
|
+
from: legacyPath,
|
|
342
|
+
to: currentStoragePath,
|
|
343
|
+
error: String(error),
|
|
344
|
+
});
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
168
347
|
try {
|
|
169
|
-
await fs.unlink(
|
|
170
|
-
log.info("Removed legacy
|
|
171
|
-
path:
|
|
348
|
+
await fs.unlink(legacyPath);
|
|
349
|
+
log.info("Removed legacy account storage file after migration", {
|
|
350
|
+
path: legacyPath,
|
|
172
351
|
});
|
|
173
352
|
}
|
|
174
353
|
catch (unlinkError) {
|
|
175
354
|
const code = unlinkError.code;
|
|
176
355
|
if (code !== "ENOENT") {
|
|
177
|
-
log.warn("Failed to remove legacy
|
|
178
|
-
path:
|
|
356
|
+
log.warn("Failed to remove legacy account storage file after migration", {
|
|
357
|
+
path: legacyPath,
|
|
179
358
|
error: String(unlinkError),
|
|
180
359
|
});
|
|
181
360
|
}
|
|
182
361
|
}
|
|
183
362
|
log.info("Migrated legacy project account storage", {
|
|
184
|
-
from:
|
|
363
|
+
from: legacyPath,
|
|
185
364
|
to: currentStoragePath,
|
|
186
|
-
accounts:
|
|
365
|
+
accounts: mergedStorage.accounts.length,
|
|
187
366
|
});
|
|
367
|
+
}
|
|
368
|
+
if (migrated) {
|
|
369
|
+
return targetStorage;
|
|
370
|
+
}
|
|
371
|
+
if (targetStorage && !existsSync(currentStoragePath)) {
|
|
372
|
+
return targetStorage;
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
async function loadNormalizedStorageFromPath(path, label) {
|
|
377
|
+
try {
|
|
378
|
+
const { normalized, schemaErrors } = await loadAccountsFromPath(path);
|
|
379
|
+
if (schemaErrors.length > 0) {
|
|
380
|
+
log.warn(`${label} schema validation warnings`, {
|
|
381
|
+
path,
|
|
382
|
+
errors: schemaErrors.slice(0, 5),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
188
385
|
return normalized;
|
|
189
386
|
}
|
|
190
387
|
catch (error) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
to
|
|
194
|
-
|
|
195
|
-
|
|
388
|
+
const code = error.code;
|
|
389
|
+
if (code !== "ENOENT") {
|
|
390
|
+
log.warn(`Failed to load ${label}`, {
|
|
391
|
+
path,
|
|
392
|
+
error: String(error),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
196
395
|
return null;
|
|
197
396
|
}
|
|
198
397
|
}
|
|
398
|
+
function mergeStorageForMigration(current, incoming) {
|
|
399
|
+
if (!current) {
|
|
400
|
+
return incoming;
|
|
401
|
+
}
|
|
402
|
+
const merged = normalizeAccountStorage({
|
|
403
|
+
version: 3,
|
|
404
|
+
activeIndex: current.activeIndex,
|
|
405
|
+
activeIndexByFamily: current.activeIndexByFamily,
|
|
406
|
+
accounts: [...current.accounts, ...incoming.accounts],
|
|
407
|
+
});
|
|
408
|
+
if (!merged) {
|
|
409
|
+
return current;
|
|
410
|
+
}
|
|
411
|
+
return merged;
|
|
412
|
+
}
|
|
199
413
|
function selectNewestAccount(current, candidate) {
|
|
200
414
|
if (!current)
|
|
201
415
|
return candidate;
|
|
@@ -256,6 +470,14 @@ export function deduplicateAccounts(accounts) {
|
|
|
256
470
|
* @param accounts - Array of accounts to deduplicate
|
|
257
471
|
* @returns New array with email duplicates removed
|
|
258
472
|
*/
|
|
473
|
+
export function normalizeEmailKey(email) {
|
|
474
|
+
if (!email)
|
|
475
|
+
return undefined;
|
|
476
|
+
const trimmed = email.trim();
|
|
477
|
+
if (!trimmed)
|
|
478
|
+
return undefined;
|
|
479
|
+
return trimmed.toLowerCase();
|
|
480
|
+
}
|
|
259
481
|
export function deduplicateAccountsByEmail(accounts) {
|
|
260
482
|
const emailToNewestIndex = new Map();
|
|
261
483
|
const indicesToKeep = new Set();
|
|
@@ -263,7 +485,7 @@ export function deduplicateAccountsByEmail(accounts) {
|
|
|
263
485
|
const account = accounts[i];
|
|
264
486
|
if (!account)
|
|
265
487
|
continue;
|
|
266
|
-
const email = account.email
|
|
488
|
+
const email = normalizeEmailKey(account.email);
|
|
267
489
|
if (!email) {
|
|
268
490
|
indicesToKeep.add(i);
|
|
269
491
|
continue;
|
|
@@ -447,8 +669,12 @@ async function loadAccountsFromJournal(path) {
|
|
|
447
669
|
}
|
|
448
670
|
}
|
|
449
671
|
async function loadAccountsInternal(persistMigration) {
|
|
672
|
+
const path = getStoragePath();
|
|
673
|
+
await cleanupStaleRotatingBackupArtifacts(path);
|
|
674
|
+
const migratedLegacyStorage = persistMigration
|
|
675
|
+
? await migrateLegacyProjectStorageIfNeeded(persistMigration)
|
|
676
|
+
: null;
|
|
450
677
|
try {
|
|
451
|
-
const path = getStoragePath();
|
|
452
678
|
const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path);
|
|
453
679
|
if (schemaErrors.length > 0) {
|
|
454
680
|
log.warn("Account storage schema validation warnings", { errors: schemaErrors.slice(0, 5) });
|
|
@@ -468,11 +694,8 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
468
694
|
}
|
|
469
695
|
catch (error) {
|
|
470
696
|
const code = error.code;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const migrated = await migrateLegacyProjectStorageIfNeeded(persistMigration);
|
|
474
|
-
if (migrated)
|
|
475
|
-
return migrated;
|
|
697
|
+
if (code === "ENOENT" && migratedLegacyStorage) {
|
|
698
|
+
return migratedLegacyStorage;
|
|
476
699
|
}
|
|
477
700
|
const recoveredFromWal = await loadAccountsFromJournal(path);
|
|
478
701
|
if (recoveredFromWal) {
|
|
@@ -490,38 +713,40 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
490
713
|
return recoveredFromWal;
|
|
491
714
|
}
|
|
492
715
|
if (storageBackupEnabled) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
716
|
+
const backupCandidates = getAccountsBackupRecoveryCandidates(path);
|
|
717
|
+
for (const backupPath of backupCandidates) {
|
|
718
|
+
try {
|
|
719
|
+
const backup = await loadAccountsFromPath(backupPath);
|
|
720
|
+
if (backup.schemaErrors.length > 0) {
|
|
721
|
+
log.warn("Backup account storage schema validation warnings", {
|
|
722
|
+
path: backupPath,
|
|
723
|
+
errors: backup.schemaErrors.slice(0, 5),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
if (backup.normalized) {
|
|
727
|
+
log.warn("Recovered account storage from backup file", { path, backupPath });
|
|
728
|
+
if (persistMigration) {
|
|
729
|
+
try {
|
|
730
|
+
await persistMigration(backup.normalized);
|
|
731
|
+
}
|
|
732
|
+
catch (persistError) {
|
|
733
|
+
log.warn("Failed to persist recovered backup storage", {
|
|
734
|
+
path,
|
|
735
|
+
error: String(persistError),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
513
738
|
}
|
|
739
|
+
return backup.normalized;
|
|
514
740
|
}
|
|
515
|
-
return backup.normalized;
|
|
516
741
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
742
|
+
catch (backupError) {
|
|
743
|
+
const backupCode = backupError.code;
|
|
744
|
+
if (backupCode !== "ENOENT") {
|
|
745
|
+
log.warn("Failed to load backup account storage", {
|
|
746
|
+
path: backupPath,
|
|
747
|
+
error: String(backupError),
|
|
748
|
+
});
|
|
749
|
+
}
|
|
525
750
|
}
|
|
526
751
|
}
|
|
527
752
|
}
|
|
@@ -540,29 +765,13 @@ async function saveAccountsUnlocked(storage) {
|
|
|
540
765
|
await fs.mkdir(dirname(path), { recursive: true });
|
|
541
766
|
await ensureGitignore(path);
|
|
542
767
|
if (storageBackupEnabled && existsSync(path)) {
|
|
543
|
-
const backupPath = getAccountsBackupPath(path);
|
|
544
768
|
try {
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
await fs.copyFile(path, backupPath);
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
catch (backupError) {
|
|
551
|
-
const code = backupError.code;
|
|
552
|
-
const canRetry = (code === "EPERM" || code === "EBUSY") &&
|
|
553
|
-
attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS;
|
|
554
|
-
if (canRetry) {
|
|
555
|
-
await new Promise((resolve) => setTimeout(resolve, BACKUP_COPY_BASE_DELAY_MS * 2 ** attempt));
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
throw backupError;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
769
|
+
await createRotatingAccountsBackup(path);
|
|
561
770
|
}
|
|
562
771
|
catch (backupError) {
|
|
563
772
|
log.warn("Failed to create account storage backup", {
|
|
564
773
|
path,
|
|
565
|
-
backupPath,
|
|
774
|
+
backupPath: getAccountsBackupPath(path),
|
|
566
775
|
error: String(backupError),
|
|
567
776
|
});
|
|
568
777
|
}
|
|
@@ -657,7 +866,7 @@ export async function clearAccounts() {
|
|
|
657
866
|
return withStorageLock(async () => {
|
|
658
867
|
const path = getStoragePath();
|
|
659
868
|
const walPath = getAccountsWalPath(path);
|
|
660
|
-
const
|
|
869
|
+
const backupPaths = getAccountsBackupRecoveryCandidates(path);
|
|
661
870
|
const clearPath = async (targetPath) => {
|
|
662
871
|
try {
|
|
663
872
|
await fs.unlink(targetPath);
|
|
@@ -673,7 +882,7 @@ export async function clearAccounts() {
|
|
|
673
882
|
}
|
|
674
883
|
};
|
|
675
884
|
try {
|
|
676
|
-
await Promise.all([clearPath(path), clearPath(walPath), clearPath
|
|
885
|
+
await Promise.all([clearPath(path), clearPath(walPath), ...backupPaths.map(clearPath)]);
|
|
677
886
|
}
|
|
678
887
|
catch {
|
|
679
888
|
// Individual path cleanup is already best-effort with per-artifact logging.
|