@4yi-dev/cli 0.1.15 → 0.1.16

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 CHANGED
@@ -39,7 +39,7 @@ 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4yi-dev/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "4YI command-line launcher for OAuth login and OpenCode runtime",
5
5
  "type": "module",
6
6
  "bin": {
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 latestBackups(target, home) {
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
- if (!entries[0]) return [];
260
- const latest = path.join(dir, entries[0]);
261
- const latestRecord = JSON.parse(fs.readFileSync(latest, "utf8"));
262
- const groupId = latestRecord.group_id;
263
- if (!groupId) return [latest];
264
- return entries
265
- .map((entry) => path.join(dir, entry))
266
- .filter((file) => JSON.parse(fs.readFileSync(file, "utf8")).group_id === groupId);
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
- stdout(`Backup: ${backup}`);
588
- stdout(`Backup: ${helperBackup}`);
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}`);