oc-codex-multi-auth 6.10.1 → 6.11.1
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 +30 -13
- package/config/README.md +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -220
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/state.d.ts +1 -1
- package/dist/lib/accounts/state.js +1 -1
- package/dist/lib/accounts/warm-request.d.ts +14 -1
- package/dist/lib/accounts/warm-request.d.ts.map +1 -1
- package/dist/lib/accounts/warm-request.js +140 -21
- package/dist/lib/accounts/warm-request.js.map +1 -1
- package/dist/lib/auth/server.d.ts +1 -1
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auto-update-checker.d.ts.map +1 -1
- package/dist/lib/auto-update-checker.js +3 -2
- package/dist/lib/auto-update-checker.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +1 -0
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +17 -2
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +7 -7
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/recovery.d.ts +2 -2
- package/dist/lib/recovery.js +2 -2
- package/dist/lib/request/fetch-helpers.d.ts +8 -2
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +8 -2
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/request-transformer.d.ts +4 -1
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +4 -1
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/request/response-handler.d.ts +1 -0
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +1 -0
- package/dist/lib/request/response-handler.js.map +1 -1
- package/dist/lib/runtime.d.ts +1 -1
- package/dist/lib/runtime.js +1 -1
- package/dist/lib/storage/backup.d.ts +1 -1
- package/dist/lib/storage/backup.js +1 -1
- package/dist/lib/storage/export-import.d.ts +6 -3
- package/dist/lib/storage/export-import.d.ts.map +1 -1
- package/dist/lib/storage/export-import.js +5 -2
- package/dist/lib/storage/export-import.js.map +1 -1
- package/dist/lib/storage/keychain.d.ts +2 -2
- package/dist/lib/storage/keychain.js +2 -2
- package/dist/lib/storage/load-save.js +1 -1
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/storage/worktree-lock.d.ts +2 -2
- package/dist/lib/storage/worktree-lock.js +2 -2
- package/dist/lib/storage.d.ts +2 -2
- package/dist/lib/storage.js +2 -2
- package/dist/lib/tools/codex-diag.d.ts +1 -1
- package/dist/lib/tools/codex-diag.js +1 -1
- package/dist/lib/tools/codex-diff.d.ts.map +1 -1
- package/dist/lib/tools/codex-diff.js +1 -2
- package/dist/lib/tools/codex-diff.js.map +1 -1
- package/dist/lib/tools/index.d.ts +1 -1
- package/dist/lib/tools/index.js +1 -1
- package/package.json +7 -5
- package/scripts/install-oc-codex-multi-auth-core.js +379 -68
|
@@ -15,11 +15,13 @@ const STALE_MANAGED_MODEL_KEYS = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
const STANDALONE_COMMANDS = new Set(["doctor", "status", "list", "limits", "dashboard", "health", "diag", "warm"]);
|
|
17
17
|
const INSTALLER_COMMANDS = new Set(["install"]);
|
|
18
|
+
const UPDATE_COMMANDS = new Set(["update"]);
|
|
18
19
|
|
|
19
20
|
function splitCommandArgv(argv) {
|
|
20
21
|
const [first, ...rest] = argv;
|
|
21
22
|
if (!first) return { kind: "install", argv };
|
|
22
23
|
if (INSTALLER_COMMANDS.has(first)) return { kind: "install", argv: rest };
|
|
24
|
+
if (UPDATE_COMMANDS.has(first)) return { kind: "update", argv: rest };
|
|
23
25
|
if (STANDALONE_COMMANDS.has(first)) return { kind: "standalone", command: first, argv: rest };
|
|
24
26
|
if (first.startsWith("-")) return { kind: "install", argv };
|
|
25
27
|
return { kind: "unknown", command: first, argv: rest };
|
|
@@ -76,23 +78,26 @@ export function isDirectRunPath(argvPath, modulePath, resolveRealPath = realpath
|
|
|
76
78
|
function printHelp() {
|
|
77
79
|
console.log(`Usage: ${PACKAGE_NAME} [command] [options]\n\n` +
|
|
78
80
|
"Commands:\n" +
|
|
79
|
-
" install
|
|
81
|
+
" install Register plugin entries (default with no command)\n" +
|
|
82
|
+
" update Refresh the cached package without changing OpenCode config\n" +
|
|
80
83
|
" doctor Run local account/config diagnostics\n" +
|
|
81
84
|
" status Show account/config status\n" +
|
|
82
85
|
" list List configured accounts\n" +
|
|
83
|
-
" limits Show
|
|
86
|
+
" limits Show live 5-hour and weekly usage for each account\n" +
|
|
84
87
|
" dashboard Print dashboard guidance\n" +
|
|
85
88
|
" health Check local token/account health\n" +
|
|
86
89
|
" diag Alias for doctor --deep\n" +
|
|
87
90
|
" warm Open every enabled account's usage window now (one request each)\n\n" +
|
|
88
|
-
`Installer usage: ${PACKAGE_NAME} [--modern|--full|--legacy] [--dry-run] [--no-cache-clear]\n
|
|
91
|
+
`Installer usage: ${PACKAGE_NAME} install [--plugin-only|--modern|--full|--legacy] [--dry-run] [--no-cache-clear]\n` +
|
|
92
|
+
`Updater usage: ${PACKAGE_NAME} update [--dry-run]\n\n` +
|
|
89
93
|
"Default behavior:\n" +
|
|
90
|
-
" -
|
|
94
|
+
" - Registers plugin entries without changing provider.openai\n" +
|
|
91
95
|
" - Enables the prompt status bar TUI plugin at ~/.config/opencode/tui.json\n" +
|
|
92
|
-
" -
|
|
96
|
+
" - Installs model catalogs only with --modern, --full, or --legacy\n" +
|
|
93
97
|
" - Ensures plugin is unpinned (latest)\n" +
|
|
94
98
|
" - Clears OpenCode plugin cache\n\n" +
|
|
95
99
|
"Options:\n" +
|
|
100
|
+
" --plugin-only Register plugins without changing provider.openai\n" +
|
|
96
101
|
" --modern Force compact modern config (12 base OAuth models + --variant presets)\n" +
|
|
97
102
|
" --full Install compact base models plus 53 explicit selector entries\n" +
|
|
98
103
|
" --legacy Force explicit legacy config (53 preset model entries)\n" +
|
|
@@ -139,7 +144,10 @@ function buildPaths(homeDir) {
|
|
|
139
144
|
tuiConfigPath: join(configDir, "tui.json"),
|
|
140
145
|
cacheDir,
|
|
141
146
|
cacheNodeModulesPaths: getManagedPackageNames().map((name) => join(cacheDir, "node_modules", name)),
|
|
142
|
-
cachePackagePaths: getManagedPackageNames().
|
|
147
|
+
cachePackagePaths: getManagedPackageNames().flatMap((name) => [
|
|
148
|
+
join(cacheDir, "packages", name),
|
|
149
|
+
join(cacheDir, "packages", `${name}@latest`),
|
|
150
|
+
]),
|
|
143
151
|
cacheBunLock: join(cacheDir, "bun.lock"),
|
|
144
152
|
cachePackageJson: join(cacheDir, "package.json"),
|
|
145
153
|
modernTemplatePath,
|
|
@@ -158,21 +166,40 @@ function parseCliArgs(argv = process.argv.slice(2)) {
|
|
|
158
166
|
const requestedModern = args.has("--modern");
|
|
159
167
|
const requestedFull = args.has("--full");
|
|
160
168
|
const requestedLegacy = args.has("--legacy");
|
|
169
|
+
const explicitPluginOnly = args.has("--plugin-only");
|
|
161
170
|
|
|
162
171
|
const requestedModes = [requestedModern, requestedFull, requestedLegacy]
|
|
163
172
|
.filter(Boolean).length;
|
|
164
173
|
if (requestedModes > 1) {
|
|
165
174
|
throw new Error("Choose only one of --modern, --full, or --legacy.");
|
|
166
175
|
}
|
|
176
|
+
if (explicitPluginOnly && requestedModes > 0) {
|
|
177
|
+
throw new Error("--plugin-only cannot be combined with --modern, --full, or --legacy.");
|
|
178
|
+
}
|
|
179
|
+
const pluginOnly = explicitPluginOnly || requestedModes === 0;
|
|
167
180
|
|
|
168
181
|
return {
|
|
169
182
|
wantsHelp: false,
|
|
170
183
|
dryRun: args.has("--dry-run"),
|
|
171
184
|
skipCacheClear: args.has("--no-cache-clear"),
|
|
185
|
+
pluginOnly,
|
|
172
186
|
configMode: requestedFull ? "full" : requestedLegacy ? "legacy" : "modern",
|
|
173
187
|
};
|
|
174
188
|
}
|
|
175
189
|
|
|
190
|
+
function parseUpdateArgs(argv) {
|
|
191
|
+
const args = new Set(argv);
|
|
192
|
+
const supported = new Set(["--dry-run", "--help", "-h"]);
|
|
193
|
+
const unknown = argv.find((arg) => !supported.has(arg));
|
|
194
|
+
if (unknown) {
|
|
195
|
+
throw new Error(`Unknown option for update command: ${unknown}`);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
wantsHelp: args.has("--help") || args.has("-h"),
|
|
199
|
+
dryRun: args.has("--dry-run"),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
176
203
|
function normalizePluginEntryForMatch(entry) {
|
|
177
204
|
const trimmed = entry.trim();
|
|
178
205
|
let normalized = trimmed.toLowerCase();
|
|
@@ -353,34 +380,54 @@ function printStandaloneResult(command, payload, json) {
|
|
|
353
380
|
if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
|
|
354
381
|
}
|
|
355
382
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Import compiled modules from dist/ so the standalone CLI behaves identically
|
|
385
|
+
* to the in-conversation tools. dist/ ships in the npm package (files
|
|
386
|
+
* allowlist) and none of these modules import the OpenCode plugin runtime, so
|
|
387
|
+
* they load cleanly in plain Node.
|
|
388
|
+
*/
|
|
389
|
+
async function loadDistModules(relativePaths, label) {
|
|
361
390
|
const distRoot = join(repoRoot, "dist", "lib");
|
|
362
391
|
const toUrl = (rel) => pathToFileURL(join(distRoot, rel)).href;
|
|
363
392
|
try {
|
|
364
|
-
|
|
365
|
-
import(toUrl("storage.js")),
|
|
366
|
-
import(toUrl("codex-usage.js")),
|
|
367
|
-
import(toUrl("accounts/warm-request.js")),
|
|
368
|
-
import(toUrl("accounts/warm.js")),
|
|
369
|
-
import(toUrl("shutdown.js")),
|
|
370
|
-
]);
|
|
371
|
-
// Unlike the plugin, this CLI *is* the process, so it owns termination:
|
|
372
|
-
// Ctrl+C must abort the warm run rather than wait for it to drain.
|
|
373
|
-
// Refreshing a token here persists credentials, which registers the
|
|
374
|
-
// shutdown handler via the storage lock.
|
|
375
|
-
shutdownMod.setShutdownOwnsProcess(true);
|
|
376
|
-
return { storageMod, usageMod, warmReqMod, warmMod, shutdownMod };
|
|
393
|
+
return await Promise.all(relativePaths.map((rel) => import(toUrl(rel))));
|
|
377
394
|
} catch (error) {
|
|
378
395
|
throw new Error(
|
|
379
|
-
`Could not load
|
|
396
|
+
`Could not load ${label} runtime from dist/. Build the package first (npm run build). Cause: ${formatErrorForLog(error)}`,
|
|
380
397
|
);
|
|
381
398
|
}
|
|
382
399
|
}
|
|
383
400
|
|
|
401
|
+
async function loadWarmRuntime(env) {
|
|
402
|
+
const [storageMod, usageMod, warmReqMod, warmMod, shutdownMod] = await loadDistModules(
|
|
403
|
+
[
|
|
404
|
+
"storage.js",
|
|
405
|
+
"codex-usage.js",
|
|
406
|
+
"accounts/warm-request.js",
|
|
407
|
+
"accounts/warm.js",
|
|
408
|
+
"shutdown.js",
|
|
409
|
+
],
|
|
410
|
+
"warm",
|
|
411
|
+
);
|
|
412
|
+
// Unlike the plugin, this CLI *is* the process, so it owns termination:
|
|
413
|
+
// Ctrl+C must abort the warm run rather than wait for it to drain.
|
|
414
|
+
// Refreshing a token here persists credentials, which registers the
|
|
415
|
+
// shutdown handler via the storage lock.
|
|
416
|
+
shutdownMod.setShutdownOwnsProcess(true);
|
|
417
|
+
return { storageMod, usageMod, warmReqMod, warmMod, shutdownMod };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function loadLimitsRuntime(env) {
|
|
421
|
+
const [storageMod, usageMod, shutdownMod, loggerMod] = await loadDistModules(
|
|
422
|
+
["storage.js", "codex-usage.js", "shutdown.js", "logger.js"],
|
|
423
|
+
"limits",
|
|
424
|
+
);
|
|
425
|
+
// Fetching usage can refresh (and therefore persist) a token, so the same
|
|
426
|
+
// process-owns-termination rule as `warm` applies.
|
|
427
|
+
shutdownMod.setShutdownOwnsProcess(true);
|
|
428
|
+
return { storageMod, usageMod, shutdownMod, loggerMod };
|
|
429
|
+
}
|
|
430
|
+
|
|
384
431
|
export async function runWarmCommand(parsed, options = {}) {
|
|
385
432
|
const { env = process.env } = options;
|
|
386
433
|
const storagePath = getStandaloneStoragePath(parsed, env);
|
|
@@ -478,6 +525,147 @@ function printWarmResult(payload, json) {
|
|
|
478
525
|
if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
|
|
479
526
|
}
|
|
480
527
|
|
|
528
|
+
/**
|
|
529
|
+
* `limits` — show live 5-hour and weekly Codex usage per account (#209).
|
|
530
|
+
*
|
|
531
|
+
* Reuses the compiled `codex-usage` runtime so the CLI reports the same windows
|
|
532
|
+
* as the in-conversation `codex-limits` tool. The locally persisted
|
|
533
|
+
* `rateLimitResetTimes` is carried in the payload as well: it is the only
|
|
534
|
+
* rate-limit state available for an account whose live fetch fails, and
|
|
535
|
+
* `--json` consumers of the previous behavior still find the field.
|
|
536
|
+
*/
|
|
537
|
+
export async function runLimitsCommand(parsed, options = {}) {
|
|
538
|
+
const { env = process.env } = options;
|
|
539
|
+
const storagePath = getStandaloneStoragePath(parsed, env);
|
|
540
|
+
|
|
541
|
+
let runtime;
|
|
542
|
+
try {
|
|
543
|
+
runtime = await loadLimitsRuntime(env);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
const payload = { command: "limits", storagePath, error: formatErrorForLog(error) };
|
|
546
|
+
printLimitsResult(payload, parsed.json);
|
|
547
|
+
return { exitCode: 1, action: "limits", storagePath };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const { storageMod, usageMod, loggerMod } = runtime;
|
|
551
|
+
// Point dist storage at the resolved accounts file so a refreshed token is
|
|
552
|
+
// persisted to the SAME file the rest of the toolchain reads.
|
|
553
|
+
storageMod.setStoragePathDirect(storagePath);
|
|
554
|
+
|
|
555
|
+
const storage = await storageMod.loadAccounts();
|
|
556
|
+
const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
|
|
557
|
+
if (accounts.length === 0) {
|
|
558
|
+
const payload = {
|
|
559
|
+
command: "limits",
|
|
560
|
+
storagePath,
|
|
561
|
+
totalAccounts: 0,
|
|
562
|
+
accounts: [],
|
|
563
|
+
message: "No accounts configured.",
|
|
564
|
+
nextAction: "Run opencode auth login.",
|
|
565
|
+
};
|
|
566
|
+
printLimitsResult(payload, parsed.json);
|
|
567
|
+
return { exitCode: 0, action: "limits", storagePath };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Same workspace dedupe the codex-limits tool applies, so two entries for one
|
|
571
|
+
// workspace are not billed and printed twice.
|
|
572
|
+
const indices = usageMod.deduplicateUsageAccountIndices(storage);
|
|
573
|
+
// `--tag` must gate which accounts are contacted at all, not just which are
|
|
574
|
+
// printed: an untagged account would otherwise be billed a usage fetch and
|
|
575
|
+
// could have its refreshed credentials persisted.
|
|
576
|
+
const normalizedTag =
|
|
577
|
+
typeof parsed.tag === "string" ? parsed.tag.trim().toLowerCase() : "";
|
|
578
|
+
const results = [];
|
|
579
|
+
let failedCount = 0;
|
|
580
|
+
|
|
581
|
+
for (const index of indices) {
|
|
582
|
+
const account = accounts[index];
|
|
583
|
+
if (!account) continue;
|
|
584
|
+
if (
|
|
585
|
+
normalizedTag &&
|
|
586
|
+
!(
|
|
587
|
+
Array.isArray(account.accountTags) &&
|
|
588
|
+
account.accountTags.some((entry) => String(entry).toLowerCase() === normalizedTag)
|
|
589
|
+
)
|
|
590
|
+
) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const entry = {
|
|
594
|
+
index,
|
|
595
|
+
label: account.accountLabel ?? `Account ${index + 1}`,
|
|
596
|
+
email: maskValue(account.email, parsed.includeSensitive),
|
|
597
|
+
rateLimitResetTimes: account.rateLimitResetTimes ?? {},
|
|
598
|
+
};
|
|
599
|
+
try {
|
|
600
|
+
const { accessToken } = await usageMod.ensureCodexUsageAccessToken({ storage, account });
|
|
601
|
+
const accountId = usageMod.resolveCodexUsageAccountId({ account, accessToken });
|
|
602
|
+
if (!accountId) {
|
|
603
|
+
throw new Error("could not resolve account id (re-login may be required)");
|
|
604
|
+
}
|
|
605
|
+
const usage = usageMod.parseCodexUsagePayload(
|
|
606
|
+
await usageMod.fetchCodexUsage({
|
|
607
|
+
accountId,
|
|
608
|
+
accessToken,
|
|
609
|
+
organizationId: account.organizationId,
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
entry.planType = usage.planType;
|
|
613
|
+
entry.credits = usage.credits;
|
|
614
|
+
entry.limits = usage.limits;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
// `ensureCodexUsageAccessToken` can surface a raw OAuth refresh
|
|
617
|
+
// response, so the message is redacted through the logger's token
|
|
618
|
+
// patterns before it reaches stdout, JSON output, or CI logs.
|
|
619
|
+
// Truncation alone does not protect bearer/JWT/refresh-token material.
|
|
620
|
+
entry.error = loggerMod.maskString(formatErrorForLog(error)).slice(0, 160);
|
|
621
|
+
failedCount += 1;
|
|
622
|
+
}
|
|
623
|
+
results.push(entry);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const payload = {
|
|
627
|
+
command: "limits",
|
|
628
|
+
storagePath,
|
|
629
|
+
totalAccounts: accounts.length,
|
|
630
|
+
shownAccounts: results.length,
|
|
631
|
+
accounts: results,
|
|
632
|
+
};
|
|
633
|
+
printLimitsResult(payload, parsed.json);
|
|
634
|
+
return { exitCode: failedCount > 0 ? 1 : 0, action: "limits", storagePath };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function printLimitsResult(payload, json) {
|
|
638
|
+
if (json) {
|
|
639
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
console.log(`oc-codex-multi-auth limits`);
|
|
643
|
+
if (payload.message) console.log(payload.message);
|
|
644
|
+
console.log(`Storage: ${payload.storagePath}`);
|
|
645
|
+
if (payload.error) {
|
|
646
|
+
console.log(`Error: ${payload.error}`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
console.log(`Accounts: ${payload.totalAccounts}`);
|
|
650
|
+
for (const account of payload.accounts ?? []) {
|
|
651
|
+
const label = account.email ? `${account.label} (${account.email})` : account.label;
|
|
652
|
+
console.log(`- [${account.index}] ${label}`);
|
|
653
|
+
if (account.error) {
|
|
654
|
+
console.log(` Error: ${account.error}`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
for (const limit of account.limits ?? []) {
|
|
658
|
+
console.log(` ${limit.name}: ${limit.summary}`);
|
|
659
|
+
}
|
|
660
|
+
if ((account.limits ?? []).length === 0) {
|
|
661
|
+
console.log(" No usage windows reported yet.");
|
|
662
|
+
}
|
|
663
|
+
if (account.planType) console.log(` Plan: ${account.planType}`);
|
|
664
|
+
if (account.credits) console.log(` Credits: ${account.credits}`);
|
|
665
|
+
}
|
|
666
|
+
if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
|
|
667
|
+
}
|
|
668
|
+
|
|
481
669
|
export async function runStandaloneCommand(command, argv = [], options = {}) {
|
|
482
670
|
const parsed = parseStandaloneArgs(argv);
|
|
483
671
|
if (command === "diag") {
|
|
@@ -491,6 +679,9 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
|
|
|
491
679
|
if (command === "warm") {
|
|
492
680
|
return runWarmCommand(parsed, options);
|
|
493
681
|
}
|
|
682
|
+
if (command === "limits") {
|
|
683
|
+
return runLimitsCommand(parsed, options);
|
|
684
|
+
}
|
|
494
685
|
const { env = process.env } = options;
|
|
495
686
|
const storagePath = getStandaloneStoragePath(parsed, env);
|
|
496
687
|
const { storage, error } = await readStandaloneStorage(storagePath);
|
|
@@ -514,8 +705,6 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
|
|
|
514
705
|
payload.deep = parsed.deep;
|
|
515
706
|
payload.fixApplied = parsed.fix ? false : undefined;
|
|
516
707
|
payload.nextAction = totalAccounts > 0 ? "Run oc-codex-multi-auth health --json for scriptable checks." : "Run opencode auth login.";
|
|
517
|
-
} else if (command === "limits") {
|
|
518
|
-
payload.rateLimits = accounts.map((account) => ({ index: account.index, rateLimitResetTimes: account.rateLimitResetTimes }));
|
|
519
708
|
} else if (command === "health") {
|
|
520
709
|
payload.healthyCount = accounts.filter((account) => account.enabled && account.hasRefreshToken).length;
|
|
521
710
|
payload.unhealthyCount = accounts.filter((account) => !account.enabled || !account.hasRefreshToken).length;
|
|
@@ -602,6 +791,51 @@ function formatConfigDiff(existingConfig, nextConfig) {
|
|
|
602
791
|
return lines.join("\n");
|
|
603
792
|
}
|
|
604
793
|
|
|
794
|
+
function formatRedactedConfigDiff(existingConfig, nextConfig) {
|
|
795
|
+
const missing = Symbol("missing");
|
|
796
|
+
const changes = [];
|
|
797
|
+
const visit = (existing, next, path) => {
|
|
798
|
+
if (existing === missing) {
|
|
799
|
+
changes.push(`+ ${path}`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (next === missing) {
|
|
803
|
+
changes.push(`- ${path}`);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (Object.is(existing, next)) return;
|
|
807
|
+
|
|
808
|
+
if (Array.isArray(existing) && Array.isArray(next)) {
|
|
809
|
+
const length = Math.max(existing.length, next.length);
|
|
810
|
+
for (let index = 0; index < length; index += 1) {
|
|
811
|
+
visit(
|
|
812
|
+
index < existing.length ? existing[index] : missing,
|
|
813
|
+
index < next.length ? next[index] : missing,
|
|
814
|
+
`${path}[${index}]`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (isPlainObject(existing) && isPlainObject(next)) {
|
|
821
|
+
const keys = new Set([...Object.keys(existing), ...Object.keys(next)]);
|
|
822
|
+
for (const key of keys) {
|
|
823
|
+
visit(
|
|
824
|
+
Object.hasOwn(existing, key) ? existing[key] : missing,
|
|
825
|
+
Object.hasOwn(next, key) ? next[key] : missing,
|
|
826
|
+
`${path}.${key}`,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
changes.push(`~ ${path}`);
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
visit(existingConfig === undefined ? missing : existingConfig, nextConfig, "$");
|
|
836
|
+
return changes.length > 0 ? changes.join("\n") : "(no changes)";
|
|
837
|
+
}
|
|
838
|
+
|
|
605
839
|
function mergeFullTemplate(modernTemplate, legacyTemplate) {
|
|
606
840
|
const modernModels = modernTemplate.provider?.openai?.models ?? {};
|
|
607
841
|
const legacyModels = legacyTemplate.provider?.openai?.models ?? {};
|
|
@@ -657,6 +891,28 @@ async function renameWithWindowsRetry(sourcePath, destinationPath) {
|
|
|
657
891
|
}
|
|
658
892
|
}
|
|
659
893
|
|
|
894
|
+
async function removeWithWindowsRetry(path, options) {
|
|
895
|
+
let lastError = null;
|
|
896
|
+
|
|
897
|
+
for (let attempt = 0; attempt < WINDOWS_RENAME_RETRY_ATTEMPTS; attempt += 1) {
|
|
898
|
+
try {
|
|
899
|
+
await rm(path, options);
|
|
900
|
+
return;
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (isWindowsLockError(error)) {
|
|
903
|
+
lastError = error;
|
|
904
|
+
await delay(WINDOWS_RENAME_RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (lastError) {
|
|
912
|
+
throw lastError;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
660
916
|
async function writeFileAtomic(filePath, content) {
|
|
661
917
|
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
662
918
|
const tempPath = `${filePath}.${uniqueSuffix}.tmp`;
|
|
@@ -784,12 +1040,12 @@ async function clearCache(paths, dryRun, skipCacheClear) {
|
|
|
784
1040
|
log(`[dry-run] Would remove ${paths.cacheBunLock}`);
|
|
785
1041
|
} else {
|
|
786
1042
|
for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
|
|
787
|
-
await
|
|
1043
|
+
await removeWithWindowsRetry(cacheNodeModulesPath, { recursive: true, force: true });
|
|
788
1044
|
}
|
|
789
1045
|
for (const cachePackagePath of paths.cachePackagePaths) {
|
|
790
|
-
await
|
|
1046
|
+
await removeWithWindowsRetry(cachePackagePath, { recursive: true, force: true });
|
|
791
1047
|
}
|
|
792
|
-
await
|
|
1048
|
+
await removeWithWindowsRetry(paths.cacheBunLock, { force: true });
|
|
793
1049
|
}
|
|
794
1050
|
|
|
795
1051
|
await removePluginFromCachePackage(paths, dryRun);
|
|
@@ -804,20 +1060,37 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
804
1060
|
printHelp();
|
|
805
1061
|
throw new Error(`Unknown command: ${split.command}`);
|
|
806
1062
|
}
|
|
1063
|
+
const { env = process.env } = options;
|
|
1064
|
+
const paths = buildPaths(resolveHomeDirectory(env));
|
|
1065
|
+
if (split.kind === "update") {
|
|
1066
|
+
const parsedUpdate = parseUpdateArgs(split.argv);
|
|
1067
|
+
if (parsedUpdate.wantsHelp) {
|
|
1068
|
+
printHelp();
|
|
1069
|
+
return { exitCode: 0, action: "help" };
|
|
1070
|
+
}
|
|
1071
|
+
await clearCache(paths, parsedUpdate.dryRun, false);
|
|
1072
|
+
log(`\n${parsedUpdate.dryRun ? "Dry run complete." : "Cache cleared."} Restart OpenCode to install the latest plugin.`);
|
|
1073
|
+
return {
|
|
1074
|
+
exitCode: 0,
|
|
1075
|
+
action: "update",
|
|
1076
|
+
dryRun: Boolean(parsedUpdate.dryRun),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
807
1079
|
const parsed = parseCliArgs(split.argv);
|
|
808
1080
|
if (parsed.wantsHelp) {
|
|
809
1081
|
printHelp();
|
|
810
1082
|
return { exitCode: 0, action: "help" };
|
|
811
1083
|
}
|
|
812
1084
|
|
|
813
|
-
const {
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1085
|
+
const { configMode, dryRun, skipCacheClear, pluginOnly } = parsed;
|
|
1086
|
+
const effectiveConfigMode = pluginOnly ? "plugin-only" : configMode;
|
|
1087
|
+
const requiredTemplatePaths = pluginOnly
|
|
1088
|
+
? []
|
|
1089
|
+
: configMode === "modern"
|
|
1090
|
+
? [paths.modernTemplatePath]
|
|
1091
|
+
: configMode === "legacy"
|
|
1092
|
+
? [paths.legacyTemplatePath]
|
|
1093
|
+
: [paths.modernTemplatePath, paths.legacyTemplatePath];
|
|
821
1094
|
|
|
822
1095
|
for (const templatePath of requiredTemplatePaths) {
|
|
823
1096
|
if (!existsSync(templatePath)) {
|
|
@@ -825,40 +1098,51 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
825
1098
|
}
|
|
826
1099
|
}
|
|
827
1100
|
|
|
828
|
-
const template =
|
|
1101
|
+
const template = pluginOnly
|
|
1102
|
+
? { $schema: "https://opencode.ai/config.json", plugin: [PACKAGE_NAME] }
|
|
1103
|
+
: await loadTemplate(configMode, paths);
|
|
829
1104
|
template.plugin = [PACKAGE_NAME];
|
|
830
1105
|
const modelKeysToRemove = new Set(STALE_MANAGED_MODEL_KEYS);
|
|
831
|
-
if (configMode === "modern") {
|
|
1106
|
+
if (!pluginOnly && configMode === "modern") {
|
|
832
1107
|
for (const key of getTemplateModelKeys(await readJson(paths.legacyTemplatePath))) {
|
|
833
1108
|
modelKeysToRemove.add(key);
|
|
834
1109
|
}
|
|
835
1110
|
}
|
|
836
|
-
if (configMode === "legacy") {
|
|
1111
|
+
if (!pluginOnly && configMode === "legacy") {
|
|
837
1112
|
for (const key of getTemplateModelKeys(await readJson(paths.modernTemplatePath))) {
|
|
838
1113
|
modelKeysToRemove.add(key);
|
|
839
1114
|
}
|
|
840
1115
|
}
|
|
841
1116
|
|
|
842
|
-
let nextConfig =
|
|
1117
|
+
let nextConfig = pluginOnly
|
|
1118
|
+
? { $schema: template.$schema, plugin: [PACKAGE_NAME] }
|
|
1119
|
+
: template;
|
|
843
1120
|
let existingConfig;
|
|
844
1121
|
if (existsSync(paths.configPath)) {
|
|
845
|
-
const backupPath = await backupConfig(paths.configPath, dryRun);
|
|
846
|
-
log(`${dryRun ? "[dry-run] Would create backup" : "Backup created"}: ${backupPath}`);
|
|
847
|
-
|
|
848
1122
|
try {
|
|
849
1123
|
const existing = await readJson(paths.configPath);
|
|
1124
|
+
if (!isPlainObject(existing)) {
|
|
1125
|
+
throw new Error("config root must be a JSON object");
|
|
1126
|
+
}
|
|
850
1127
|
existingConfig = existing;
|
|
851
1128
|
const merged = { ...existing };
|
|
852
1129
|
merged.plugin = normalizePluginList(existing.plugin);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1130
|
+
if (!pluginOnly) {
|
|
1131
|
+
const provider = (existing.provider && typeof existing.provider === "object")
|
|
1132
|
+
? { ...existing.provider }
|
|
1133
|
+
: {};
|
|
1134
|
+
provider.openai = mergeOpenaiProvider(existing.provider?.openai, template.provider?.openai, {
|
|
1135
|
+
modelKeysToRemove,
|
|
1136
|
+
});
|
|
1137
|
+
merged.provider = provider;
|
|
1138
|
+
}
|
|
860
1139
|
nextConfig = merged;
|
|
861
1140
|
} catch (error) {
|
|
1141
|
+
if (pluginOnly) {
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
`Could not parse existing config (${formatErrorForLog(error)}). Refusing to replace it in --plugin-only mode.`,
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
862
1146
|
log(`Warning: Could not parse existing config (${formatErrorForLog(error)}). Replacing with template.`);
|
|
863
1147
|
existingConfig = undefined;
|
|
864
1148
|
nextConfig = template;
|
|
@@ -870,14 +1154,19 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
870
1154
|
let nextTuiConfig = mergeTuiConfig(undefined);
|
|
871
1155
|
let existingTuiConfig;
|
|
872
1156
|
if (existsSync(paths.tuiConfigPath)) {
|
|
873
|
-
const backupPath = await backupConfig(paths.tuiConfigPath, dryRun);
|
|
874
|
-
log(`${dryRun ? "[dry-run] Would create backup" : "Backup created"}: ${backupPath}`);
|
|
875
|
-
|
|
876
1157
|
try {
|
|
877
1158
|
const existing = await readJson(paths.tuiConfigPath);
|
|
1159
|
+
if (!isPlainObject(existing)) {
|
|
1160
|
+
throw new Error("TUI config root must be a JSON object");
|
|
1161
|
+
}
|
|
878
1162
|
existingTuiConfig = existing;
|
|
879
1163
|
nextTuiConfig = mergeTuiConfig(existing);
|
|
880
1164
|
} catch (error) {
|
|
1165
|
+
if (pluginOnly) {
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
`Could not parse existing TUI config (${formatErrorForLog(error)}). Refusing to replace it in --plugin-only mode.`,
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
881
1170
|
log(`Warning: Could not parse existing TUI config (${formatErrorForLog(error)}). Replacing with minimal TUI config.`);
|
|
882
1171
|
existingTuiConfig = undefined;
|
|
883
1172
|
nextTuiConfig = mergeTuiConfig(undefined);
|
|
@@ -886,40 +1175,60 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
886
1175
|
log("No existing TUI config found. Creating new global TUI config.");
|
|
887
1176
|
}
|
|
888
1177
|
|
|
1178
|
+
const configChanged = existingConfig === undefined || formatJson(existingConfig) !== formatJson(nextConfig);
|
|
1179
|
+
const tuiConfigChanged = existingTuiConfig === undefined || formatJson(existingTuiConfig) !== formatJson(nextTuiConfig);
|
|
889
1180
|
let wrote = false;
|
|
890
1181
|
if (dryRun) {
|
|
891
|
-
log(`[dry-run] Would write ${paths.configPath} using ${
|
|
1182
|
+
log(`[dry-run] ${configChanged ? "Would write" : "Would leave unchanged"} ${paths.configPath} using ${effectiveConfigMode} config`);
|
|
892
1183
|
log(`[dry-run] Diff for ${paths.configPath}:`);
|
|
893
|
-
log(
|
|
894
|
-
log(`[dry-run] Would write ${paths.tuiConfigPath} with the TUI status plugin`);
|
|
1184
|
+
log(formatRedactedConfigDiff(existingConfig, nextConfig));
|
|
1185
|
+
log(`[dry-run] ${tuiConfigChanged ? "Would write" : "Would leave unchanged"} ${paths.tuiConfigPath} with the TUI status plugin`);
|
|
895
1186
|
log(`[dry-run] Diff for ${paths.tuiConfigPath}:`);
|
|
896
|
-
log(
|
|
1187
|
+
log(formatRedactedConfigDiff(existingTuiConfig, nextTuiConfig));
|
|
897
1188
|
} else {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1189
|
+
if (configChanged) {
|
|
1190
|
+
if (existsSync(paths.configPath)) {
|
|
1191
|
+
const backupPath = await backupConfig(paths.configPath, false);
|
|
1192
|
+
log(`Backup created: ${backupPath}`);
|
|
1193
|
+
}
|
|
1194
|
+
await writeFileAtomic(paths.configPath, formatJson(nextConfig));
|
|
1195
|
+
wrote = true;
|
|
1196
|
+
log(`Wrote ${paths.configPath} (${effectiveConfigMode} config)`);
|
|
1197
|
+
} else {
|
|
1198
|
+
log(`Left ${paths.configPath} unchanged`);
|
|
1199
|
+
}
|
|
1200
|
+
if (tuiConfigChanged) {
|
|
1201
|
+
if (existsSync(paths.tuiConfigPath)) {
|
|
1202
|
+
const backupPath = await backupConfig(paths.tuiConfigPath, false);
|
|
1203
|
+
log(`Backup created: ${backupPath}`);
|
|
1204
|
+
}
|
|
1205
|
+
await writeFileAtomic(paths.tuiConfigPath, formatJson(nextTuiConfig));
|
|
1206
|
+
wrote = true;
|
|
1207
|
+
log(`Wrote ${paths.tuiConfigPath} (TUI status plugin)`);
|
|
1208
|
+
} else {
|
|
1209
|
+
log(`Left ${paths.tuiConfigPath} unchanged`);
|
|
1210
|
+
}
|
|
903
1211
|
}
|
|
904
1212
|
|
|
905
1213
|
await clearCache(paths, dryRun, skipCacheClear);
|
|
906
1214
|
|
|
907
1215
|
log("\nDone. Restart OpenCode to (re)install the plugin.");
|
|
908
1216
|
log("Example: opencode");
|
|
909
|
-
if (configMode === "modern") {
|
|
1217
|
+
if (!pluginOnly && configMode === "modern") {
|
|
910
1218
|
log("Note: Modern config intentionally shows 12 base OAuth model entries; use the variant picker for reasoning presets.");
|
|
911
1219
|
}
|
|
912
|
-
if (configMode === "legacy") {
|
|
1220
|
+
if (!pluginOnly && configMode === "legacy") {
|
|
913
1221
|
log("Note: Legacy config writes 53 explicit preset entries and is also safe for older OpenCode versions.");
|
|
914
1222
|
}
|
|
915
|
-
if (configMode === "full") {
|
|
1223
|
+
if (!pluginOnly && configMode === "full") {
|
|
916
1224
|
log("Note: Full config installs both compact base models and explicit preset entries for direct selector IDs.");
|
|
917
1225
|
}
|
|
918
1226
|
|
|
919
1227
|
return {
|
|
920
1228
|
exitCode: 0,
|
|
921
1229
|
action: "install",
|
|
922
|
-
configMode,
|
|
1230
|
+
configMode: effectiveConfigMode,
|
|
1231
|
+
pluginOnly,
|
|
923
1232
|
configPath: paths.configPath,
|
|
924
1233
|
tuiConfigPath: paths.tuiConfigPath,
|
|
925
1234
|
dryRun: Boolean(dryRun),
|
|
@@ -932,10 +1241,12 @@ export const __test = {
|
|
|
932
1241
|
backupConfig,
|
|
933
1242
|
copyFileWithWindowsRetry,
|
|
934
1243
|
formatConfigDiff,
|
|
1244
|
+
formatRedactedConfigDiff,
|
|
935
1245
|
mergeFullTemplate,
|
|
936
1246
|
mergeOpenaiProvider,
|
|
937
1247
|
mergeTuiConfig,
|
|
938
1248
|
parseCliArgs,
|
|
1249
|
+
removeWithWindowsRetry,
|
|
939
1250
|
runStandaloneCommand,
|
|
940
1251
|
splitCommandArgv,
|
|
941
1252
|
writeFileAtomic,
|