@yansigit/opencodex 2.36.1-dev.20260829.48 → 2.36.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/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
- package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
- package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
- package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
- package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
- package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
- package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
- package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
- package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
- package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
- package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
- package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
- package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
- package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
- package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/generated/compatibility-version.json +94 -58
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/store.ts +16 -6
- package/src/providers/label.ts +34 -1
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/management/logs-usage-routes.ts +5 -27
- package/src/server/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +79 -33
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
package/src/cli/dispatch.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* never needs to import the entry module back (no cycle).
|
|
10
10
|
*/
|
|
11
11
|
import { CLI_COMMANDS } from "./registry";
|
|
12
|
+
import { isValidProviderName } from "../config/provider-name";
|
|
12
13
|
import type { CliHead } from "./root";
|
|
13
14
|
import type { ReadyArgs } from "./ready";
|
|
14
15
|
import type { LiveProxy } from "../server/proxy-liveness";
|
|
@@ -21,6 +22,7 @@ import { restoreNativeCodexAsync } from "../codex/inject";
|
|
|
21
22
|
import { stripGrokConfig } from "../grok/inject";
|
|
22
23
|
import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
|
|
23
24
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
25
|
+
import { isJsonOption, takeFlag } from "./runtime-api";
|
|
24
26
|
|
|
25
27
|
export interface CliDispatchDeps {
|
|
26
28
|
args: string[];
|
|
@@ -69,33 +71,37 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
69
71
|
return Number(process.exitCode ?? 0);
|
|
70
72
|
},
|
|
71
73
|
restore: async deps => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
+
const restoreArgs = deps.args.slice(1);
|
|
75
|
+
const restoreJson = takeFlag(restoreArgs, "--json");
|
|
76
|
+
if (restoreArgs[0] === "back") {
|
|
74
77
|
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
75
78
|
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
76
79
|
// this only re-runs the same inject (config + catalog + history) `ocx start` does.
|
|
80
|
+
// takeFlag above makes `ocx restore --json back` restore-back, not eject.
|
|
81
|
+
const { skippedRestoreEnvelope } = await import("../codex/inject");
|
|
82
|
+
const emitBack = (success: boolean, message: string, code: number): number => {
|
|
83
|
+
if (restoreJson) console.log(JSON.stringify(skippedRestoreEnvelope(success, message)));
|
|
84
|
+
else if (code === 0) console.log(message);
|
|
85
|
+
else console.error(message);
|
|
86
|
+
return code;
|
|
87
|
+
};
|
|
77
88
|
const live = await deps.findLiveProxy();
|
|
78
89
|
if (!live) {
|
|
79
|
-
|
|
80
|
-
return 1;
|
|
90
|
+
return emitBack(false, "No running proxy found. Run 'ocx start' — it injects opencodex automatically.", 1);
|
|
81
91
|
}
|
|
82
92
|
const desired = setIntegrationEnabled("codex", true);
|
|
83
93
|
if (!desired.ok) {
|
|
84
|
-
|
|
85
|
-
return desired.reason === "conflict" ? 2 : 1;
|
|
94
|
+
return emitBack(false, `Codex desired state was not saved (${desired.reason}).`, desired.reason === "conflict" ? 2 : 1);
|
|
86
95
|
}
|
|
87
96
|
const synced = await syncModelsToCodex(live.port);
|
|
88
97
|
if (synced.status === "skipped") {
|
|
89
|
-
|
|
90
|
-
return 2;
|
|
98
|
+
return emitBack(false, "Codex integration is OFF; restore back did not change Codex. Retry after the competing integration change finishes.", 2);
|
|
91
99
|
}
|
|
92
100
|
if (!synced.ok) {
|
|
93
|
-
|
|
94
|
-
return 1;
|
|
101
|
+
return emitBack(false, "Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.", 1);
|
|
95
102
|
}
|
|
96
103
|
const target = collectOrcaCodexHomeDiagnostic();
|
|
97
|
-
|
|
98
|
-
return 0;
|
|
104
|
+
return emitBack(true, `Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`, 0);
|
|
99
105
|
}
|
|
100
106
|
const desired = setIntegrationEnabled("codex", false);
|
|
101
107
|
if (!desired.ok) {
|
|
@@ -172,14 +178,35 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
172
178
|
},
|
|
173
179
|
doctor: async deps => {
|
|
174
180
|
const doctorArgs = deps.args.slice(1);
|
|
175
|
-
|
|
181
|
+
// `--json` was silently ignored here: runDoctor scans for its own flags and prints human
|
|
182
|
+
// output regardless, so a caller that asked for JSON got prose and exit 0 -- and the skill
|
|
183
|
+
// recipes recommended exactly that invocation. Refusing it is worse than supporting it and
|
|
184
|
+
// better than lying about it.
|
|
185
|
+
//
|
|
186
|
+
// Not implemented rather than deferred silently: runDoctor has no report collection at all
|
|
187
|
+
// (a module-level failure bit plus ~90 direct console emissions), and this runner appends
|
|
188
|
+
// the Codex Log Guard's human output after it returns, so emitting a JSON document here
|
|
189
|
+
// would interleave prose with JSON on one stdout -- unparseable, which is worse than the
|
|
190
|
+
// ignored flag. The structured-report refactor is tracked as its own work-phase.
|
|
191
|
+
if (doctorArgs.some(isJsonOption)) {
|
|
192
|
+
console.error("ocx doctor does not support --json yet. Run `ocx doctor` for the human report, or use `ocx status --json` and `ocx ready --json` for machine-readable health.");
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
const { RECOVER_ZERO_BYTE_COORDINATOR_FLAG, runDoctor, doctorFailed } = await import("./doctor");
|
|
176
196
|
await runDoctor(doctorArgs);
|
|
177
197
|
if (!doctorArgs.includes("--fix-codex-runtime") && !doctorArgs.includes(RECOVER_ZERO_BYTE_COORDINATOR_FLAG)) {
|
|
178
198
|
console.log("");
|
|
179
199
|
const { printCodexLogGuardDoctor } = await import("./codex-log-guard-doctor");
|
|
180
200
|
printCodexLogGuardDoctor();
|
|
181
201
|
}
|
|
182
|
-
|
|
202
|
+
// A diagnostic that always exits 0 cannot gate a script. `runDoctor` reports by direct
|
|
203
|
+
// console.log with no checks collection, and signals its own special-flag failures
|
|
204
|
+
// through process.exitCode, so honour both: an explicit exitCode wins, otherwise a
|
|
205
|
+
// FAIL-level check fails the command. This is a BREAKING change for pipelines that ran
|
|
206
|
+
// `ocx doctor` and ignored the result; a diagnostic that cannot fail is worse.
|
|
207
|
+
const explicit = Number(process.exitCode ?? 0);
|
|
208
|
+
if (explicit !== 0) return explicit;
|
|
209
|
+
return doctorFailed() ? 1 : 0;
|
|
183
210
|
},
|
|
184
211
|
debug: async deps => {
|
|
185
212
|
const { handleDebugCommand } = await import("./debug");
|
|
@@ -196,10 +223,61 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
196
223
|
return 0;
|
|
197
224
|
},
|
|
198
225
|
logout: async deps => {
|
|
226
|
+
// Argv is parsed BEFORE any store access, which is the whole point of this shape.
|
|
227
|
+
// Previously `args[1]` was taken as the provider name with no parsing, so
|
|
228
|
+
// `ocx logout --json` called removeCredential("--json"), printed "Logged out of
|
|
229
|
+
// --json." and exited 0 -- a silent false success, the worst outcome for a caller
|
|
230
|
+
// that can only see the exit code.
|
|
231
|
+
//
|
|
232
|
+
// That is not merely a wasted call. `normalizeAuthStore` copies every top-level key
|
|
233
|
+
// it finds, so a hand-edited, legacy, or corrupted auth.json containing a `--json`
|
|
234
|
+
// key would have its active account deleted -- and the key dropped entirely if that
|
|
235
|
+
// was its last account. A flag must never reach the store as a provider name.
|
|
236
|
+
const logoutArgs = deps.args.slice(1);
|
|
237
|
+
const wantsJson = logoutArgs.includes("--json");
|
|
238
|
+
// Any leading dash is an option, not a provider. Matching only `--` left the same defect
|
|
239
|
+
// one dash shorter: `ocx logout -j` treated `-j` as the provider name and, with a `-j` key
|
|
240
|
+
// present in the store, deleted it and exited 0.
|
|
241
|
+
const isOption = (arg: string): boolean => arg.startsWith("-");
|
|
242
|
+
const positionals = logoutArgs.filter(arg => !isOption(arg));
|
|
243
|
+
const unknownFlags = logoutArgs.filter(arg => isOption(arg) && arg !== "--json");
|
|
244
|
+
const name = (positionals[0] ?? "").trim().toLowerCase();
|
|
245
|
+
|
|
246
|
+
// Usage failures exit 2 and touch nothing. A missing provider is a usage error; a
|
|
247
|
+
// provider that simply has no credential is a not-found (4) further down, because the
|
|
248
|
+
// vocabulary distinguishes "you called this wrong" from "the thing is not there".
|
|
249
|
+
//
|
|
250
|
+
// The shape check is `isValidProviderName`, not another dash test. Rejecting a leading
|
|
251
|
+
// ASCII `-` fixed `-j` and still let `logout —json` through with a Unicode dash, which is
|
|
252
|
+
// the same defect a third time: each patch named one spelling instead of the class. The
|
|
253
|
+
// canonical validator states the rule positively -- start and end alphanumeric, internal
|
|
254
|
+
// `._-` allowed -- so `github-copilot` and `google-antigravity` pass while every dash
|
|
255
|
+
// variant, empty string, and reserved name fails. Anything that is not a possible
|
|
256
|
+
// provider id cannot reach the store at all.
|
|
257
|
+
const malformedName = Boolean(name) && !isValidProviderName(name);
|
|
258
|
+
if (unknownFlags.length > 0 || positionals.length > 1 || !name || malformedName) {
|
|
259
|
+
const problem = unknownFlags.length > 0
|
|
260
|
+
? `unknown option ${unknownFlags[0]}`
|
|
261
|
+
: positionals.length > 1 ? "too many arguments"
|
|
262
|
+
: malformedName ? `not a valid provider name: ${name}`
|
|
263
|
+
: "missing provider";
|
|
264
|
+
console.error(`Usage: ocx logout <provider> [--json] (${problem})`);
|
|
265
|
+
return 2;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The disposition comes from inside the store mutation, not from a read-then-remove
|
|
269
|
+
// preflight. `mutateStore` serializes writes, so a preflight leaves a window where a
|
|
270
|
+
// concurrent logout removes the same account and BOTH callers exit 0 claiming a removal --
|
|
271
|
+
// a false success again, just a narrower one than the flag bug above.
|
|
199
272
|
const { removeCredential } = await import("../oauth/store");
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
273
|
+
const outcome = await removeCredential(name);
|
|
274
|
+
if (outcome === "not-found") {
|
|
275
|
+
if (wantsJson) console.log(JSON.stringify({ schemaVersion: 1, ok: false, provider: name, removed: false, reason: "not_found" }, null, 2));
|
|
276
|
+
else console.error(`No stored credential for '${name}'.`);
|
|
277
|
+
return 4;
|
|
278
|
+
}
|
|
279
|
+
if (wantsJson) console.log(JSON.stringify({ schemaVersion: 1, ok: true, provider: name, removed: true }, null, 2));
|
|
280
|
+
else console.log(`Logged out of ${name}.`);
|
|
203
281
|
return 0;
|
|
204
282
|
},
|
|
205
283
|
sync: async deps => {
|
|
@@ -270,18 +348,76 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
270
348
|
const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
|
|
271
349
|
const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
|
|
272
350
|
const { getCodexHome } = await import("../codex/paths");
|
|
351
|
+
const { readCodexCatalogPathForHome } = await import("../codex/catalog/parsing");
|
|
352
|
+
const { existsSync } = await import("node:fs");
|
|
273
353
|
const owningCodexHome = getCodexHome();
|
|
274
354
|
const desiredDisabled = !shouldSyncCodexOnStart(deps.loadConfig());
|
|
275
355
|
const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
276
356
|
invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, { allowWhenDesiredDisabled: true }));
|
|
357
|
+
const cacheJson = cacheArgs.includes("--json");
|
|
358
|
+
const jsonSafeLog = cacheJson
|
|
359
|
+
? { log: (...values: unknown[]) => console.error(...values), error: (...values: unknown[]) => console.error(...values) }
|
|
360
|
+
: console;
|
|
277
361
|
// Only warn/restart when models_cache was actually rewritten from a readable catalog.
|
|
278
362
|
if (invalidated.kind === "completed" && invalidated.value) {
|
|
279
|
-
afterCatalogWriteHandleAppServers({ restart: restartCodex, log:
|
|
280
|
-
if (restartDesktopApp) await handleDesktopAppRestart(
|
|
281
|
-
} else if (desiredDisabled) {
|
|
282
|
-
|
|
363
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: jsonSafeLog });
|
|
364
|
+
if (restartDesktopApp) await handleDesktopAppRestart(jsonSafeLog);
|
|
365
|
+
} else if (desiredDisabled && !cacheJson) {
|
|
366
|
+
// Worth saying in the human path, because it explains why nothing was written.
|
|
367
|
+
// Under --json this belongs on the envelope, not as a second stdout line.
|
|
368
|
+
console.log("Codex integration is OFF; no catalog or cache write resulted.");
|
|
283
369
|
}
|
|
284
|
-
|
|
370
|
+
// `completed` with a falsy value means the cache was NOT rewritten. Previously every
|
|
371
|
+
// outcome exited 0, so a script could not tell a refreshed cache from a skipped one.
|
|
372
|
+
//
|
|
373
|
+
// Losing the catalog write lock to another process is a skip, not a failure:
|
|
374
|
+
// serialization working as designed is the expected outcome under concurrency, and a
|
|
375
|
+
// proxy startup holding the permit would otherwise make a perfectly healthy
|
|
376
|
+
// `ocx sync-cache` exit 1 and fail the pipeline that called it -- intermittently, so it
|
|
377
|
+
// would read as a flake rather than a bug. `codex-retained-root-serialization.test.ts`
|
|
378
|
+
// pins exactly that: contended lock, no cache write, exit 0.
|
|
379
|
+
//
|
|
380
|
+
// `desiredDisabled` is deliberately NOT part of the success test, which is the subtle
|
|
381
|
+
// part. This call passes `allowWhenDesiredDisabled: true`, so the OFF gate inside the
|
|
382
|
+
// refresh never fires and the work is genuinely attempted -- an explicit `ocx sync-cache`
|
|
383
|
+
// means the user asked for it regardless of the toggle. Treating OFF as automatic success
|
|
384
|
+
// would report exit 0 and `skipped: true` for a refresh that actually failed.
|
|
385
|
+
//
|
|
386
|
+
// But `invalidateCodexModelsCacheWithPermit` returns a bare boolean for four different
|
|
387
|
+
// situations -- wrote it, no catalog file exists, the OFF gate fired, or it threw -- so
|
|
388
|
+
// `false` alone cannot be read as failure either. `!existsSync(catalogPath)` is a
|
|
389
|
+
// legitimate nothing-to-do: with no catalog there is no cache to derive, which is the
|
|
390
|
+
// normal state of a fully native home and the case
|
|
391
|
+
// `codex-composed-acceptance.test.ts` pins at exit 0. It is checked here rather than by
|
|
392
|
+
// widening that function's return type, because its boolean is consumed by a dozen
|
|
393
|
+
// management routes that have no use for the distinction.
|
|
394
|
+
const wrote = invalidated.kind === "completed" && Boolean(invalidated.value);
|
|
395
|
+
const contended = invalidated.kind === "unavailable" && invalidated.reason === "busy";
|
|
396
|
+
const noCatalog = !wrote && !existsSync(readCodexCatalogPathForHome(owningCodexHome));
|
|
397
|
+
const ok = wrote || contended || noCatalog;
|
|
398
|
+
if (cacheJson) {
|
|
399
|
+
console.log(JSON.stringify({
|
|
400
|
+
schemaVersion: 1,
|
|
401
|
+
ok,
|
|
402
|
+
wrote,
|
|
403
|
+
skipped: contended || noCatalog,
|
|
404
|
+
outcome: invalidated.kind,
|
|
405
|
+
// `outcome` alone cannot separate a contended lock from a hard serialization
|
|
406
|
+
// failure -- both are `unavailable`. Carry the reason so a caller can.
|
|
407
|
+
reason: invalidated.kind === "unavailable" ? invalidated.reason : undefined,
|
|
408
|
+
// Which of the two benign skips this was, so `skipped: true` is never opaque.
|
|
409
|
+
skippedReason: contended ? "contended" : noCatalog ? "no_catalog" : undefined,
|
|
410
|
+
desiredDisabled,
|
|
411
|
+
codexHome: owningCodexHome,
|
|
412
|
+
}, null, 2));
|
|
413
|
+
} else if (contended) {
|
|
414
|
+
console.log("Another process owns the catalog write; cache sync skipped.");
|
|
415
|
+
} else if (noCatalog) {
|
|
416
|
+
console.log("No Codex catalog to derive a cache from; nothing to sync.");
|
|
417
|
+
} else if (!ok) {
|
|
418
|
+
console.error(`Cache refresh did not complete (${invalidated.kind}). The Codex model cache was not rewritten.`);
|
|
419
|
+
}
|
|
420
|
+
return ok ? 0 : 1;
|
|
285
421
|
},
|
|
286
422
|
gui: async deps => {
|
|
287
423
|
const config = deps.loadConfig();
|
|
@@ -315,8 +451,12 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
315
451
|
},
|
|
316
452
|
tray: async deps => {
|
|
317
453
|
const { windowsTrayCommand } = await import("../tray/windows");
|
|
454
|
+
// windowsTrayCommand reports failure through process.exitCode (tray/windows.ts sets
|
|
455
|
+
// it for bad usage and for a failed install/start/stop/uninstall) and returns void,
|
|
456
|
+
// so a literal 0 here made `ocx tray install` print an error and exit 0 (#2697).
|
|
457
|
+
process.exitCode = 0;
|
|
318
458
|
await windowsTrayCommand(deps.args.slice(1));
|
|
319
|
-
return 0;
|
|
459
|
+
return Number(process.exitCode ?? 0);
|
|
320
460
|
},
|
|
321
461
|
"codex-shim": async deps => {
|
|
322
462
|
const { codexShimStatus, diagnoseCodexShim, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
|
|
@@ -395,6 +535,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
395
535
|
await deps.handleProxyRestart(deps.handleRestartStartWhenStopped);
|
|
396
536
|
return Number(process.exitCode ?? 0);
|
|
397
537
|
},
|
|
538
|
+
capabilities: async deps => {
|
|
539
|
+
const { runCapabilities } = await import("./capabilities-command");
|
|
540
|
+
return await runCapabilities(deps.args.slice(1));
|
|
541
|
+
},
|
|
398
542
|
health: async deps => {
|
|
399
543
|
const healthArgs = deps.args.slice(1);
|
|
400
544
|
const wantsHealthJson = healthArgs.includes("--json");
|
|
@@ -418,8 +562,15 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
418
562
|
},
|
|
419
563
|
provider: async deps => {
|
|
420
564
|
const { handleProviderCommand } = await import("./provider");
|
|
565
|
+
// Reset first, like the service runner below: reading process.exitCode only
|
|
566
|
+
// reports THIS command's outcome if nothing earlier in the process set it.
|
|
567
|
+
process.exitCode = 0;
|
|
421
568
|
await handleProviderCommand(deps.args.slice(1));
|
|
422
|
-
|
|
569
|
+
// handleProviderCommand reports failure through process.exitCode, which it sets
|
|
570
|
+
// from handleProviderRuntimeCommand. Returning a literal 0 here made index.ts
|
|
571
|
+
// call process.exit(0) and erase it, so `ocx provider quota` against a stopped
|
|
572
|
+
// proxy printed an error and still exited 0 (#2697).
|
|
573
|
+
return Number(process.exitCode ?? 0);
|
|
423
574
|
},
|
|
424
575
|
account: async deps => {
|
|
425
576
|
const { cmdAccount } = await import("./account");
|
|
@@ -427,8 +578,11 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
427
578
|
},
|
|
428
579
|
models: async deps => {
|
|
429
580
|
const { handleModels } = await import("./models");
|
|
581
|
+
process.exitCode = 0;
|
|
430
582
|
await handleModels(deps.args.slice(1));
|
|
431
|
-
|
|
583
|
+
// Same as the provider runner above: handleModels sets process.exitCode from
|
|
584
|
+
// handleModelsRuntimeCommand, and a literal 0 discarded it (#2697).
|
|
585
|
+
return Number(process.exitCode ?? 0);
|
|
432
586
|
},
|
|
433
587
|
alias: async deps => {
|
|
434
588
|
const { handleAliasCommand } = await import("./alias");
|
|
@@ -459,6 +613,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
459
613
|
const { handleObserveCommand } = await import("./observe");
|
|
460
614
|
return await handleObserveCommand(deps.args.slice(1));
|
|
461
615
|
},
|
|
616
|
+
inspect: async deps => {
|
|
617
|
+
const { handleInspectCommand } = await import("./inspect");
|
|
618
|
+
return await handleInspectCommand(deps.args.slice(1));
|
|
619
|
+
},
|
|
462
620
|
logs: async deps => {
|
|
463
621
|
const { handleObserveCommand } = await import("./observe");
|
|
464
622
|
return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
|
|
@@ -468,8 +626,12 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
468
626
|
return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
|
|
469
627
|
},
|
|
470
628
|
storage: async deps => {
|
|
471
|
-
|
|
472
|
-
|
|
629
|
+
// `ocx storage` used to be a pure alias of `observe storage`, which reached only the report
|
|
630
|
+
// route. wp7 gave it cleanup, trash, and policy subcommands, so it dispatches to its own
|
|
631
|
+
// module -- with `report` as the default subcommand, so a bare `ocx storage` still prints
|
|
632
|
+
// the same thing it printed before.
|
|
633
|
+
const { handleStorageCommand } = await import("./storage");
|
|
634
|
+
return await handleStorageCommand(deps.args.slice(1));
|
|
473
635
|
},
|
|
474
636
|
memory: async deps => {
|
|
475
637
|
const { handleObserveCommand } = await import("./observe");
|
|
@@ -496,6 +658,12 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
496
658
|
if (integration === "grok") {
|
|
497
659
|
const { handleGrokCommand } = await import("./integrations");
|
|
498
660
|
return await handleGrokCommand(deps.args.slice(2));
|
|
661
|
+
} else if (integration === "native") {
|
|
662
|
+
// The native client toggles are a separate server surface from the reversible file
|
|
663
|
+
// integrations `client` manages, so they get their own subcommand rather than being
|
|
664
|
+
// folded into one that means something else.
|
|
665
|
+
const { handleIntegrationCommand } = await import("./inspect");
|
|
666
|
+
return await handleIntegrationCommand(deps.args.slice(1));
|
|
499
667
|
} else if (integration === "claude") {
|
|
500
668
|
const { handleClaudeConfigCommand } = await import("./integrations");
|
|
501
669
|
return await handleClaudeConfigCommand(deps.args.slice(2));
|
package/src/cli/doctor.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
|
|
|
16
16
|
import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
|
|
17
17
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
18
18
|
import { maskAccountId } from "../lib/privacy";
|
|
19
|
+
import { tokenCollidesWithAdmin } from "../lib/admin-secrets";
|
|
20
|
+
import { readInstalledServiceToken } from "../lib/service-secrets";
|
|
19
21
|
import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
|
|
20
22
|
import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
|
|
21
23
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
@@ -58,7 +60,35 @@ import {
|
|
|
58
60
|
} from "../server/local-management-read-client";
|
|
59
61
|
export { resolveCodexHomeDir } from "../codex/home";
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
/**
|
|
64
|
+
* `FAIL` exists for a condition that makes the surface unusable rather than degraded.
|
|
65
|
+
* A review of the #2696 work pointed out that reporting a fully fenced management plane
|
|
66
|
+
* — every `/api/*` returning 503 — at the same level as a directory-permission note
|
|
67
|
+
* misleads the reader about severity.
|
|
68
|
+
*
|
|
69
|
+
* Doctor's own exit code still belongs to the uniform contract in wp3b (devlog 025);
|
|
70
|
+
* this type only fixes what the operator is told.
|
|
71
|
+
*/
|
|
72
|
+
export type OAuthDoctorCheck = { level: "OK" | "WARN" | "FAIL"; message: string };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether any FAIL-level condition was seen during this `runDoctor` pass.
|
|
76
|
+
*
|
|
77
|
+
* Module-scoped and reset at the top of `runDoctor` rather than threaded through, because
|
|
78
|
+
* `runDoctor` reports by direct `console.log` across a dozen sections and has no checks
|
|
79
|
+
* collection to inspect. Reset matters for the test suite, which calls `runDoctor` several
|
|
80
|
+
* times in one process; a sticky flag would make the second call fail because the first did.
|
|
81
|
+
*/
|
|
82
|
+
let doctorSawFailure = false;
|
|
83
|
+
|
|
84
|
+
function recordDoctorFailure(): void {
|
|
85
|
+
doctorSawFailure = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True when the last `runDoctor` pass saw a FAIL-level condition. */
|
|
89
|
+
export function doctorFailed(): boolean {
|
|
90
|
+
return doctorSawFailure;
|
|
91
|
+
}
|
|
62
92
|
|
|
63
93
|
function pathIsWritable(path: string): boolean {
|
|
64
94
|
try {
|
|
@@ -137,6 +167,50 @@ function describeDoctorHealth(entry: OAuthHealthEntry): string {
|
|
|
137
167
|
}
|
|
138
168
|
}
|
|
139
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Detect the management/data-plane credential collision behind #2696.
|
|
172
|
+
*
|
|
173
|
+
* The service exports the service token file as `OPENCODEX_API_AUTH_TOKEN` before
|
|
174
|
+
* starting the proxy. When that value is the admin token, the server treats the
|
|
175
|
+
* management credential as a data-plane admission secret and fences the ENTIRE
|
|
176
|
+
* management plane closed at boot: every `/api/*` returns 503, including on a loopback
|
|
177
|
+
* install that never needed a data-plane secret.
|
|
178
|
+
*
|
|
179
|
+
* `assertNotAdminToken` in src/service.ts now refuses to create this state, but an
|
|
180
|
+
* install made before that guard existed is already broken on disk, and the symptom
|
|
181
|
+
* (every management command failing) points nowhere. This is the check that names it.
|
|
182
|
+
*
|
|
183
|
+
* Observe-only, like the rest of doctor: it compares shapes and never prints, logs, or
|
|
184
|
+
* returns a credential value.
|
|
185
|
+
*/
|
|
186
|
+
export function dataPlaneCredentialCollisionCheck(
|
|
187
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
188
|
+
installedServiceToken: string | null = readInstalledServiceToken(),
|
|
189
|
+
): OAuthDoctorCheck {
|
|
190
|
+
const dataPlane = env.OPENCODEX_API_AUTH_TOKEN?.trim() || installedServiceToken?.trim() || "";
|
|
191
|
+
if (!dataPlane) {
|
|
192
|
+
return { level: "OK", message: "No data-plane token is set, so it cannot collide with the management token." };
|
|
193
|
+
}
|
|
194
|
+
// Same comparison as assertNotAdminToken: minted prefix or configuredAdminToken
|
|
195
|
+
// (env or admin-api-token file). The file token is the one the service wrapper
|
|
196
|
+
// actually exports; inspecting only the doctor process env reported OK on every
|
|
197
|
+
// already-broken install (#2696).
|
|
198
|
+
if (!tokenCollidesWithAdmin(dataPlane, env)) {
|
|
199
|
+
return { level: "OK", message: "Data-plane and management credentials are distinct." };
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
// Not a degradation: while this holds, every /api/* returns 503 and no ocx
|
|
203
|
+
// management command can work at all.
|
|
204
|
+
level: "FAIL",
|
|
205
|
+
message:
|
|
206
|
+
"The data-plane secret (OPENCODEX_API_AUTH_TOKEN or the service token file) holds the "
|
|
207
|
+
+ "management (admin) token, so the proxy fences the whole management API closed and "
|
|
208
|
+
+ "every ocx management command fails with 503. "
|
|
209
|
+
+ "Action: unset OPENCODEX_API_AUTH_TOKEN, replace the service token file with a distinct "
|
|
210
|
+
+ "data-plane key, then re-run `ocx service install` and restart the proxy",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
140
214
|
/**
|
|
141
215
|
* OAuth reliability checks for `ocx doctor`. Observe-only: never mutates
|
|
142
216
|
* credentials, locks, or networking. Every WARN includes a recovery Action.
|
|
@@ -147,6 +221,8 @@ export async function collectOAuthDoctorChecks(
|
|
|
147
221
|
): Promise<OAuthDoctorCheck[]> {
|
|
148
222
|
const checks: OAuthDoctorCheck[] = [];
|
|
149
223
|
|
|
224
|
+
checks.push(dataPlaneCredentialCollisionCheck());
|
|
225
|
+
|
|
150
226
|
if (isOAuthCredentialStorageWritable()) {
|
|
151
227
|
checks.push({ level: "OK", message: "OAuth credential storage directory is writable for atomic auth.json updates." });
|
|
152
228
|
} else {
|
|
@@ -935,6 +1011,9 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
935
1011
|
}
|
|
936
1012
|
|
|
937
1013
|
console.log("opencodex doctor\n");
|
|
1014
|
+
// Reset per pass: the suite drives runDoctor several times in one process, and a sticky
|
|
1015
|
+
// flag would fail the second call because the first saw a problem.
|
|
1016
|
+
doctorSawFailure = false;
|
|
938
1017
|
|
|
939
1018
|
// Ordering note: the memory/runtime section renders after "Running proxy
|
|
940
1019
|
// process proxy env" below; helpers live above runDoctor for testability.
|
|
@@ -1020,6 +1099,20 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1020
1099
|
configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
|
|
1021
1100
|
});
|
|
1022
1101
|
|
|
1102
|
+
// Mirrors `ocx status` through the same comparison rather than a second implementation:
|
|
1103
|
+
// two diagnostics disagreeing about whether an install is stale is worse than one (#2701).
|
|
1104
|
+
// No extra probe -- findLiveProxy already carried the version back.
|
|
1105
|
+
{
|
|
1106
|
+
const { packageVersion } = await import("./help");
|
|
1107
|
+
const { computeVersionSkew } = await import("./version-skew");
|
|
1108
|
+
const skew = computeVersionSkew(packageVersion(), live?.version);
|
|
1109
|
+
if (skew.skewed && skew.warning) {
|
|
1110
|
+
console.log(`!! ${skew.warning}`);
|
|
1111
|
+
} else if (skew.proxyVersion !== null) {
|
|
1112
|
+
console.log(`ok ocx ${skew.cliVersion} matches the running proxy`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1023
1116
|
const currentProxyEnv = collectProxyEnv();
|
|
1024
1117
|
const configuredProxy = collectConfiguredProxy();
|
|
1025
1118
|
const runningProxyEnv = collectRunningProxyEnv({
|
|
@@ -1148,6 +1241,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1148
1241
|
console.log("\nOAuth reliability");
|
|
1149
1242
|
for (const check of await collectOAuthDoctorChecks()) {
|
|
1150
1243
|
console.log(` [${check.level}] ${check.message}`);
|
|
1244
|
+
// A diagnostic that always exits 0 cannot gate anything, which defeats the point of
|
|
1245
|
+
// running it from a script (#2697's sibling defect). FAIL is the level reserved for a
|
|
1246
|
+
// surface that is unusable rather than degraded, so it -- and only it -- fails the
|
|
1247
|
+
// command. WARN stays exit 0 on purpose: warning on a degraded-but-working install
|
|
1248
|
+
// must not break a pipeline that is legitimately green.
|
|
1249
|
+
if (check.level === "FAIL") recordDoctorFailure();
|
|
1151
1250
|
}
|
|
1152
1251
|
|
|
1153
1252
|
// #857: a running Codex app-server can keep an older in-memory catalog than
|
package/src/cli/help.ts
CHANGED
|
@@ -5,7 +5,14 @@ import { findCommand } from "./registry";
|
|
|
5
5
|
|
|
6
6
|
const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Version of the `ocx` bundle this process is running from.
|
|
10
|
+
*
|
|
11
|
+
* Exported so `status`/`doctor` can compare it against the version the live proxy reports,
|
|
12
|
+
* which is how a stale `ocx` earlier on PATH becomes visible (#2701). Returns `"unknown"`
|
|
13
|
+
* rather than throwing; callers must treat that as "cannot compare", not as a mismatch.
|
|
14
|
+
*/
|
|
15
|
+
export function packageVersion(): string {
|
|
9
16
|
const raw = readFileSync(join(repoRoot, "package.json"), "utf8");
|
|
10
17
|
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
11
18
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
@@ -48,6 +55,7 @@ Usage:
|
|
|
48
55
|
ocx restart Stop and restart the proxy
|
|
49
56
|
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)
|
|
50
57
|
ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
|
|
58
|
+
ocx capabilities [--json] List declared capabilities and the API routes they drive
|
|
51
59
|
ocx ready [--json] [--wait [--timeout <s>]] Check post-sync readiness (exit 0 only when ready)
|
|
52
60
|
ocx provider <sub> Providers, connectivity, quota, and selected models
|
|
53
61
|
ocx account <sub> Accounts, login/reauth, key pools, and quota controls
|
|
@@ -56,11 +64,12 @@ Usage:
|
|
|
56
64
|
ocx combo <sub> Combo failover/round-robin routing
|
|
57
65
|
ocx agent <sub> Subagents, roles, injection, effort caps, and sidecars
|
|
58
66
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
67
|
+
ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
|
|
59
68
|
ocx route <sub> Routing features (combo, policy)
|
|
60
69
|
ocx logs [filters] Alias of ocx observe logs
|
|
61
70
|
ocx usage [--range <today|1d|7d|30d|all>] [--provider <name>] [--model <id>]
|
|
62
71
|
Token and estimated-cost report (alias of ocx observe usage)
|
|
63
|
-
ocx storage
|
|
72
|
+
ocx storage <sub> Storage report, cleanup, trash, and the cleanup policy
|
|
64
73
|
ocx memory [--json] Alias of ocx observe memory
|
|
65
74
|
ocx api-key <sub> Alias of ocx access key
|
|
66
75
|
ocx access <sub> External API keys and endpoint information
|
package/src/cli/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
writeRuntimePort,
|
|
27
27
|
} from "../config/process-state";
|
|
28
28
|
import { collectStatus, unusedProxyWarningLines } from "./status";
|
|
29
|
+
import { takeFlag } from "./runtime-api";
|
|
29
30
|
|
|
30
31
|
import {
|
|
31
32
|
discoverStableProxyForRestart,
|
|
@@ -45,7 +46,7 @@ import { runReady, type ReadyArgs } from "./ready";
|
|
|
45
46
|
import { runCli } from "./root";
|
|
46
47
|
import { ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
|
|
47
48
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
48
|
-
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
49
|
+
import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
49
50
|
import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
50
51
|
import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
|
|
51
52
|
import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
|
|
@@ -229,6 +230,11 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
229
230
|
// auth path reads OPENCODEX_API_AUTH_TOKEN from the environment.
|
|
230
231
|
const serviceToken = loadServiceTokenFromFile(process.env);
|
|
231
232
|
if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
|
|
233
|
+
// The service wrapper (and WinSW via OCX_API_TOKEN_FILE) can still export a colliding
|
|
234
|
+
// token that install now refuses to write. Refuse it here too, before bind, so an
|
|
235
|
+
// already-broken file cannot fence /api/* closed at boot (#2696).
|
|
236
|
+
const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
237
|
+
if (present) assertNotAdminToken(present);
|
|
232
238
|
const requestedPort = parsePortOption();
|
|
233
239
|
const owner = await findProxyOwnerBeforeJournalRecovery();
|
|
234
240
|
if (owner.live) {
|
|
@@ -836,8 +842,12 @@ async function handleUninstall() {
|
|
|
836
842
|
|
|
837
843
|
async function handleStatus() {
|
|
838
844
|
const statusArgs = args.slice(1);
|
|
839
|
-
|
|
840
|
-
|
|
845
|
+
// Order-independent: the previous form only honoured `--json` as the LONE argument, so
|
|
846
|
+
// `ocx status --json --anything` silently printed human output to a caller that asked
|
|
847
|
+
// for JSON. Take the flag out of argv, then reject whatever is left over -- which keeps
|
|
848
|
+
// the strict unknown-argument behaviour rather than trading one defect for another.
|
|
849
|
+
const wantsJson = takeFlag(statusArgs, "--json");
|
|
850
|
+
if (statusArgs.length > 0) {
|
|
841
851
|
console.error("Usage: ocx status [--json]");
|
|
842
852
|
process.exit(1);
|
|
843
853
|
}
|
|
@@ -854,6 +864,12 @@ async function handleStatus() {
|
|
|
854
864
|
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
855
865
|
}
|
|
856
866
|
console.log(` Health: ${status.healthLabel}`);
|
|
867
|
+
// Printed here, not only in --json: a stale ocx on PATH is exactly the situation where
|
|
868
|
+
// the operator is reading human output and wondering why the CLI disagrees with the
|
|
869
|
+
// dashboard. Adding the JSON field alone would satisfy a test and help nobody (#2701).
|
|
870
|
+
if (status.json.versionSkew.warning) {
|
|
871
|
+
console.log(` ⚠️ ${status.json.versionSkew.warning}`);
|
|
872
|
+
}
|
|
857
873
|
for (const line of unusedProxyWarningLines({
|
|
858
874
|
proxyUp: Boolean(status.json.proxy.pid || status.json.proxy.health.ok),
|
|
859
875
|
routingKind: status.json.startup.routingKind,
|