oc-codex-multi-auth 6.1.3 → 6.1.6

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.
@@ -8,21 +8,28 @@ const PACKAGE_NAME = "oc-codex-multi-auth";
8
8
  const LEGACY_PACKAGE_NAMES = ["oc-chatgpt-multi-auth"];
9
9
  const WINDOWS_RENAME_RETRY_ATTEMPTS = 5;
10
10
  const WINDOWS_RENAME_RETRY_BASE_DELAY_MS = 10;
11
+ const STALE_MANAGED_MODEL_KEYS = new Set([
12
+ "gpt-5.2",
13
+ "gpt-5.3-codex",
14
+ "gpt-5.4",
15
+ ]);
11
16
 
12
17
  function getManagedPackageNames() {
13
18
  return [PACKAGE_NAME, ...LEGACY_PACKAGE_NAMES];
14
19
  }
15
20
 
16
21
  function printHelp() {
17
- console.log(`Usage: ${PACKAGE_NAME} [--modern|--legacy] [--dry-run] [--no-cache-clear]\n\n` +
22
+ console.log(`Usage: ${PACKAGE_NAME} [--modern|--full|--legacy] [--dry-run] [--no-cache-clear]\n\n` +
18
23
  "Default behavior:\n" +
19
24
  " - Installs/updates global config at ~/.config/opencode/opencode.json\n" +
20
- " - Uses full catalog config by default (9 base models + 34 explicit presets)\n" +
25
+ " - Enables the prompt status bar TUI plugin at ~/.config/opencode/tui.json\n" +
26
+ " - Uses compact UI config by default (9 base OAuth models + variant picker presets)\n" +
21
27
  " - Ensures plugin is unpinned (latest)\n" +
22
28
  " - Clears OpenCode plugin cache\n\n" +
23
29
  "Options:\n" +
24
- " --modern Force compact modern config (9 base models + --variant presets)\n" +
25
- " --legacy Force explicit legacy config (34 preset model entries)\n" +
30
+ " --modern Force compact modern config (9 base OAuth models + --variant presets)\n" +
31
+ " --full Install compact base models plus 36 explicit selector entries\n" +
32
+ " --legacy Force explicit legacy config (36 preset model entries)\n" +
26
33
  " --dry-run Show actions without writing\n" +
27
34
  " --no-cache-clear Skip clearing OpenCode cache\n"
28
35
  );
@@ -63,8 +70,10 @@ function buildPaths(homeDir) {
63
70
  return {
64
71
  configDir,
65
72
  configPath: join(configDir, "opencode.json"),
73
+ tuiConfigPath: join(configDir, "tui.json"),
66
74
  cacheDir,
67
75
  cacheNodeModulesPaths: getManagedPackageNames().map((name) => join(cacheDir, "node_modules", name)),
76
+ cachePackagePaths: getManagedPackageNames().map((name) => join(cacheDir, "packages", `${name}@latest`)),
68
77
  cacheBunLock: join(cacheDir, "bun.lock"),
69
78
  cachePackageJson: join(cacheDir, "package.json"),
70
79
  modernTemplatePath,
@@ -81,30 +90,67 @@ function parseCliArgs(argv = process.argv.slice(2)) {
81
90
  }
82
91
 
83
92
  const requestedModern = args.has("--modern");
93
+ const requestedFull = args.has("--full");
84
94
  const requestedLegacy = args.has("--legacy");
85
95
 
86
- if (requestedModern && requestedLegacy) {
87
- throw new Error("Choose only one of --modern or --legacy.");
96
+ const requestedModes = [requestedModern, requestedFull, requestedLegacy]
97
+ .filter(Boolean).length;
98
+ if (requestedModes > 1) {
99
+ throw new Error("Choose only one of --modern, --full, or --legacy.");
88
100
  }
89
101
 
90
102
  return {
91
103
  wantsHelp: false,
92
104
  dryRun: args.has("--dry-run"),
93
105
  skipCacheClear: args.has("--no-cache-clear"),
94
- configMode: requestedModern ? "modern" : requestedLegacy ? "legacy" : "full",
106
+ configMode: requestedFull ? "full" : requestedLegacy ? "legacy" : "modern",
95
107
  };
96
108
  }
97
109
 
110
+ function normalizePluginEntryForMatch(entry) {
111
+ const trimmed = entry.trim();
112
+ let normalized = trimmed.toLowerCase();
113
+ try {
114
+ normalized = decodeURIComponent(normalized);
115
+ } catch {
116
+ // Keep the raw lowercased value when a malformed URI escape is present.
117
+ }
118
+ normalized = normalized.replace(/\\/g, "/").replace(/\/+$/g, "");
119
+ if (normalized.endsWith("/dist")) {
120
+ normalized = normalized.slice(0, -"/dist".length);
121
+ }
122
+ return normalized;
123
+ }
124
+
125
+ function isManagedPluginEntry(entry) {
126
+ if (typeof entry !== "string") return false;
127
+ const trimmed = entry.trim().toLowerCase();
128
+ const normalized = normalizePluginEntryForMatch(entry);
129
+ return getManagedPackageNames().some((name) => {
130
+ const lowerName = name.toLowerCase();
131
+ return trimmed === lowerName ||
132
+ trimmed.startsWith(`${lowerName}@`) ||
133
+ normalized.endsWith(`/${lowerName}`) ||
134
+ normalized.endsWith(`/node_modules/${lowerName}`);
135
+ });
136
+ }
137
+
98
138
  function normalizePluginList(list) {
99
139
  const entries = Array.isArray(list) ? list.filter(Boolean) : [];
100
- const managedNames = getManagedPackageNames();
101
- const filtered = entries.filter((entry) => {
102
- if (typeof entry !== "string") return true;
103
- return !managedNames.some((name) => entry === name || entry.startsWith(`${name}@`));
104
- });
140
+ const filtered = entries.filter((entry) => !isManagedPluginEntry(entry));
105
141
  return [...filtered, PACKAGE_NAME];
106
142
  }
107
143
 
144
+ function mergeTuiConfig(existingConfig) {
145
+ const existing = isPlainObject(existingConfig) ? { ...existingConfig } : {};
146
+ const next = { ...existing };
147
+ if (typeof next.$schema !== "string" || !next.$schema.trim()) {
148
+ next.$schema = "https://opencode.ai/tui.json";
149
+ }
150
+ next.plugin = normalizePluginList(existing.plugin);
151
+ return next;
152
+ }
153
+
108
154
  function formatJson(obj) {
109
155
  return `${JSON.stringify(obj, null, 2)}\n`;
110
156
  }
@@ -125,9 +171,12 @@ function isPlainObject(value) {
125
171
  // installer overwrite the managed shape it ships. This replaces the earlier
126
172
  // wholesale overwrite which clobbered custom user-added keys (see audit top-20
127
173
  // #6).
128
- function mergeOpenaiProvider(existingOpenai, templateOpenai) {
174
+ function mergeOpenaiProvider(existingOpenai, templateOpenai, options = {}) {
129
175
  const existingSafe = isPlainObject(existingOpenai) ? existingOpenai : {};
130
176
  const templateSafe = isPlainObject(templateOpenai) ? templateOpenai : {};
177
+ const modelKeysToRemove = options.modelKeysToRemove instanceof Set
178
+ ? options.modelKeysToRemove
179
+ : new Set();
131
180
 
132
181
  const result = {};
133
182
 
@@ -147,7 +196,10 @@ function mergeOpenaiProvider(existingOpenai, templateOpenai) {
147
196
  // 3. Merge `models` by id: template wins on collision, user-added ids survive.
148
197
  const existingModels = isPlainObject(existingSafe.models) ? existingSafe.models : {};
149
198
  const templateModels = isPlainObject(templateSafe.models) ? templateSafe.models : {};
150
- const mergedModels = { ...existingModels, ...templateModels };
199
+ const prunedExistingModels = Object.fromEntries(
200
+ Object.entries(existingModels).filter(([key]) => !modelKeysToRemove.has(key)),
201
+ );
202
+ const mergedModels = { ...prunedExistingModels, ...templateModels };
151
203
  if (Object.keys(mergedModels).length > 0) {
152
204
  result.models = mergedModels;
153
205
  }
@@ -203,6 +255,10 @@ function mergeFullTemplate(modernTemplate, legacyTemplate) {
203
255
  };
204
256
  }
205
257
 
258
+ function getTemplateModelKeys(template) {
259
+ return new Set(Object.keys(template.provider?.openai?.models ?? {}));
260
+ }
261
+
206
262
  async function readJson(filePath) {
207
263
  const content = await readFile(filePath, "utf-8");
208
264
  return JSON.parse(content.charCodeAt(0) === 0xfeff ? content.slice(1) : content);
@@ -351,11 +407,17 @@ async function clearCache(paths, dryRun, skipCacheClear) {
351
407
  for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
352
408
  log(`[dry-run] Would remove ${cacheNodeModulesPath}`);
353
409
  }
410
+ for (const cachePackagePath of paths.cachePackagePaths) {
411
+ log(`[dry-run] Would remove ${cachePackagePath}`);
412
+ }
354
413
  log(`[dry-run] Would remove ${paths.cacheBunLock}`);
355
414
  } else {
356
415
  for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
357
416
  await rm(cacheNodeModulesPath, { recursive: true, force: true });
358
417
  }
418
+ for (const cachePackagePath of paths.cachePackagePaths) {
419
+ await rm(cachePackagePath, { recursive: true, force: true });
420
+ }
359
421
  await rm(paths.cacheBunLock, { force: true });
360
422
  }
361
423
 
@@ -386,6 +448,17 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
386
448
 
387
449
  const template = await loadTemplate(configMode, paths);
388
450
  template.plugin = [PACKAGE_NAME];
451
+ const modelKeysToRemove = new Set(STALE_MANAGED_MODEL_KEYS);
452
+ if (configMode === "modern") {
453
+ for (const key of getTemplateModelKeys(await readJson(paths.legacyTemplatePath))) {
454
+ modelKeysToRemove.add(key);
455
+ }
456
+ }
457
+ if (configMode === "legacy") {
458
+ for (const key of getTemplateModelKeys(await readJson(paths.modernTemplatePath))) {
459
+ modelKeysToRemove.add(key);
460
+ }
461
+ }
389
462
 
390
463
  let nextConfig = template;
391
464
  let existingConfig;
@@ -401,7 +474,9 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
401
474
  const provider = (existing.provider && typeof existing.provider === "object")
402
475
  ? { ...existing.provider }
403
476
  : {};
404
- provider.openai = mergeOpenaiProvider(existing.provider?.openai, template.provider?.openai);
477
+ provider.openai = mergeOpenaiProvider(existing.provider?.openai, template.provider?.openai, {
478
+ modelKeysToRemove,
479
+ });
405
480
  merged.provider = provider;
406
481
  nextConfig = merged;
407
482
  } catch (error) {
@@ -413,15 +488,39 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
413
488
  log("No existing config found. Creating new global config.");
414
489
  }
415
490
 
491
+ let nextTuiConfig = mergeTuiConfig(undefined);
492
+ let existingTuiConfig;
493
+ if (existsSync(paths.tuiConfigPath)) {
494
+ const backupPath = await backupConfig(paths.tuiConfigPath, dryRun);
495
+ log(`${dryRun ? "[dry-run] Would create backup" : "Backup created"}: ${backupPath}`);
496
+
497
+ try {
498
+ const existing = await readJson(paths.tuiConfigPath);
499
+ existingTuiConfig = existing;
500
+ nextTuiConfig = mergeTuiConfig(existing);
501
+ } catch (error) {
502
+ log(`Warning: Could not parse existing TUI config (${formatErrorForLog(error)}). Replacing with minimal TUI config.`);
503
+ existingTuiConfig = undefined;
504
+ nextTuiConfig = mergeTuiConfig(undefined);
505
+ }
506
+ } else {
507
+ log("No existing TUI config found. Creating new global TUI config.");
508
+ }
509
+
416
510
  let wrote = false;
417
511
  if (dryRun) {
418
512
  log(`[dry-run] Would write ${paths.configPath} using ${configMode} config`);
419
513
  log(`[dry-run] Diff for ${paths.configPath}:`);
420
514
  log(formatConfigDiff(existingConfig, nextConfig));
515
+ log(`[dry-run] Would write ${paths.tuiConfigPath} with the TUI status plugin`);
516
+ log(`[dry-run] Diff for ${paths.tuiConfigPath}:`);
517
+ log(formatConfigDiff(existingTuiConfig, nextTuiConfig));
421
518
  } else {
422
519
  await writeFileAtomic(paths.configPath, formatJson(nextConfig));
520
+ await writeFileAtomic(paths.tuiConfigPath, formatJson(nextTuiConfig));
423
521
  wrote = true;
424
522
  log(`Wrote ${paths.configPath} (${configMode} config)`);
523
+ log(`Wrote ${paths.tuiConfigPath} (TUI status plugin)`);
425
524
  }
426
525
 
427
526
  await clearCache(paths, dryRun, skipCacheClear);
@@ -429,13 +528,13 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
429
528
  log("\nDone. Restart OpenCode to (re)install the plugin.");
430
529
  log("Example: opencode");
431
530
  if (configMode === "modern") {
432
- log("Note: Modern config intentionally shows 9 base model entries; use --variant to access all 34 shipped presets.");
531
+ log("Note: Modern config intentionally shows 9 base OAuth model entries; use the variant picker for reasoning presets.");
433
532
  }
434
533
  if (configMode === "legacy") {
435
- log("Note: Legacy config writes 34 explicit preset entries and is also safe for older OpenCode versions.");
534
+ log("Note: Legacy config writes 36 explicit preset entries and is also safe for older OpenCode versions.");
436
535
  }
437
536
  if (configMode === "full") {
438
- log("Note: Full config installs both modern base models and explicit preset entries so the full shipped catalog is visible by default.");
537
+ log("Note: Full config installs both compact base models and explicit preset entries for direct selector IDs.");
439
538
  }
440
539
 
441
540
  return {
@@ -443,6 +542,7 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
443
542
  action: "install",
444
543
  configMode,
445
544
  configPath: paths.configPath,
545
+ tuiConfigPath: paths.tuiConfigPath,
446
546
  dryRun: Boolean(dryRun),
447
547
  wrote,
448
548
  };
@@ -455,6 +555,7 @@ export const __test = {
455
555
  formatConfigDiff,
456
556
  mergeFullTemplate,
457
557
  mergeOpenaiProvider,
558
+ mergeTuiConfig,
458
559
  parseCliArgs,
459
560
  writeFileAtomic,
460
561
  renameWithWindowsRetry,