oc-codex-multi-auth 6.21.0 → 6.23.0
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 +168 -10
- package/config/README.md +53 -33
- package/config/opencode-legacy.json +129 -279
- package/config/opencode-modern.json +43 -132
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +293 -57
- package/dist/index.js.map +1 -1
- package/dist/lib/account-display.d.ts +46 -0
- package/dist/lib/account-display.d.ts.map +1 -1
- package/dist/lib/account-display.js +260 -0
- package/dist/lib/account-display.js.map +1 -1
- package/dist/lib/accounts/warm-request.d.ts.map +1 -1
- package/dist/lib/accounts/warm-request.js +9 -11
- package/dist/lib/accounts/warm-request.js.map +1 -1
- package/dist/lib/accounts.d.ts +4 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +20 -15
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +79 -2
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/auto-update-checker.d.ts +17 -1
- package/dist/lib/auto-update-checker.d.ts.map +1 -1
- package/dist/lib/auto-update-checker.js +49 -3
- package/dist/lib/auto-update-checker.js.map +1 -1
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +9 -3
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +4 -3
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +13 -11
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +75 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +152 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/constants.d.ts +13 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +13 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/plan-allotment.d.ts +72 -0
- package/dist/lib/plan-allotment.d.ts.map +1 -0
- package/dist/lib/plan-allotment.js +142 -0
- package/dist/lib/plan-allotment.js.map +1 -0
- package/dist/lib/plugin-origin.d.ts +77 -0
- package/dist/lib/plugin-origin.d.ts.map +1 -0
- package/dist/lib/plugin-origin.js +301 -0
- package/dist/lib/plugin-origin.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +12 -2
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +47 -16
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/quota-display.d.ts +40 -0
- package/dist/lib/quota-display.d.ts.map +1 -0
- package/dist/lib/quota-display.js +45 -0
- package/dist/lib/quota-display.js.map +1 -0
- package/dist/lib/quota-notifications.d.ts +2 -1
- package/dist/lib/quota-notifications.d.ts.map +1 -1
- package/dist/lib/quota-notifications.js +34 -14
- package/dist/lib/quota-notifications.js.map +1 -1
- package/dist/lib/quota-overview.d.ts +253 -0
- package/dist/lib/quota-overview.d.ts.map +1 -0
- package/dist/lib/quota-overview.js +707 -0
- package/dist/lib/quota-overview.js.map +1 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +84 -50
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts +10 -0
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
- package/dist/lib/request/helpers/model-map.js +39 -11
- package/dist/lib/request/helpers/model-map.js.map +1 -1
- package/dist/lib/request/helpers/responses-lite.d.ts.map +1 -1
- package/dist/lib/request/helpers/responses-lite.js +4 -1
- package/dist/lib/request/helpers/responses-lite.js.map +1 -1
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +37 -15
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/request/retry-budget.d.ts +26 -0
- package/dist/lib/request/retry-budget.d.ts.map +1 -1
- package/dist/lib/request/retry-budget.js +43 -0
- package/dist/lib/request/retry-budget.js.map +1 -1
- package/dist/lib/schemas.d.ts +73 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +53 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/backup.d.ts +33 -0
- package/dist/lib/storage/backup.d.ts.map +1 -1
- package/dist/lib/storage/backup.js +43 -7
- package/dist/lib/storage/backup.js.map +1 -1
- package/dist/lib/storage/credential-snapshots.d.ts +78 -0
- package/dist/lib/storage/credential-snapshots.d.ts.map +1 -0
- package/dist/lib/storage/credential-snapshots.js +302 -0
- package/dist/lib/storage/credential-snapshots.js.map +1 -0
- package/dist/lib/storage/export-import.d.ts.map +1 -1
- package/dist/lib/storage/export-import.js +7 -0
- package/dist/lib/storage/export-import.js.map +1 -1
- package/dist/lib/storage/flagged.d.ts.map +1 -1
- package/dist/lib/storage/flagged.js +23 -1
- package/dist/lib/storage/flagged.js.map +1 -1
- package/dist/lib/storage/load-save.d.ts +4 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +50 -0
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/storage/paths.d.ts +1 -0
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +1 -1
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage/test-home-guard.d.ts +35 -0
- package/dist/lib/storage/test-home-guard.d.ts.map +1 -0
- package/dist/lib/storage/test-home-guard.js +59 -0
- package/dist/lib/storage/test-home-guard.js.map +1 -0
- package/dist/lib/tools/codex-dashboard.d.ts.map +1 -1
- package/dist/lib/tools/codex-dashboard.js +3 -2
- package/dist/lib/tools/codex-dashboard.js.map +1 -1
- package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
- package/dist/lib/tools/codex-doctor.js +16 -0
- package/dist/lib/tools/codex-doctor.js.map +1 -1
- package/dist/lib/tools/codex-health.d.ts.map +1 -1
- package/dist/lib/tools/codex-health.js +10 -2
- package/dist/lib/tools/codex-health.js.map +1 -1
- package/dist/lib/tools/codex-label.d.ts.map +1 -1
- package/dist/lib/tools/codex-label.js +1 -0
- package/dist/lib/tools/codex-label.js.map +1 -1
- package/dist/lib/tools/codex-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +13 -9
- package/dist/lib/tools/codex-limits.js.map +1 -1
- package/dist/lib/tools/codex-list.d.ts.map +1 -1
- package/dist/lib/tools/codex-list.js +28 -3
- package/dist/lib/tools/codex-list.js.map +1 -1
- package/dist/lib/tools/codex-note.d.ts.map +1 -1
- package/dist/lib/tools/codex-note.js +4 -1
- package/dist/lib/tools/codex-note.js.map +1 -1
- package/dist/lib/tools/codex-pool.d.ts.map +1 -1
- package/dist/lib/tools/codex-pool.js +6 -2
- package/dist/lib/tools/codex-pool.js.map +1 -1
- package/dist/lib/tools/codex-refresh.d.ts.map +1 -1
- package/dist/lib/tools/codex-refresh.js +4 -1
- package/dist/lib/tools/codex-refresh.js.map +1 -1
- package/dist/lib/tools/codex-remove.d.ts.map +1 -1
- package/dist/lib/tools/codex-remove.js +1 -0
- package/dist/lib/tools/codex-remove.js.map +1 -1
- package/dist/lib/tools/codex-reset.d.ts.map +1 -1
- package/dist/lib/tools/codex-reset.js +13 -7
- package/dist/lib/tools/codex-reset.js.map +1 -1
- package/dist/lib/tools/codex-status.d.ts.map +1 -1
- package/dist/lib/tools/codex-status.js +25 -2
- package/dist/lib/tools/codex-status.js.map +1 -1
- package/dist/lib/tools/codex-switch.d.ts.map +1 -1
- package/dist/lib/tools/codex-switch.js +1 -0
- package/dist/lib/tools/codex-switch.js.map +1 -1
- package/dist/lib/tools/codex-tag.d.ts.map +1 -1
- package/dist/lib/tools/codex-tag.js +4 -1
- package/dist/lib/tools/codex-tag.js.map +1 -1
- package/dist/lib/tools/codex-warm.d.ts.map +1 -1
- package/dist/lib/tools/codex-warm.js +1 -0
- package/dist/lib/tools/codex-warm.js.map +1 -1
- package/dist/lib/tools/index.d.ts +9 -0
- package/dist/lib/tools/index.d.ts.map +1 -1
- package/dist/lib/tools/index.js.map +1 -1
- package/dist/lib/tui-quota-cache.d.ts +35 -0
- package/dist/lib/tui-quota-cache.d.ts.map +1 -1
- package/dist/lib/tui-quota-cache.js +76 -11
- package/dist/lib/tui-quota-cache.js.map +1 -1
- package/dist/lib/tui-quota-overview.d.ts +55 -0
- package/dist/lib/tui-quota-overview.d.ts.map +1 -0
- package/dist/lib/tui-quota-overview.js +209 -0
- package/dist/lib/tui-quota-overview.js.map +1 -0
- package/dist/lib/tui-status.d.ts +55 -0
- package/dist/lib/tui-status.d.ts.map +1 -1
- package/dist/lib/tui-status.js +155 -14
- package/dist/lib/tui-status.js.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts +2 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +10 -6
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/tui.d.ts +55 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +511 -29
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-oc-codex-multi-auth-core.js +583 -66
|
@@ -1,17 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
3
|
import { copyFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
|
|
7
8
|
const PACKAGE_NAME = "oc-codex-multi-auth";
|
|
8
9
|
const LEGACY_PACKAGE_NAMES = ["oc-chatgpt-multi-auth"];
|
|
10
|
+
const ORIGIN_HISTORY_FILE_NAME = "oc-codex-multi-auth-origin.json";
|
|
9
11
|
const WINDOWS_RENAME_RETRY_ATTEMPTS = 5;
|
|
10
12
|
const WINDOWS_RENAME_RETRY_BASE_DELAY_MS = 10;
|
|
11
13
|
const STALE_MANAGED_MODEL_KEYS = new Set([
|
|
12
14
|
"gpt-5.2",
|
|
13
15
|
"gpt-5.3-codex",
|
|
14
16
|
"gpt-5.4",
|
|
17
|
+
// Retired per OpenAI's docs and dropped from the templates: gpt-5.4-mini left
|
|
18
|
+
// Codex (ChatGPT sign-in) on 2026-08-31; gpt-5-codex and the gpt-5.1-codex
|
|
19
|
+
// family were shut down on 2026-07-23 (developers.openai.com/api/docs/deprecations).
|
|
20
|
+
"gpt-5.4-mini",
|
|
21
|
+
"gpt-5-codex",
|
|
22
|
+
"gpt-5.1-codex",
|
|
23
|
+
"gpt-5.1-codex-max",
|
|
24
|
+
"gpt-5.1-codex-mini",
|
|
25
|
+
...["none", "low", "medium", "high", "xhigh"].map((e) => `gpt-5.4-mini-${e}`),
|
|
26
|
+
...["low", "medium", "high"].map((e) => `gpt-5-codex-${e}`),
|
|
27
|
+
...["low", "medium", "high"].map((e) => `gpt-5.1-codex-${e}`),
|
|
28
|
+
...["low", "medium", "high", "xhigh"].map((e) => `gpt-5.1-codex-max-${e}`),
|
|
29
|
+
...["medium", "high"].map((e) => `gpt-5.1-codex-mini-${e}`),
|
|
15
30
|
]);
|
|
16
31
|
const STANDALONE_COMMANDS = new Set(["doctor", "status", "list", "limits", "dashboard", "health", "diag", "warm"]);
|
|
17
32
|
const INSTALLER_COMMANDS = new Set(["install"]);
|
|
@@ -98,9 +113,9 @@ function printHelp() {
|
|
|
98
113
|
" - Clears OpenCode plugin cache\n\n" +
|
|
99
114
|
"Options:\n" +
|
|
100
115
|
" --plugin-only Register plugins without changing provider.openai\n" +
|
|
101
|
-
" --modern Force compact modern config (
|
|
102
|
-
" --full Install compact base models plus
|
|
103
|
-
" --legacy Force explicit legacy config (
|
|
116
|
+
" --modern Force compact modern config (10 base OAuth models + --variant presets)\n" +
|
|
117
|
+
" --full Install compact base models plus 53 explicit selector entries\n" +
|
|
118
|
+
" --legacy Force explicit legacy config (53 preset model entries)\n" +
|
|
104
119
|
" --dry-run Show actions without writing\n" +
|
|
105
120
|
" --no-cache-clear Skip clearing OpenCode cache\n"
|
|
106
121
|
);
|
|
@@ -150,6 +165,7 @@ function buildPaths(homeDir) {
|
|
|
150
165
|
]),
|
|
151
166
|
cacheBunLock: join(cacheDir, "bun.lock"),
|
|
152
167
|
cachePackageJson: join(cacheDir, "package.json"),
|
|
168
|
+
originHistoryPath: join(homeDir, ".opencode", ORIGIN_HISTORY_FILE_NAME),
|
|
153
169
|
modernTemplatePath,
|
|
154
170
|
legacyTemplatePath,
|
|
155
171
|
};
|
|
@@ -200,47 +216,323 @@ function parseUpdateArgs(argv) {
|
|
|
200
216
|
};
|
|
201
217
|
}
|
|
202
218
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
219
|
+
const MANAGED_PACKAGE_ENTRY = "managed-package";
|
|
220
|
+
const LOCAL_CHECKOUT_ENTRY = "local-checkout";
|
|
221
|
+
const UNRELATED_ENTRY = "unrelated";
|
|
222
|
+
const DECLARED_NAME_LOOKUP_DEPTH = 3;
|
|
223
|
+
|
|
224
|
+
function pluginEntrySpecifier(entry) {
|
|
225
|
+
if (typeof entry === "string") return entry;
|
|
226
|
+
// `[specifier, options]` configures a plugin without changing where it loads from.
|
|
227
|
+
if (Array.isArray(entry) && typeof entry[0] === "string") return entry[0];
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Spellings that can only mean a location on disk: absolute paths, `~`,
|
|
233
|
+
* `./`/`../`, Windows drive letters, and UNC shares. Anything else that merely
|
|
234
|
+
* contains a separator (`@scope/name`, git URLs, `npm:` aliases) is ambiguous
|
|
235
|
+
* and counts as a path only when it resolves on this machine.
|
|
236
|
+
*/
|
|
237
|
+
function isExplicitPathSpecifier(specifier) {
|
|
238
|
+
return (
|
|
239
|
+
/^[a-zA-Z]:[\\/]/.test(specifier) ||
|
|
240
|
+
/^[\\/]/.test(specifier) ||
|
|
241
|
+
/^~[\\/]/.test(specifier) ||
|
|
242
|
+
/^\.\.?[\\/]/.test(specifier)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The path an entry names, exactly as the config spells it - and only when the
|
|
248
|
+
* specifier actually is a path. A `/` alone does not make one: registry and
|
|
249
|
+
* URL spellings can end in `oc-codex-multi-auth` without naming this package's
|
|
250
|
+
* checkout, so an ambiguous specifier counts only when it resolves on disk.
|
|
251
|
+
*/
|
|
252
|
+
function pluginEntryPath(specifier, baseDirectory) {
|
|
253
|
+
const trimmed = specifier.trim();
|
|
254
|
+
if (!trimmed) return null;
|
|
255
|
+
if (/^file:\/\//i.test(trimmed)) {
|
|
256
|
+
try {
|
|
257
|
+
return fileURLToPath(trimmed);
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (isExplicitPathSpecifier(trimmed)) return trimmed;
|
|
263
|
+
if (!trimmed.includes("/") && !trimmed.includes("\\")) return null;
|
|
264
|
+
const inspectionPath = resolveInspectionPath(trimmed, baseDirectory);
|
|
265
|
+
return inspectionPath && existsSync(inspectionPath) ? trimmed : null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function pluginPathSegments(entryPath) {
|
|
269
|
+
return entryPath.replaceAll("\\", "/").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Compared as written, without resolving symlinks: this asks whether an entry
|
|
274
|
+
* names something `clearCache` removes, and `clearCache` removes the paths
|
|
275
|
+
* exactly as it spells them - `rm` unlinks a symlink rather than descending
|
|
276
|
+
* into it. Cache eviction resolves symlinks because it decides the opposite
|
|
277
|
+
* question, whether a recursive delete is safe.
|
|
278
|
+
*/
|
|
279
|
+
function isInsideDirectory(candidate, directory, platform) {
|
|
280
|
+
const fold = (value) => (platform === "win32" ? value.toLowerCase() : value);
|
|
281
|
+
const relativePath = relative(fold(resolve(directory)), fold(resolve(candidate)));
|
|
282
|
+
return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isPackageManagerPath(entryPath, options = {}) {
|
|
286
|
+
const { platform = process.platform, cacheDirectory, inspectionPath } = options;
|
|
287
|
+
// Windows reaches one directory under many spellings, so `NODE_MODULES`
|
|
288
|
+
// there is the same package-manager output as `node_modules`. Elsewhere the
|
|
289
|
+
// two are different directories and must stay so.
|
|
290
|
+
const segments = pluginPathSegments(entryPath).map((segment) =>
|
|
291
|
+
platform === "win32" ? segment.toLowerCase() : segment,
|
|
292
|
+
);
|
|
293
|
+
if (
|
|
294
|
+
segments.some(
|
|
295
|
+
(segment, index) =>
|
|
296
|
+
segment === "node_modules" ||
|
|
297
|
+
// OpenCode's plugin cache spells the version into the directory name.
|
|
298
|
+
// A `packages/` directory without one is an ordinary monorepo.
|
|
299
|
+
(segments[index - 1] === "packages" && segment.includes("@")),
|
|
300
|
+
)
|
|
301
|
+
) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
// The cache is where this installer puts its own copies, and `clearCache`
|
|
305
|
+
// empties it on the same run. Reading spelling alone leaves the cache's
|
|
306
|
+
// unversioned `packages/<name>` looking like somebody's monorepo, so the
|
|
307
|
+
// entry is kept while the directory under it is deleted - a config left
|
|
308
|
+
// pointing at nothing. Whose directory it is settles that; the spelling
|
|
309
|
+
// cannot.
|
|
310
|
+
return Boolean(
|
|
311
|
+
cacheDirectory &&
|
|
312
|
+
inspectionPath &&
|
|
313
|
+
isInsideDirectory(inspectionPath, cacheDirectory, platform),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Where an entry points, for reading metadata about it only. OpenCode resolves
|
|
319
|
+
* a relative entry against the config file that declares it, so that directory
|
|
320
|
+
* is what makes such a path mean anything; the installer's working directory
|
|
321
|
+
* would name somewhere else entirely. Null when a relative entry arrives with
|
|
322
|
+
* no declaring directory to resolve it against.
|
|
323
|
+
*/
|
|
324
|
+
function resolveInspectionPath(entryPath, baseDirectory) {
|
|
325
|
+
if (isAbsolute(entryPath)) return entryPath;
|
|
326
|
+
return baseDirectory ? resolve(baseDirectory, entryPath) : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Last-resort identification for a path that is not present on this machine.
|
|
331
|
+
* Spelling alone never authorizes deleting an entry; it only names the package a
|
|
332
|
+
* missing path was probably meant to point at.
|
|
333
|
+
*/
|
|
334
|
+
function managedNameFromPathSpelling(entryPath) {
|
|
335
|
+
const segments = pluginPathSegments(entryPath);
|
|
336
|
+
const last = segments.at(-1) === "dist" ? segments.at(-2) : segments.at(-1);
|
|
337
|
+
if (!last) return null;
|
|
338
|
+
let candidate = last.toLowerCase();
|
|
206
339
|
try {
|
|
207
|
-
|
|
340
|
+
candidate = decodeURIComponent(candidate);
|
|
208
341
|
} catch {
|
|
209
|
-
// Keep the raw
|
|
342
|
+
// Keep the raw segment when it carries a malformed escape.
|
|
210
343
|
}
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
344
|
+
const versionSuffix = candidate.indexOf("@");
|
|
345
|
+
if (versionSuffix > 0) candidate = candidate.slice(0, versionSuffix);
|
|
346
|
+
return getManagedPackageNames().find((name) => name.toLowerCase() === candidate) ?? null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function readDeclaredPackageName(directoryPath) {
|
|
350
|
+
try {
|
|
351
|
+
const parsed = JSON.parse(readFileSync(join(directoryPath, "package.json"), "utf8"));
|
|
352
|
+
const name = parsed?.name;
|
|
353
|
+
return typeof name === "string" && name.trim() ? name.trim() : null;
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
214
356
|
}
|
|
215
|
-
return normalized;
|
|
216
357
|
}
|
|
217
358
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
359
|
+
/** An entry may point at a build output inside the package, so walk upwards. */
|
|
360
|
+
function resolveDeclaredPackageName(entryPath) {
|
|
361
|
+
if (!isAbsolute(entryPath)) return null;
|
|
362
|
+
let current = resolve(entryPath);
|
|
363
|
+
for (let depth = 0; depth <= DECLARED_NAME_LOOKUP_DEPTH; depth += 1) {
|
|
364
|
+
const name = readDeclaredPackageName(current);
|
|
365
|
+
if (name) return name;
|
|
366
|
+
const parent = dirname(current);
|
|
367
|
+
if (parent === current) return null;
|
|
368
|
+
current = parent;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Decides what a plugin entry is, by identity rather than by spelling.
|
|
375
|
+
*
|
|
376
|
+
* The distinction that matters is not which package an entry names but who
|
|
377
|
+
* chose the location. A bare specifier or a path inside `node_modules` is a
|
|
378
|
+
* reference the installer itself produced and may retire. Any other path is
|
|
379
|
+
* somewhere a human deliberately pointed OpenCode - a checkout of this package
|
|
380
|
+
* being developed on, most often - and is never the installer's to remove.
|
|
381
|
+
*/
|
|
382
|
+
function classifyPluginEntry(entry, options = {}) {
|
|
383
|
+
const {
|
|
384
|
+
resolveDeclaredName = resolveDeclaredPackageName,
|
|
385
|
+
baseDirectory,
|
|
386
|
+
cacheDirectory,
|
|
387
|
+
platform = process.platform,
|
|
388
|
+
} = options;
|
|
389
|
+
const specifier = pluginEntrySpecifier(entry);
|
|
390
|
+
if (specifier === null) return { kind: UNRELATED_ENTRY, name: null };
|
|
391
|
+
|
|
392
|
+
const entryPath = pluginEntryPath(specifier, baseDirectory);
|
|
393
|
+
if (entryPath === null) {
|
|
394
|
+
const bare = specifier.trim().toLowerCase();
|
|
395
|
+
const name = getManagedPackageNames().find(
|
|
396
|
+
(managed) =>
|
|
397
|
+
bare === managed.toLowerCase() || bare.startsWith(`${managed.toLowerCase()}@`),
|
|
398
|
+
);
|
|
399
|
+
return name
|
|
400
|
+
? { kind: MANAGED_PACKAGE_ENTRY, name }
|
|
401
|
+
: { kind: UNRELATED_ENTRY, name: null };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const inspectionPath = resolveInspectionPath(entryPath, baseDirectory);
|
|
405
|
+
const declaredName = inspectionPath ? resolveDeclaredName(inspectionPath) : null;
|
|
406
|
+
const managedName = declaredName
|
|
407
|
+
? getManagedPackageNames().find(
|
|
408
|
+
(managed) => managed.toLowerCase() === declaredName.toLowerCase(),
|
|
409
|
+
) ?? null
|
|
410
|
+
: managedNameFromPathSpelling(entryPath);
|
|
411
|
+
|
|
412
|
+
if (!managedName) return { kind: UNRELATED_ENTRY, name: null };
|
|
413
|
+
|
|
414
|
+
return isPackageManagerPath(entryPath, { platform, cacheDirectory, inspectionPath })
|
|
415
|
+
? { kind: MANAGED_PACKAGE_ENTRY, name: managedName }
|
|
416
|
+
: {
|
|
417
|
+
kind: LOCAL_CHECKOUT_ENTRY,
|
|
418
|
+
name: managedName,
|
|
419
|
+
path: inspectionPath ?? entryPath,
|
|
420
|
+
resolvesOnDisk: Boolean(inspectionPath && existsSync(inspectionPath)),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Ensures this plugin is registered exactly once, without changing how an
|
|
426
|
+
* existing registration is spelled. Appending the published package name is the
|
|
427
|
+
* fallback for a config that does not reference the plugin at all, not the
|
|
428
|
+
* canonical form every config is rewritten into.
|
|
429
|
+
*/
|
|
430
|
+
function normalizePluginList(list, onNotice, options = {}) {
|
|
431
|
+
const entries = Array.isArray(list)
|
|
432
|
+
? list.filter((entry) => entry !== null && entry !== undefined && entry !== "")
|
|
433
|
+
: [];
|
|
434
|
+
const classifications = entries.map((entry) => classifyPluginEntry(entry, options));
|
|
435
|
+
// A checkout of this package already IS the registration, so a published
|
|
436
|
+
// entry beside it is a second copy of the same plugin for OpenCode to load.
|
|
437
|
+
// `options.checkoutRegistered` carries the same fact across config files: a
|
|
438
|
+
// checkout registered only in opencode.json still suppresses the published
|
|
439
|
+
// name in tui.json, and vice versa. Only a checkout of the CURRENT package
|
|
440
|
+
// counts: the former name is valid for cleanup, never as the registration
|
|
441
|
+
// the installer exists to ensure.
|
|
442
|
+
const checkoutRegistered = options.checkoutRegistered === true || classifications.some(
|
|
443
|
+
(classification) =>
|
|
444
|
+
classification.kind === LOCAL_CHECKOUT_ENTRY && classification.name === PACKAGE_NAME,
|
|
445
|
+
);
|
|
446
|
+
const kept = [];
|
|
447
|
+
let keptPublishedName = false;
|
|
448
|
+
|
|
449
|
+
entries.forEach((entry, index) => {
|
|
450
|
+
const classification = classifications[index];
|
|
451
|
+
|
|
452
|
+
if (classification.kind === LOCAL_CHECKOUT_ENTRY) {
|
|
453
|
+
kept.push(entry);
|
|
454
|
+
if (classification.resolvesOnDisk === false) {
|
|
455
|
+
onNotice?.(
|
|
456
|
+
`Warning: keeping ${classification.path} registered, but it does not resolve on disk; ` +
|
|
457
|
+
"the plugin may not load until the path exists again.",
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
onNotice?.(
|
|
461
|
+
`Keeping the local ${classification.name} checkout registered at ${classification.path}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (classification.kind === MANAGED_PACKAGE_ENTRY) {
|
|
468
|
+
// Retire stale duplicates, version pins, renamed packages, and paths
|
|
469
|
+
// into package-manager output; keep one published-name entry in place
|
|
470
|
+
// unless a checkout already covers it.
|
|
471
|
+
const isPublishedName = pluginEntrySpecifier(entry) === PACKAGE_NAME;
|
|
472
|
+
if (isPublishedName && !checkoutRegistered && !keptPublishedName) {
|
|
473
|
+
keptPublishedName = true;
|
|
474
|
+
kept.push(entry);
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
kept.push(entry);
|
|
228
480
|
});
|
|
481
|
+
|
|
482
|
+
return checkoutRegistered || keptPublishedName ? kept : [...kept, PACKAGE_NAME];
|
|
229
483
|
}
|
|
230
484
|
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
485
|
+
function readLocalCheckoutSightings(historyPath) {
|
|
486
|
+
try {
|
|
487
|
+
const parsed = JSON.parse(readFileSync(historyPath, "utf8"));
|
|
488
|
+
const sightings = parsed?.sightings;
|
|
489
|
+
if (!Array.isArray(sightings)) return [];
|
|
490
|
+
return sightings.filter(
|
|
491
|
+
(sighting) =>
|
|
492
|
+
sighting &&
|
|
493
|
+
typeof sighting === "object" &&
|
|
494
|
+
sighting.isLocalCheckout === true &&
|
|
495
|
+
typeof sighting.root === "string" &&
|
|
496
|
+
typeof sighting.lastSeen === "string" &&
|
|
497
|
+
getManagedPackageNames().includes(sighting.name),
|
|
498
|
+
);
|
|
499
|
+
} catch {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* A checkout the plugin has run from that the finished config does not
|
|
506
|
+
* register. Reported rather than restored: config history is evidence of what
|
|
507
|
+
* happened, not authority over what the user wants registered now.
|
|
508
|
+
*/
|
|
509
|
+
function findUnregisteredLocalCheckout(pluginList, historyPath, options = {}) {
|
|
510
|
+
const entries = Array.isArray(pluginList) ? pluginList : [];
|
|
511
|
+
if (entries.some((entry) => classifyPluginEntry(entry, options).kind === LOCAL_CHECKOUT_ENTRY)) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const latest = readLocalCheckoutSightings(historyPath)
|
|
515
|
+
.sort((left, right) => (Date.parse(left.lastSeen) || 0) - (Date.parse(right.lastSeen) || 0))
|
|
516
|
+
.at(-1);
|
|
517
|
+
if (!latest) return null;
|
|
518
|
+
// The directory has to still hold the package that was recorded there. A
|
|
519
|
+
// path gets reused - a checkout deleted and something else cloned into its
|
|
520
|
+
// place - and a recorded path that now declares another project would
|
|
521
|
+
// otherwise be offered as somewhere to point OpenCode back at.
|
|
522
|
+
const declaredName = resolveDeclaredPackageName(latest.root);
|
|
523
|
+
if (!declaredName || declaredName.toLowerCase() !== String(latest.name).toLowerCase()) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
return latest;
|
|
235
527
|
}
|
|
236
528
|
|
|
237
|
-
function mergeTuiConfig(existingConfig) {
|
|
529
|
+
function mergeTuiConfig(existingConfig, onNotice, options = {}) {
|
|
238
530
|
const existing = isPlainObject(existingConfig) ? { ...existingConfig } : {};
|
|
239
531
|
const next = { ...existing };
|
|
240
532
|
if (typeof next.$schema !== "string" || !next.$schema.trim()) {
|
|
241
533
|
next.$schema = "https://opencode.ai/tui.json";
|
|
242
534
|
}
|
|
243
|
-
next.plugin = normalizePluginList(existing.plugin);
|
|
535
|
+
next.plugin = normalizePluginList(existing.plugin, onNotice, options);
|
|
244
536
|
return next;
|
|
245
537
|
}
|
|
246
538
|
|
|
@@ -410,24 +702,167 @@ function accountIdSuffix(accountId, includeSensitive) {
|
|
|
410
702
|
return accountId.slice(-4);
|
|
411
703
|
}
|
|
412
704
|
|
|
705
|
+
// A member id is what tells two seats of one Business workspace apart, and no
|
|
706
|
+
// fixed-length tail always does it: member ids sharing a six-character tail
|
|
707
|
+
// were observed, and in a real nine-seat pool the ids are 67 characters with
|
|
708
|
+
// no shared tail at all, so growing a tail until it separates them prints most
|
|
709
|
+
// of the id in every row. The renderer below mirrors `resolveSeatRenderer` in
|
|
710
|
+
// lib/account-display.ts - a tail, else one window anchored where the ids first
|
|
711
|
+
// diverge, else short windows at each position where a pair first differs
|
|
712
|
+
// joined by `..`, else a hash prefix, each capped - and the id whole only if
|
|
713
|
+
// none of those separate them, which needs a 128-bit SHA-256 collision. The
|
|
714
|
+
// hash outcome is reachable, and it prints a value that cannot be matched
|
|
715
|
+
// against the id by eye.
|
|
716
|
+
const STANDALONE_SEAT_MAX_LENGTH = 12;
|
|
717
|
+
const STANDALONE_SEAT_HASH_LENGTHS = [8, 12, 16, 24, 32];
|
|
718
|
+
const STANDALONE_SEAT_WINDOW_SEPARATOR = "..";
|
|
719
|
+
|
|
720
|
+
function seatIsDisclosable(accountUserId, includeSensitive) {
|
|
721
|
+
if (!accountUserId) return false;
|
|
722
|
+
return includeSensitive || accountUserId.length >= MASK_MIN_LENGTH;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function seatTail(accountUserId, length) {
|
|
726
|
+
return accountUserId.length > length ? accountUserId.slice(-length) : accountUserId;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function seatWindow(accountUserId, start, length) {
|
|
730
|
+
if (accountUserId.length <= length) return accountUserId;
|
|
731
|
+
const begin = Math.max(0, Math.min(start, accountUserId.length - length));
|
|
732
|
+
return accountUserId.slice(begin, begin + length);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function seatCommonPrefixLength(values) {
|
|
736
|
+
const [first] = values;
|
|
737
|
+
if (first === undefined) return 0;
|
|
738
|
+
let shared = first.length;
|
|
739
|
+
for (const value of values) {
|
|
740
|
+
let index = 0;
|
|
741
|
+
while (index < shared && index < value.length && first[index] === value[index]) {
|
|
742
|
+
index += 1;
|
|
743
|
+
}
|
|
744
|
+
shared = index;
|
|
745
|
+
if (shared === 0) break;
|
|
746
|
+
}
|
|
747
|
+
return shared;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function seatFirstDivergence(left, right) {
|
|
751
|
+
const limit = Math.min(left.length, right.length);
|
|
752
|
+
let index = 0;
|
|
753
|
+
while (index < limit && left[index] === right[index]) index += 1;
|
|
754
|
+
return index;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// For every pair, the first index at which that pair differs - not every index
|
|
758
|
+
// where the ids disagree, which across a handful of random-looking ids is
|
|
759
|
+
// nearly all of them and localizes nothing.
|
|
760
|
+
function seatDivergenceAnchors(values) {
|
|
761
|
+
const anchors = new Set();
|
|
762
|
+
for (let left = 0; left < values.length; left += 1) {
|
|
763
|
+
for (let right = left + 1; right < values.length; right += 1) {
|
|
764
|
+
anchors.add(seatFirstDivergence(values[left], values[right]));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return [...anchors].sort((left, right) => left - right);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function seatAnchorWindowStarts(anchors, width) {
|
|
771
|
+
const starts = [];
|
|
772
|
+
for (const anchor of anchors) {
|
|
773
|
+
const last = starts[starts.length - 1];
|
|
774
|
+
if (last !== undefined && anchor < last + width) continue;
|
|
775
|
+
starts.push(anchor);
|
|
776
|
+
}
|
|
777
|
+
return starts;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function resolveStandaloneSeatRenderer(accountUserIds, includeSensitive) {
|
|
781
|
+
// Starts at the length the mask above allows, so masked output widens only
|
|
782
|
+
// when leaving it short would print a lie.
|
|
783
|
+
const base = includeSensitive ? 6 : 4;
|
|
784
|
+
const distinct = [];
|
|
785
|
+
const seen = new Set();
|
|
786
|
+
for (const accountUserId of accountUserIds) {
|
|
787
|
+
if (!seatIsDisclosable(accountUserId, includeSensitive)) continue;
|
|
788
|
+
if (seen.has(accountUserId)) continue;
|
|
789
|
+
seen.add(accountUserId);
|
|
790
|
+
distinct.push(accountUserId);
|
|
791
|
+
}
|
|
792
|
+
const atBase = (accountUserId) => seatTail(accountUserId, base);
|
|
793
|
+
if (distinct.length <= 1) return atBase;
|
|
794
|
+
|
|
795
|
+
const separates = (render) => new Set(distinct.map(render)).size === distinct.length;
|
|
796
|
+
|
|
797
|
+
for (let length = base; length <= STANDALONE_SEAT_MAX_LENGTH; length += 1) {
|
|
798
|
+
const render = (accountUserId) => seatTail(accountUserId, length);
|
|
799
|
+
if (separates(render)) return render;
|
|
800
|
+
}
|
|
801
|
+
const start = seatCommonPrefixLength(distinct);
|
|
802
|
+
for (let length = base; length <= STANDALONE_SEAT_MAX_LENGTH; length += 1) {
|
|
803
|
+
const render = (accountUserId) => seatWindow(accountUserId, start, length);
|
|
804
|
+
if (separates(render)) return render;
|
|
805
|
+
}
|
|
806
|
+
const anchors = seatDivergenceAnchors(distinct);
|
|
807
|
+
for (let width = 2; width <= STANDALONE_SEAT_MAX_LENGTH; width += 1) {
|
|
808
|
+
const starts = seatAnchorWindowStarts(anchors, width);
|
|
809
|
+
const rendered =
|
|
810
|
+
starts.length * width + (starts.length - 1) * STANDALONE_SEAT_WINDOW_SEPARATOR.length;
|
|
811
|
+
// Skipped, not abandoned: a wider window can span two nearby anchors
|
|
812
|
+
// that needed one window each, so the cost falls as the window count
|
|
813
|
+
// does. Mirrors `resolveSeatRenderer` in lib/account-display.ts, where
|
|
814
|
+
// the measured counter-example is written out.
|
|
815
|
+
if (rendered > STANDALONE_SEAT_MAX_LENGTH) continue;
|
|
816
|
+
const render = (accountUserId) =>
|
|
817
|
+
starts
|
|
818
|
+
.map((windowStart) => accountUserId.slice(windowStart, windowStart + width))
|
|
819
|
+
.join(STANDALONE_SEAT_WINDOW_SEPARATOR);
|
|
820
|
+
if (separates(render)) return render;
|
|
821
|
+
}
|
|
822
|
+
for (const length of STANDALONE_SEAT_HASH_LENGTHS) {
|
|
823
|
+
const render = (accountUserId) => createHash("sha256").update(accountUserId).digest("hex").slice(0, length);
|
|
824
|
+
if (separates(render)) return render;
|
|
825
|
+
}
|
|
826
|
+
return (accountUserId) => accountUserId;
|
|
827
|
+
}
|
|
828
|
+
|
|
413
829
|
function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
|
|
414
830
|
const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
|
|
415
831
|
const normalizedTag = typeof tag === "string" ? tag.trim().toLowerCase() : "";
|
|
416
|
-
|
|
832
|
+
const entries = accounts
|
|
417
833
|
.map((account, index) => ({ account, index }))
|
|
418
834
|
.filter(({ account }) => !normalizedTag ||
|
|
419
835
|
(Array.isArray(account?.accountTags) &&
|
|
420
|
-
account.accountTags.some((entry) => String(entry).toLowerCase() === normalizedTag)))
|
|
836
|
+
account.accountTags.some((entry) => String(entry).toLowerCase() === normalizedTag)));
|
|
837
|
+
const renderSeat = resolveStandaloneSeatRenderer(
|
|
838
|
+
entries.map(({ account }) =>
|
|
839
|
+
(typeof account?.accountUserId === "string" ? account.accountUserId.trim() : "") || undefined,
|
|
840
|
+
),
|
|
841
|
+
includeSensitive,
|
|
842
|
+
);
|
|
843
|
+
return entries
|
|
421
844
|
.map(({ account, index }) => {
|
|
422
845
|
const trimmedId =
|
|
423
846
|
typeof account?.accountId === "string" ? account.accountId.trim() : "";
|
|
424
847
|
const accountId = trimmedId || undefined;
|
|
848
|
+
// Members of one Business workspace share `accountId`, so the seat is
|
|
849
|
+
// what tells them apart. It is carried masked next to its suffix for
|
|
850
|
+
// the same reason `accountId` is: so the printed `seat:` discloses no
|
|
851
|
+
// more of an id than the field beside it unless telling two seats
|
|
852
|
+
// apart requires it.
|
|
853
|
+
const trimmedUserId =
|
|
854
|
+
typeof account?.accountUserId === "string" ? account.accountUserId.trim() : "";
|
|
855
|
+
const accountUserId = trimmedUserId || undefined;
|
|
425
856
|
return {
|
|
426
857
|
index,
|
|
427
858
|
label: account?.accountLabel ?? `Account ${index + 1}`,
|
|
428
859
|
email: maskValue(account?.email, includeSensitive),
|
|
429
860
|
accountId: maskValue(accountId, includeSensitive),
|
|
430
861
|
idSuffix: accountIdSuffix(accountId, includeSensitive),
|
|
862
|
+
accountUserId: maskValue(accountUserId, includeSensitive),
|
|
863
|
+
seatSuffix: seatIsDisclosable(accountUserId, includeSensitive)
|
|
864
|
+
? renderSeat(accountUserId)
|
|
865
|
+
: undefined,
|
|
431
866
|
accountIdSource: account?.accountIdSource,
|
|
432
867
|
enabled: account?.enabled !== false,
|
|
433
868
|
hasRefreshToken: typeof account?.refreshToken === "string" && account.refreshToken.length > 0,
|
|
@@ -453,7 +888,11 @@ function printStandaloneResult(command, payload, json) {
|
|
|
453
888
|
console.log(`Accounts: ${payload.totalAccounts}`);
|
|
454
889
|
if (Array.isArray(payload.accounts)) {
|
|
455
890
|
for (const account of payload.accounts) {
|
|
456
|
-
const identity = [
|
|
891
|
+
const identity = [
|
|
892
|
+
account.email,
|
|
893
|
+
account.idSuffix ? `id:${account.idSuffix}` : undefined,
|
|
894
|
+
account.seatSuffix ? `seat:${account.seatSuffix}` : undefined,
|
|
895
|
+
]
|
|
457
896
|
.filter(Boolean)
|
|
458
897
|
.join(", ");
|
|
459
898
|
const name = identity ? `${account.label} (${identity})` : account.label;
|
|
@@ -505,14 +944,15 @@ async function loadWarmRuntime(env) {
|
|
|
505
944
|
}
|
|
506
945
|
|
|
507
946
|
async function loadLimitsRuntime(env) {
|
|
508
|
-
const [storageMod, usageMod, shutdownMod, loggerMod] =
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
947
|
+
const [storageMod, usageMod, shutdownMod, loggerMod, configMod] =
|
|
948
|
+
await loadDistModules(
|
|
949
|
+
["storage.js", "codex-usage.js", "shutdown.js", "logger.js", "config.js"],
|
|
950
|
+
"limits",
|
|
951
|
+
);
|
|
512
952
|
// Fetching usage can refresh (and therefore persist) a token, so the same
|
|
513
953
|
// process-owns-termination rule as `warm` applies.
|
|
514
954
|
shutdownMod.setShutdownOwnsProcess(true);
|
|
515
|
-
return { storageMod, usageMod, shutdownMod, loggerMod };
|
|
955
|
+
return { storageMod, usageMod, shutdownMod, loggerMod, configMod };
|
|
516
956
|
}
|
|
517
957
|
|
|
518
958
|
export async function runWarmCommand(parsed, options = {}) {
|
|
@@ -676,7 +1116,8 @@ export async function runLimitsCommand(parsed, options = {}) {
|
|
|
676
1116
|
return { exitCode: 1, action: "limits", storagePath };
|
|
677
1117
|
}
|
|
678
1118
|
|
|
679
|
-
const { storageMod, usageMod, loggerMod } = runtime;
|
|
1119
|
+
const { storageMod, usageMod, loggerMod, configMod } = runtime;
|
|
1120
|
+
const quotaDisplay = configMod.getQuotaDisplay(configMod.loadPluginConfig());
|
|
680
1121
|
// Point dist storage at the resolved accounts file so a refreshed token is
|
|
681
1122
|
// persisted to the SAME file the rest of the toolchain reads.
|
|
682
1123
|
storageMod.setStoragePathDirect(storagePath);
|
|
@@ -756,6 +1197,7 @@ export async function runLimitsCommand(parsed, options = {}) {
|
|
|
756
1197
|
accessToken,
|
|
757
1198
|
organizationId: account.organizationId,
|
|
758
1199
|
}),
|
|
1200
|
+
quotaDisplay,
|
|
759
1201
|
);
|
|
760
1202
|
const quotaExhaustedResetAtMs = usageMod.getUsageQuotaExhaustedResetAtMs([
|
|
761
1203
|
usage.primary,
|
|
@@ -1226,6 +1668,10 @@ async function removePluginFromCachePackage(paths, dryRun) {
|
|
|
1226
1668
|
if (!existsSync(paths.cachePackageJson)) {
|
|
1227
1669
|
return;
|
|
1228
1670
|
}
|
|
1671
|
+
if (!isEvictableCachePath(paths.cachePackageJson, paths.cacheDir)) {
|
|
1672
|
+
log(`Warning: refusing to update ${paths.cachePackageJson}: it does not resolve inside the OpenCode cache.`);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1229
1675
|
|
|
1230
1676
|
let cacheData;
|
|
1231
1677
|
try {
|
|
@@ -1267,6 +1713,29 @@ async function removePluginFromCachePackage(paths, dryRun) {
|
|
|
1267
1713
|
await writeFileAtomic(paths.cachePackageJson, formatJson(cacheData));
|
|
1268
1714
|
}
|
|
1269
1715
|
|
|
1716
|
+
/**
|
|
1717
|
+
* Mirror of `isEvictableCachePath` in lib/auto-update-checker.ts. A recursive
|
|
1718
|
+
* delete must never act on a path that only spells like cache: the cache root
|
|
1719
|
+
* itself must not resolve through a symlink (`~/.cache/opencode -> ~` would
|
|
1720
|
+
* otherwise call the whole home directory "inside the cache"), and the
|
|
1721
|
+
* resolved target must stay inside the resolved root.
|
|
1722
|
+
*/
|
|
1723
|
+
function isEvictableCachePath(cachePath, cacheRoot) {
|
|
1724
|
+
const absolutePath = resolve(cachePath);
|
|
1725
|
+
const absoluteRoot = resolve(cacheRoot);
|
|
1726
|
+
if (!isInsideDirectory(absolutePath, absoluteRoot, process.platform)) return false;
|
|
1727
|
+
try {
|
|
1728
|
+
const realRoot = realpathSync(absoluteRoot);
|
|
1729
|
+
const rootIsSymlinked = process.platform === "win32"
|
|
1730
|
+
? realRoot.toLowerCase() !== absoluteRoot.toLowerCase()
|
|
1731
|
+
: realRoot !== absoluteRoot;
|
|
1732
|
+
if (rootIsSymlinked) return false;
|
|
1733
|
+
return isInsideDirectory(realpathSync(absolutePath), realRoot, process.platform);
|
|
1734
|
+
} catch {
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1270
1739
|
async function clearCache(paths, dryRun, skipCacheClear) {
|
|
1271
1740
|
if (skipCacheClear) {
|
|
1272
1741
|
log("Skipping cache clear (--no-cache-clear).");
|
|
@@ -1274,6 +1743,12 @@ async function clearCache(paths, dryRun, skipCacheClear) {
|
|
|
1274
1743
|
return;
|
|
1275
1744
|
}
|
|
1276
1745
|
|
|
1746
|
+
const cacheTargets = [
|
|
1747
|
+
...paths.cacheNodeModulesPaths,
|
|
1748
|
+
...paths.cachePackagePaths,
|
|
1749
|
+
paths.cacheBunLock,
|
|
1750
|
+
];
|
|
1751
|
+
|
|
1277
1752
|
if (dryRun) {
|
|
1278
1753
|
for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
|
|
1279
1754
|
log(`[dry-run] Would remove ${cacheNodeModulesPath}`);
|
|
@@ -1283,13 +1758,17 @@ async function clearCache(paths, dryRun, skipCacheClear) {
|
|
|
1283
1758
|
}
|
|
1284
1759
|
log(`[dry-run] Would remove ${paths.cacheBunLock}`);
|
|
1285
1760
|
} else {
|
|
1286
|
-
for (const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1761
|
+
for (const cacheTarget of cacheTargets) {
|
|
1762
|
+
if (!existsSync(cacheTarget)) continue;
|
|
1763
|
+
if (!isEvictableCachePath(cacheTarget, paths.cacheDir)) {
|
|
1764
|
+
log(`Warning: refusing to remove ${cacheTarget}: it does not resolve inside the OpenCode cache.`);
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
await removeWithWindowsRetry(cacheTarget, {
|
|
1768
|
+
recursive: cacheTarget !== paths.cacheBunLock,
|
|
1769
|
+
force: true,
|
|
1770
|
+
});
|
|
1291
1771
|
}
|
|
1292
|
-
await removeWithWindowsRetry(paths.cacheBunLock, { force: true });
|
|
1293
1772
|
}
|
|
1294
1773
|
|
|
1295
1774
|
await removePluginFromCachePackage(paths, dryRun);
|
|
@@ -1358,9 +1837,6 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
1358
1837
|
}
|
|
1359
1838
|
}
|
|
1360
1839
|
|
|
1361
|
-
let nextConfig = pluginOnly
|
|
1362
|
-
? { $schema: template.$schema, plugin: [PACKAGE_NAME] }
|
|
1363
|
-
: template;
|
|
1364
1840
|
let existingConfig;
|
|
1365
1841
|
if (existsSync(paths.configPath)) {
|
|
1366
1842
|
try {
|
|
@@ -1369,18 +1845,6 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
1369
1845
|
throw new Error("config root must be a JSON object");
|
|
1370
1846
|
}
|
|
1371
1847
|
existingConfig = existing;
|
|
1372
|
-
const merged = { ...existing };
|
|
1373
|
-
merged.plugin = normalizePluginList(existing.plugin);
|
|
1374
|
-
if (!pluginOnly) {
|
|
1375
|
-
const provider = (existing.provider && typeof existing.provider === "object")
|
|
1376
|
-
? { ...existing.provider }
|
|
1377
|
-
: {};
|
|
1378
|
-
provider.openai = mergeOpenaiProvider(existing.provider?.openai, template.provider?.openai, {
|
|
1379
|
-
modelKeysToRemove,
|
|
1380
|
-
});
|
|
1381
|
-
merged.provider = provider;
|
|
1382
|
-
}
|
|
1383
|
-
nextConfig = merged;
|
|
1384
1848
|
} catch (error) {
|
|
1385
1849
|
if (pluginOnly) {
|
|
1386
1850
|
throw new Error(
|
|
@@ -1389,13 +1853,11 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
1389
1853
|
}
|
|
1390
1854
|
log(`Warning: Could not parse existing config (${formatErrorForLog(error)}). Replacing with template.`);
|
|
1391
1855
|
existingConfig = undefined;
|
|
1392
|
-
nextConfig = template;
|
|
1393
1856
|
}
|
|
1394
1857
|
} else {
|
|
1395
1858
|
log("No existing config found. Creating new global config.");
|
|
1396
1859
|
}
|
|
1397
1860
|
|
|
1398
|
-
let nextTuiConfig = mergeTuiConfig(undefined);
|
|
1399
1861
|
let existingTuiConfig;
|
|
1400
1862
|
if (existsSync(paths.tuiConfigPath)) {
|
|
1401
1863
|
try {
|
|
@@ -1404,7 +1866,6 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
1404
1866
|
throw new Error("TUI config root must be a JSON object");
|
|
1405
1867
|
}
|
|
1406
1868
|
existingTuiConfig = existing;
|
|
1407
|
-
nextTuiConfig = mergeTuiConfig(existing);
|
|
1408
1869
|
} catch (error) {
|
|
1409
1870
|
if (pluginOnly) {
|
|
1410
1871
|
throw new Error(
|
|
@@ -1413,12 +1874,64 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
1413
1874
|
}
|
|
1414
1875
|
log(`Warning: Could not parse existing TUI config (${formatErrorForLog(error)}). Replacing with minimal TUI config.`);
|
|
1415
1876
|
existingTuiConfig = undefined;
|
|
1416
|
-
nextTuiConfig = mergeTuiConfig(undefined);
|
|
1417
1877
|
}
|
|
1418
1878
|
} else {
|
|
1419
1879
|
log("No existing TUI config found. Creating new global TUI config.");
|
|
1420
1880
|
}
|
|
1421
1881
|
|
|
1882
|
+
// A checkout of this package registered in either file already loads the
|
|
1883
|
+
// plugin, so the published name must not be written beside it anywhere -
|
|
1884
|
+
// otherwise the checkout in opencode.json and the published package in
|
|
1885
|
+
// tui.json both load.
|
|
1886
|
+
const pluginListOptions = {
|
|
1887
|
+
baseDirectory: paths.configDir,
|
|
1888
|
+
cacheDirectory: paths.cacheDir,
|
|
1889
|
+
};
|
|
1890
|
+
const checkoutRegistered = [existingConfig?.plugin, existingTuiConfig?.plugin]
|
|
1891
|
+
.flatMap((list) => (Array.isArray(list) ? list : []))
|
|
1892
|
+
.some((entry) => {
|
|
1893
|
+
const classification = classifyPluginEntry(entry, pluginListOptions);
|
|
1894
|
+
return (
|
|
1895
|
+
classification.kind === LOCAL_CHECKOUT_ENTRY &&
|
|
1896
|
+
classification.name === PACKAGE_NAME
|
|
1897
|
+
);
|
|
1898
|
+
});
|
|
1899
|
+
const normalizeOptions = { ...pluginListOptions, checkoutRegistered };
|
|
1900
|
+
|
|
1901
|
+
let nextConfig;
|
|
1902
|
+
if (existingConfig !== undefined) {
|
|
1903
|
+
const merged = { ...existingConfig };
|
|
1904
|
+
merged.plugin = normalizePluginList(existingConfig.plugin, log, normalizeOptions);
|
|
1905
|
+
if (!pluginOnly) {
|
|
1906
|
+
const provider = (existingConfig.provider && typeof existingConfig.provider === "object")
|
|
1907
|
+
? { ...existingConfig.provider }
|
|
1908
|
+
: {};
|
|
1909
|
+
provider.openai = mergeOpenaiProvider(existingConfig.provider?.openai, template.provider?.openai, {
|
|
1910
|
+
modelKeysToRemove,
|
|
1911
|
+
});
|
|
1912
|
+
merged.provider = provider;
|
|
1913
|
+
}
|
|
1914
|
+
nextConfig = merged;
|
|
1915
|
+
} else {
|
|
1916
|
+
nextConfig = pluginOnly
|
|
1917
|
+
? { $schema: template.$schema, plugin: [PACKAGE_NAME] }
|
|
1918
|
+
: template;
|
|
1919
|
+
nextConfig.plugin = normalizePluginList(nextConfig.plugin, log, normalizeOptions);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
const nextTuiConfig = mergeTuiConfig(existingTuiConfig, log, normalizeOptions);
|
|
1923
|
+
|
|
1924
|
+
const unregisteredCheckout = findUnregisteredLocalCheckout(nextConfig.plugin, paths.originHistoryPath, {
|
|
1925
|
+
baseDirectory: paths.configDir,
|
|
1926
|
+
cacheDirectory: paths.cacheDir,
|
|
1927
|
+
});
|
|
1928
|
+
if (unregisteredCheckout) {
|
|
1929
|
+
log(
|
|
1930
|
+
`Note: this plugin last loaded from a checkout at ${unregisteredCheckout.root} on ${unregisteredCheckout.lastSeen}, ` +
|
|
1931
|
+
`which ${paths.configPath} does not register. Point the plugin entry back at that path if OpenCode should keep loading your own build.`,
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1422
1935
|
const configChanged = existingConfig === undefined || formatJson(existingConfig) !== formatJson(nextConfig);
|
|
1423
1936
|
const tuiConfigChanged = existingTuiConfig === undefined || formatJson(existingTuiConfig) !== formatJson(nextTuiConfig);
|
|
1424
1937
|
let wrote = false;
|
|
@@ -1459,10 +1972,10 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
1459
1972
|
log("\nDone. Restart OpenCode to (re)install the plugin.");
|
|
1460
1973
|
log("Example: opencode");
|
|
1461
1974
|
if (!pluginOnly && configMode === "modern") {
|
|
1462
|
-
log("Note: Modern config intentionally shows
|
|
1975
|
+
log("Note: Modern config intentionally shows 10 base OAuth model entries; use the variant picker for reasoning presets.");
|
|
1463
1976
|
}
|
|
1464
1977
|
if (!pluginOnly && configMode === "legacy") {
|
|
1465
|
-
log("Note: Legacy config writes
|
|
1978
|
+
log("Note: Legacy config writes 53 explicit preset entries and is also safe for older OpenCode versions.");
|
|
1466
1979
|
}
|
|
1467
1980
|
if (!pluginOnly && configMode === "full") {
|
|
1468
1981
|
log("Note: Full config installs both compact base models and explicit preset entries for direct selector IDs.");
|
|
@@ -1481,14 +1994,18 @@ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
|
|
|
1481
1994
|
}
|
|
1482
1995
|
|
|
1483
1996
|
export const __test = {
|
|
1997
|
+
ORIGIN_HISTORY_FILE_NAME,
|
|
1484
1998
|
buildPaths,
|
|
1485
1999
|
backupConfig,
|
|
2000
|
+
classifyPluginEntry,
|
|
1486
2001
|
copyFileWithWindowsRetry,
|
|
2002
|
+
findUnregisteredLocalCheckout,
|
|
1487
2003
|
formatConfigDiff,
|
|
1488
2004
|
formatRedactedConfigDiff,
|
|
1489
2005
|
mergeFullTemplate,
|
|
1490
2006
|
mergeOpenaiProvider,
|
|
1491
2007
|
mergeTuiConfig,
|
|
2008
|
+
normalizePluginList,
|
|
1492
2009
|
parseCliArgs,
|
|
1493
2010
|
removeWithWindowsRetry,
|
|
1494
2011
|
runStandaloneCommand,
|