@4yi-dev/cli 0.1.15 → 0.1.17
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/README.md +3 -1
- package/bin/4yi.mjs +49 -3
- package/package.json +1 -1
- package/src/codex-migrate.mjs +145 -0
- package/src/connect.mjs +51 -21
package/README.md
CHANGED
|
@@ -39,10 +39,12 @@ On macOS, `4yi connect claude` also configures an installed Claude Desktop throu
|
|
|
39
39
|
|
|
40
40
|
`--scope project` changes only the project's Claude Code settings and never changes the global Claude Desktop profile.
|
|
41
41
|
|
|
42
|
-
Codex CLI and Codex App share the same Codex home on native Windows (`%USERPROFILE%\\.codex`). After `4yi connect codex`, finish active App tasks, fully quit and reopen the App, and start a new chat so its embedded Codex process loads the new provider. Microsoft Store/AppX installations may not expose a conventional executable path, but that detection does not gate the shared configuration update.
|
|
42
|
+
Codex CLI and Codex App share the same Codex home on native Windows (`%USERPROFILE%\\.codex`). After `4yi connect codex`, finish active App tasks, fully quit and reopen the App, and start a new chat so its embedded Codex process loads the new provider. Microsoft Store/AppX installations may not expose a conventional executable path, but that detection does not gate the shared configuration update. While the external 4YI provider supplies authentication, Codex App may hide its `Log out` action because there is no App-managed provider credential to clear. To switch back to OpenAI, run `4yi restore codex`, fully quit and reopen the App, and start a new chat.
|
|
43
43
|
|
|
44
44
|
Existing Codex tasks keep the provider they were created with. Run `4yi migrate codex`, choose a recent task by number, and 4YI creates a new task with the same history and project using your Plan's default model. The original task is not modified. Scripts can use `--thread <id>` and optionally `--model <id>`.
|
|
45
45
|
|
|
46
|
+
`4yi restore codex` restores the protected OpenAI configuration, then detects tasks that still belong to the 4YI provider. In an interactive terminal, choose a recent task by number, press Enter for the newest task, enter `a` for every displayed task, or enter `s` to skip. Codex copies each selected task to a new OpenAI task without modifying the original. For scripts, use `4yi restore codex --thread <id>` or `4yi restore codex --migrate-tasks all`; optionally pass `--model <id>` to override the OpenAI default model. Fully quit and reopen Codex App after the command completes.
|
|
47
|
+
|
|
46
48
|
The Codex provider uses the official command-backed authentication configuration. Its helper reads the current token from `~/.4yi/config.json`; `config.toml` and the model catalog do not contain the bearer token.
|
|
47
49
|
|
|
48
50
|
For local development:
|
package/bin/4yi.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { login, loadSession, clearSession } from "../src/auth.mjs";
|
|
3
3
|
import { runCode } from "../src/opencode.mjs";
|
|
4
4
|
import { connect, connectionStatus, prepareConnectionTools, restoreConnection } from "../src/connect.mjs";
|
|
5
|
-
import { migrateCodexTask } from "../src/codex-migrate.mjs";
|
|
5
|
+
import { migrateCodexTask, restoreCodexTasksToOpenAI } from "../src/codex-migrate.mjs";
|
|
6
6
|
|
|
7
7
|
const command = process.argv[2] || "help";
|
|
8
8
|
|
|
@@ -18,6 +18,10 @@ if (command === "help" || command === "--help" || command === "-h") {
|
|
|
18
18
|
console.log(" --model X choose a model (defaults to your Plan model)");
|
|
19
19
|
console.log(" 4yi status [claude|codex|all] inspect the current connection");
|
|
20
20
|
console.log(" 4yi restore <claude|codex|all> restore the latest protected config");
|
|
21
|
+
console.log(" Codex: interactively copy 4YI tasks to OpenAI after restoring");
|
|
22
|
+
console.log(" --thread ID copy one specific 4YI task to OpenAI");
|
|
23
|
+
console.log(" --migrate-tasks all copy every detected 4YI task to OpenAI");
|
|
24
|
+
console.log(" --model X use a specific OpenAI model for copied tasks");
|
|
21
25
|
process.exit(0);
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -66,6 +70,44 @@ function parseMigrateArgs(args) {
|
|
|
66
70
|
return { threadId, model };
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
function parseRestoreArgs(args) {
|
|
74
|
+
let target = "all";
|
|
75
|
+
let threadId;
|
|
76
|
+
let migrateTasks;
|
|
77
|
+
let model;
|
|
78
|
+
let targetSeen = false;
|
|
79
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
80
|
+
const arg = args[i];
|
|
81
|
+
if (!arg.startsWith("-") && !targetSeen) {
|
|
82
|
+
target = arg;
|
|
83
|
+
targetSeen = true;
|
|
84
|
+
} else if (arg === "--thread") {
|
|
85
|
+
threadId = args[++i];
|
|
86
|
+
if (!threadId || threadId.startsWith("-")) throw new Error("--thread requires a task id.");
|
|
87
|
+
} else if (arg.startsWith("--thread=")) {
|
|
88
|
+
threadId = arg.slice("--thread=".length);
|
|
89
|
+
} else if (arg === "--migrate-tasks") {
|
|
90
|
+
migrateTasks = args[++i];
|
|
91
|
+
if (!migrateTasks || migrateTasks.startsWith("-")) throw new Error("--migrate-tasks requires `all`.");
|
|
92
|
+
} else if (arg.startsWith("--migrate-tasks=")) {
|
|
93
|
+
migrateTasks = arg.slice("--migrate-tasks=".length);
|
|
94
|
+
} else if (arg === "--model" || arg === "-m") {
|
|
95
|
+
model = args[++i];
|
|
96
|
+
if (!model || model.startsWith("-")) throw new Error("--model requires a model id.");
|
|
97
|
+
} else if (arg.startsWith("--model=")) {
|
|
98
|
+
model = arg.slice("--model=".length);
|
|
99
|
+
} else {
|
|
100
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (migrateTasks && migrateTasks !== "all") throw new Error("--migrate-tasks currently supports only `all`.");
|
|
104
|
+
if (threadId && migrateTasks) throw new Error("Use either --thread or --migrate-tasks all, not both.");
|
|
105
|
+
if ((threadId || migrateTasks || model) && !new Set(["codex", "all"]).has(String(target).toLowerCase())) {
|
|
106
|
+
throw new Error("Codex task migration options require `4yi restore codex` or `4yi restore all`.");
|
|
107
|
+
}
|
|
108
|
+
return { target, threadId, migrateTasks, model };
|
|
109
|
+
}
|
|
110
|
+
|
|
69
111
|
/** Split out `--model <id>` / `--model=<id>` / `-m <id>`; the rest pass through to OpenCode. */
|
|
70
112
|
function parseCodeArgs(args) {
|
|
71
113
|
let preferredModel = null;
|
|
@@ -122,8 +164,12 @@ try {
|
|
|
122
164
|
const { target, scope } = parseConnectArgs(process.argv.slice(3));
|
|
123
165
|
connectionStatus({ target, scope });
|
|
124
166
|
} else if (command === "restore") {
|
|
125
|
-
const
|
|
126
|
-
restoreConnection({ target });
|
|
167
|
+
const restoreArgs = parseRestoreArgs(process.argv.slice(3));
|
|
168
|
+
const restored = restoreConnection({ target: restoreArgs.target });
|
|
169
|
+
if (restored.restoredCodex) {
|
|
170
|
+
await restoreCodexTasksToOpenAI(restoreArgs);
|
|
171
|
+
console.log("Codex 已恢复为 OpenAI。请完全退出并重新打开 Codex App,然后打开复制后的任务或创建新任务。");
|
|
172
|
+
}
|
|
127
173
|
} else {
|
|
128
174
|
console.error(`Unknown command: ${command}`);
|
|
129
175
|
process.exit(1);
|
package/package.json
CHANGED
package/src/codex-migrate.mjs
CHANGED
|
@@ -136,6 +136,20 @@ function mergeCandidateThreads(serverThreads, sessionThreads, limit) {
|
|
|
136
136
|
.slice(0, limit);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function mergeProviderThreads(serverThreads, sessionThreads, provider, limit = Number.POSITIVE_INFINITY) {
|
|
140
|
+
const threads = new Map();
|
|
141
|
+
for (const thread of sessionThreads) if (thread?.id) threads.set(thread.id, thread);
|
|
142
|
+
for (const thread of serverThreads) if (thread?.id) threads.set(thread.id, { ...threads.get(thread.id), ...thread });
|
|
143
|
+
return [...threads.values()]
|
|
144
|
+
.filter((thread) => thread.modelProvider === provider && !thread.ephemeral)
|
|
145
|
+
.sort((left, right) => {
|
|
146
|
+
const leftTime = left.recencyAt || left.updatedAt || left.createdAt || 0;
|
|
147
|
+
const rightTime = right.recencyAt || right.updatedAt || right.createdAt || 0;
|
|
148
|
+
return rightTime - leftTime;
|
|
149
|
+
})
|
|
150
|
+
.slice(0, limit);
|
|
151
|
+
}
|
|
152
|
+
|
|
139
153
|
export function describeCodexThread(thread, { now = Date.now() } = {}) {
|
|
140
154
|
const title = oneLine(thread.name || thread.preview, "未命名任务");
|
|
141
155
|
const project = path.basename(String(thread.cwd || "")) || String(thread.cwd || "未知项目");
|
|
@@ -173,6 +187,35 @@ export async function selectCodexThread(threads, {
|
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
189
|
|
|
190
|
+
export async function selectCodexRestoreThreads(threads, {
|
|
191
|
+
input = process.stdin,
|
|
192
|
+
output = process.stdout,
|
|
193
|
+
now = Date.now(),
|
|
194
|
+
} = {}) {
|
|
195
|
+
if (!input?.isTTY || !output?.isTTY) return null;
|
|
196
|
+
|
|
197
|
+
output.write("\n发现仍绑定 4YI Gateway 的 Codex 任务。选择要复制到 OpenAI 的任务:\n\n");
|
|
198
|
+
threads.forEach((thread, index) => {
|
|
199
|
+
const item = describeCodexThread(thread, { now });
|
|
200
|
+
output.write(`${index + 1}. ${item.title}\n ${item.project} · ${item.age}\n`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const readline = createInterface({ input, output });
|
|
204
|
+
try {
|
|
205
|
+
while (true) {
|
|
206
|
+
const answer = await readline.question("\n输入序号(默认 1,a 复制以上全部,s 跳过):");
|
|
207
|
+
const value = answer.trim().toLowerCase();
|
|
208
|
+
if (value === "s" || value === "skip" || value === "q" || value === "quit") return [];
|
|
209
|
+
if (value === "a" || value === "all") return threads;
|
|
210
|
+
const index = value === "" ? 0 : Number(value) - 1;
|
|
211
|
+
if (Number.isInteger(index) && index >= 0 && index < threads.length) return [threads[index]];
|
|
212
|
+
output.write(`请输入 1-${threads.length} 之间的序号、a 或 s。\n`);
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
readline.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
176
219
|
export class CodexAppServerClient {
|
|
177
220
|
constructor({
|
|
178
221
|
command = executableName("codex"),
|
|
@@ -292,6 +335,105 @@ async function loadDefaultCodexModel(session) {
|
|
|
292
335
|
return ids.includes(response.default_model) ? response.default_model : ids[0];
|
|
293
336
|
}
|
|
294
337
|
|
|
338
|
+
async function loadDefaultOpenAIModel(appServer) {
|
|
339
|
+
const response = await appServer.request("model/list", { limit: 100, includeHidden: false });
|
|
340
|
+
const models = response?.data || [];
|
|
341
|
+
const selected = models.find((model) => model?.isDefault) || models[0];
|
|
342
|
+
const id = selected?.id || selected?.model;
|
|
343
|
+
if (!id) throw new Error("Codex did not report an available OpenAI model after restore.");
|
|
344
|
+
return id;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function listProviderThreads(appServer, provider, limit = SESSION_SCAN_LIMIT) {
|
|
348
|
+
const threads = [];
|
|
349
|
+
let cursor;
|
|
350
|
+
do {
|
|
351
|
+
const response = await appServer.request("thread/list", {
|
|
352
|
+
cursor,
|
|
353
|
+
limit: Math.min(100, limit - threads.length),
|
|
354
|
+
sortKey: "recency_at",
|
|
355
|
+
sortDirection: "desc",
|
|
356
|
+
modelProviders: [provider],
|
|
357
|
+
useStateDbOnly: false,
|
|
358
|
+
});
|
|
359
|
+
threads.push(...(response?.data || []));
|
|
360
|
+
cursor = response?.nextCursor || null;
|
|
361
|
+
} while (cursor && threads.length < limit);
|
|
362
|
+
return threads;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function restoreCodexTasksToOpenAI({
|
|
366
|
+
threadId,
|
|
367
|
+
migrateTasks,
|
|
368
|
+
model,
|
|
369
|
+
limit = DEFAULT_LIMIT,
|
|
370
|
+
input = process.stdin,
|
|
371
|
+
output = process.stdout,
|
|
372
|
+
stdout = console.log,
|
|
373
|
+
client,
|
|
374
|
+
clientOptions = {},
|
|
375
|
+
selectThreads = selectCodexRestoreThreads,
|
|
376
|
+
scanThreads = scanCodexSessionThreads,
|
|
377
|
+
} = {}) {
|
|
378
|
+
const appServer = client || new CodexAppServerClient(clientOptions);
|
|
379
|
+
try {
|
|
380
|
+
await appServer.initialize();
|
|
381
|
+
let selected;
|
|
382
|
+
if (threadId) {
|
|
383
|
+
selected = [{ id: threadId }];
|
|
384
|
+
} else {
|
|
385
|
+
const serverThreads = await listProviderThreads(appServer, "4yi");
|
|
386
|
+
const sessionThreads = scanThreads({ codexHome: codexHomeForEnv(clientOptions.env || process.env) });
|
|
387
|
+
const candidates = mergeProviderThreads(serverThreads, sessionThreads, "4yi");
|
|
388
|
+
if (candidates.length === 0) {
|
|
389
|
+
stdout("未发现仍绑定 4YI Gateway 的 Codex 任务。新任务将使用 OpenAI。");
|
|
390
|
+
return { cancelled: false, migrated: [], failed: [] };
|
|
391
|
+
}
|
|
392
|
+
if (migrateTasks === "all") {
|
|
393
|
+
selected = candidates;
|
|
394
|
+
} else if (!input?.isTTY || !output?.isTTY) {
|
|
395
|
+
stdout(`发现 ${candidates.length} 个仍绑定 4YI Gateway 的任务;非交互模式未自动复制。可运行 \`4yi restore codex --migrate-tasks all\` 或使用 \`--thread <任务ID>\`。`);
|
|
396
|
+
return { cancelled: true, migrated: [], failed: [] };
|
|
397
|
+
} else {
|
|
398
|
+
selected = await selectThreads(candidates.slice(0, limit), { input, output });
|
|
399
|
+
if (!selected || selected.length === 0) {
|
|
400
|
+
stdout("已跳过任务复制。原 4YI 任务未修改。");
|
|
401
|
+
return { cancelled: true, migrated: [], failed: [] };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const selectedModel = model || await loadDefaultOpenAIModel(appServer);
|
|
407
|
+
const migrated = [];
|
|
408
|
+
const failed = [];
|
|
409
|
+
for (const thread of selected) {
|
|
410
|
+
const item = describeCodexThread(thread);
|
|
411
|
+
stdout(`正在复制到 OpenAI:${item.title}…`);
|
|
412
|
+
try {
|
|
413
|
+
const result = await appServer.request("thread/fork", {
|
|
414
|
+
threadId: thread.id,
|
|
415
|
+
model: selectedModel,
|
|
416
|
+
modelProvider: "openai",
|
|
417
|
+
excludeTurns: true,
|
|
418
|
+
});
|
|
419
|
+
if (!result?.thread?.id) throw new Error("Codex did not return the copied task id.");
|
|
420
|
+
if (result.modelProvider !== "openai" && result.thread.modelProvider !== "openai") {
|
|
421
|
+
throw new Error("Codex created the task but did not apply the OpenAI provider.");
|
|
422
|
+
}
|
|
423
|
+
migrated.push({ sourceThreadId: thread.id, threadId: result.thread.id });
|
|
424
|
+
stdout(`✓ OpenAI 任务 ID: ${result.thread.id}`);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
failed.push({ sourceThreadId: thread.id, error: error.message });
|
|
427
|
+
stdout(`✗ 复制失败(${thread.id}):${error.message}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
stdout(`任务复制完成:成功 ${migrated.length},失败 ${failed.length}。原 4YI 任务均未修改。`);
|
|
431
|
+
return { cancelled: false, migrated, failed, model: selectedModel };
|
|
432
|
+
} finally {
|
|
433
|
+
if (!client) await appServer.close();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
295
437
|
export async function migrateCodexTask({
|
|
296
438
|
session,
|
|
297
439
|
threadId,
|
|
@@ -355,6 +497,9 @@ export const __testing = {
|
|
|
355
497
|
codexHomeForEnv,
|
|
356
498
|
loadDefaultCodexModel,
|
|
357
499
|
mergeCandidateThreads,
|
|
500
|
+
mergeProviderThreads,
|
|
501
|
+
listProviderThreads,
|
|
502
|
+
loadDefaultOpenAIModel,
|
|
358
503
|
relativeTime,
|
|
359
504
|
scanCodexSessionThreads,
|
|
360
505
|
sessionThreadFromFile,
|
package/src/connect.mjs
CHANGED
|
@@ -216,9 +216,9 @@ function reportDesktopApp(target, {
|
|
|
216
216
|
const detected = candidates.some((candidate) => exists(candidate));
|
|
217
217
|
if (target === "codex" && platform === "win32") {
|
|
218
218
|
if (detected) {
|
|
219
|
-
stdout("Codex App detected. 4YI updated the shared Windows Codex home. Finish active tasks, fully quit and reopen the App, then start a new chat to load the new provider.");
|
|
219
|
+
stdout("Codex App detected. 4YI updated the shared Windows Codex home. Finish active tasks, fully quit and reopen the App, then start a new chat to load the new provider. The App may hide Log out while this external provider supplies authentication; use `4yi restore codex`, then reopen the App, to return to OpenAI.");
|
|
220
220
|
} else {
|
|
221
|
-
stdout("4YI updated the shared Windows Codex home. A Microsoft Store/AppX install may not expose a standard executable path; finish active tasks, fully quit and reopen any running Codex App, then start a new chat.");
|
|
221
|
+
stdout("4YI updated the shared Windows Codex home. A Microsoft Store/AppX install may not expose a standard executable path; finish active tasks, fully quit and reopen any running Codex App, then start a new chat. The App may hide Log out while this external provider supplies authentication; use `4yi restore codex`, then reopen the App, to return to OpenAI.");
|
|
222
222
|
}
|
|
223
223
|
return detected;
|
|
224
224
|
}
|
|
@@ -249,21 +249,37 @@ function backupFile(target, file, home, groupId = timestamp()) {
|
|
|
249
249
|
return backup;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
function
|
|
252
|
+
function backupGroups(target, home) {
|
|
253
253
|
const dir = path.join(pathsForHome(home).backupsDir, target);
|
|
254
254
|
if (!fs.existsSync(dir)) return [];
|
|
255
255
|
const entries = fs.readdirSync(dir)
|
|
256
256
|
.filter((entry) => entry.endsWith(".json"))
|
|
257
257
|
.sort()
|
|
258
258
|
.reverse();
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
.
|
|
266
|
-
|
|
259
|
+
const groups = new Map();
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
const file = path.join(dir, entry);
|
|
262
|
+
const record = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
263
|
+
const groupId = record.group_id || entry;
|
|
264
|
+
if (!groups.has(groupId)) groups.set(groupId, []);
|
|
265
|
+
groups.get(groupId).push({ file, record });
|
|
266
|
+
}
|
|
267
|
+
return [...groups.values()];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function latestBackups(target, home) {
|
|
271
|
+
const groups = backupGroups(target, home);
|
|
272
|
+
if (target !== "codex") return groups[0] || [];
|
|
273
|
+
|
|
274
|
+
// Older CLI releases created a fresh backup every time `connect codex` ran,
|
|
275
|
+
// even when Codex was already connected. Such a group only restores the 4YI
|
|
276
|
+
// provider and credential helper. Walk back to the newest group whose config
|
|
277
|
+
// predates 4YI management so existing installations can still return to the
|
|
278
|
+
// official provider.
|
|
279
|
+
return groups.find((group) => !group.some(({ record }) => (
|
|
280
|
+
typeof record.content === "string"
|
|
281
|
+
&& (record.content.includes(CODEX_ROOT_START) || record.content.includes(CODEX_PROVIDER_START))
|
|
282
|
+
))) || [];
|
|
267
283
|
}
|
|
268
284
|
|
|
269
285
|
function mask(value) {
|
|
@@ -566,10 +582,15 @@ async function connectCodex({ session, home, codexHome, codexBaseUrl, stdout, sk
|
|
|
566
582
|
const catalog = buildCodexCatalog(models, template);
|
|
567
583
|
atomicWrite(paths.codexCatalog, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
568
584
|
|
|
569
|
-
const backupGroup = timestamp();
|
|
570
|
-
const backup = backupFile("codex", paths.codexConfig, home, backupGroup);
|
|
571
|
-
const helperBackup = backupFile("codex", paths.codexCredentialHelper, home, backupGroup);
|
|
572
585
|
let existing = fs.existsSync(paths.codexConfig) ? fs.readFileSync(paths.codexConfig, "utf8") : "";
|
|
586
|
+
const alreadyManaged = existing.includes(CODEX_ROOT_START) || existing.includes(CODEX_PROVIDER_START);
|
|
587
|
+
let backup;
|
|
588
|
+
let helperBackup;
|
|
589
|
+
if (!alreadyManaged) {
|
|
590
|
+
const backupGroup = timestamp();
|
|
591
|
+
backup = backupFile("codex", paths.codexConfig, home, backupGroup);
|
|
592
|
+
helperBackup = backupFile("codex", paths.codexCredentialHelper, home, backupGroup);
|
|
593
|
+
}
|
|
573
594
|
existing = removeManagedBlock(existing, CODEX_ROOT_START, CODEX_ROOT_END);
|
|
574
595
|
existing = removeManagedBlock(existing, CODEX_PROVIDER_START, CODEX_PROVIDER_END);
|
|
575
596
|
existing = removeCodexRootAssignments(existing);
|
|
@@ -584,8 +605,12 @@ async function connectCodex({ session, home, codexHome, codexBaseUrl, stdout, sk
|
|
|
584
605
|
stdout(`Connected Codex: ${paths.codexConfig}`);
|
|
585
606
|
stdout(`Codex credential helper: ${paths.codexCredentialHelper}`);
|
|
586
607
|
stdout(`Available Codex models: ${models.map((model) => model.id).join(", ")}`);
|
|
587
|
-
|
|
588
|
-
|
|
608
|
+
if (backup && helperBackup) {
|
|
609
|
+
stdout(`Backup: ${backup}`);
|
|
610
|
+
stdout(`Backup: ${helperBackup}`);
|
|
611
|
+
} else {
|
|
612
|
+
stdout("Preserved the existing Codex restore point.");
|
|
613
|
+
}
|
|
589
614
|
}
|
|
590
615
|
|
|
591
616
|
function resolveUrls(session, options) {
|
|
@@ -684,8 +709,7 @@ function restoreOne(target, home, stdout, { required = true } = {}) {
|
|
|
684
709
|
if (required) throw new Error(`No ${target} backup found.`);
|
|
685
710
|
return false;
|
|
686
711
|
}
|
|
687
|
-
for (const backup of backups) {
|
|
688
|
-
const record = JSON.parse(fs.readFileSync(backup, "utf8"));
|
|
712
|
+
for (const { file: backup, record } of backups) {
|
|
689
713
|
if (record.existed) atomicWrite(record.source, record.content || "");
|
|
690
714
|
else if (fs.existsSync(record.source)) fs.unlinkSync(record.source);
|
|
691
715
|
stdout(`Restored ${target}: ${record.source}`);
|
|
@@ -696,12 +720,18 @@ function restoreOne(target, home, stdout, { required = true } = {}) {
|
|
|
696
720
|
|
|
697
721
|
export function restoreConnection({ target = "all", home = os.homedir(), stdout = console.log } = {}) {
|
|
698
722
|
const normalized = normalizeTarget(target);
|
|
699
|
-
if (normalized === "claude")
|
|
700
|
-
|
|
723
|
+
if (normalized === "claude") {
|
|
724
|
+
restoreOne("claude", home, stdout);
|
|
725
|
+
return { restoredClaude: true, restoredCodex: false };
|
|
726
|
+
}
|
|
727
|
+
if (normalized === "codex") {
|
|
728
|
+
restoreOne("codex", home, stdout);
|
|
729
|
+
return { restoredClaude: false, restoredCodex: true };
|
|
730
|
+
}
|
|
701
731
|
const restoredClaude = restoreOne("claude", home, stdout, { required: false });
|
|
702
732
|
const restoredCodex = restoreOne("codex", home, stdout, { required: false });
|
|
703
733
|
if (!restoredClaude && !restoredCodex) throw new Error("No Claude or Codex backup found.");
|
|
704
|
-
return
|
|
734
|
+
return { restoredClaude, restoredCodex };
|
|
705
735
|
}
|
|
706
736
|
|
|
707
737
|
export const __testing = {
|