@bitkyc08/opencodex 2.7.42 → 2.7.43
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 +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/cli/doctor.ts
CHANGED
|
@@ -11,9 +11,11 @@ import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, readRuntimePort, resolveEnvValue } from "../config";
|
|
14
|
+
import { findLiveProxy } from "../server/proxy-liveness";
|
|
14
15
|
import { gracefulStopHost } from "../lib/process-control";
|
|
15
16
|
import { maskAccountId } from "../lib/privacy";
|
|
16
|
-
import {
|
|
17
|
+
import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
|
|
18
|
+
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
17
19
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
18
20
|
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
19
21
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
@@ -285,17 +287,15 @@ export function collectWslDualInstall(deps: WslDualInstallDeps = {}): WslDualIns
|
|
|
285
287
|
};
|
|
286
288
|
}
|
|
287
289
|
|
|
288
|
-
const PROXY_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"] as const;
|
|
289
|
-
|
|
290
290
|
export type ProxyEnvRow = { key: string; present: boolean };
|
|
291
291
|
export type EnvMap = Record<string, string | undefined>;
|
|
292
292
|
|
|
293
293
|
/** Report only presence/absence of proxy env vars - never the value (it may
|
|
294
294
|
* embed credentials). Checks both upper- and lower-case forms. */
|
|
295
295
|
export function collectProxyEnv(env: EnvMap = process.env): ProxyEnvRow[] {
|
|
296
|
-
return
|
|
296
|
+
return PROXY_ENV_KEYS.map(key => ({
|
|
297
297
|
key,
|
|
298
|
-
present:
|
|
298
|
+
present: proxyEnvPresent(key, env),
|
|
299
299
|
}));
|
|
300
300
|
}
|
|
301
301
|
|
|
@@ -571,7 +571,7 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
|
|
|
571
571
|
const lines: string[] = [];
|
|
572
572
|
lines.push(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
573
573
|
if (report.status === "unauthorized") {
|
|
574
|
-
lines.push(" -- proxy reachable but rejected the request — set
|
|
574
|
+
lines.push(" -- proxy reachable but rejected the request — set OPENCODEX_ADMIN_AUTH_TOKEN to match the service");
|
|
575
575
|
return lines;
|
|
576
576
|
}
|
|
577
577
|
if (report.status === "unreachable") {
|
|
@@ -718,9 +718,21 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
+
// #618: identity-verified liveness first so pid-file absence does not hide a live service.
|
|
722
|
+
// Reuse the diagnostics config already loaded above so doctor stays read-only on malformed JSON.
|
|
723
|
+
const live = await findLiveProxy({
|
|
724
|
+
configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
|
|
725
|
+
});
|
|
726
|
+
const livePid = live ? live.pid : readPid();
|
|
727
|
+
const liveRuntime = live
|
|
728
|
+
? { pid: live.pid ?? 0, port: live.port, hostname: live.hostname }
|
|
729
|
+
: (livePid ? readRuntimePort(livePid) : null);
|
|
730
|
+
|
|
721
731
|
const currentProxyEnv = collectProxyEnv();
|
|
722
732
|
const configuredProxy = collectConfiguredProxy();
|
|
723
|
-
const runningProxyEnv = collectRunningProxyEnv(
|
|
733
|
+
const runningProxyEnv = collectRunningProxyEnv({
|
|
734
|
+
readPidFn: () => (live ? live.pid : readPid()),
|
|
735
|
+
});
|
|
724
736
|
|
|
725
737
|
console.log("\nCurrent doctor process proxy env (presence only)");
|
|
726
738
|
for (const row of currentProxyEnv) {
|
|
@@ -742,21 +754,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
742
754
|
}
|
|
743
755
|
}
|
|
744
756
|
|
|
745
|
-
// #314: service-process memory/runtime identity via the authed management
|
|
746
|
-
// endpoint. readPid() FIRST (liveness), then the pid-scoped runtime record —
|
|
747
|
-
// readRuntimePort alone can serve a stale file pointing at a foreign port.
|
|
748
|
-
// Hoisted out of the block below: the Hints section reuses the same liveness pair
|
|
749
|
-
// for the proxy-down restart hint.
|
|
750
|
-
const livePid = readPid();
|
|
751
|
-
const liveRuntime = livePid ? readRuntimePort(livePid) : null;
|
|
752
757
|
console.log("\nMemory / runtime");
|
|
753
758
|
{
|
|
754
759
|
const runtime = liveRuntime;
|
|
755
|
-
if (!runtime) {
|
|
760
|
+
if (!runtime || !live) {
|
|
756
761
|
console.log(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
757
762
|
console.log(" -- no running ocx proxy found (no live pid/runtime record)");
|
|
758
763
|
} else {
|
|
759
|
-
const token =
|
|
764
|
+
const token = configuredAdminToken();
|
|
760
765
|
const report = await fetchServiceMemory(gracefulStopHost(runtime.hostname), runtime.port, token);
|
|
761
766
|
for (const line of formatServiceMemoryLines(report)) console.log(line);
|
|
762
767
|
}
|
|
@@ -815,8 +820,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
815
820
|
// Hints, not fixes.
|
|
816
821
|
const hints: string[] = [];
|
|
817
822
|
const proxyDown = proxyDownRestartHint({
|
|
818
|
-
proxyRunning: Boolean(
|
|
819
|
-
port: doctorConfig.port ?? 10100,
|
|
823
|
+
proxyRunning: Boolean(live),
|
|
824
|
+
port: live?.port ?? doctorConfig.port ?? 10100,
|
|
820
825
|
serviceViable: startup.serviceViable,
|
|
821
826
|
});
|
|
822
827
|
if (proxyDown) hints.push(proxyDown);
|
package/src/cli/help.ts
CHANGED
|
@@ -30,12 +30,18 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
30
30
|
uninstall: {
|
|
31
31
|
usage: "ocx uninstall",
|
|
32
32
|
summary: "Remove service/shim/config and restore native Codex.",
|
|
33
|
-
details: [
|
|
33
|
+
details: [
|
|
34
|
+
"Alias: ocx remove",
|
|
35
|
+
"Config cleanup requires ownership metadata created by a fresh install; legacy or shared directories are left in place.",
|
|
36
|
+
],
|
|
34
37
|
},
|
|
35
38
|
remove: {
|
|
36
39
|
usage: "ocx remove",
|
|
37
40
|
summary: "Remove service/shim/config and restore native Codex.",
|
|
38
|
-
details: [
|
|
41
|
+
details: [
|
|
42
|
+
"Alias of: ocx uninstall",
|
|
43
|
+
"Config cleanup requires ownership metadata created by a fresh install; legacy or shared directories are left in place.",
|
|
44
|
+
],
|
|
39
45
|
},
|
|
40
46
|
service: {
|
|
41
47
|
usage: "ocx service [install|start|stop|status|uninstall|remove]",
|
|
@@ -60,8 +66,22 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
60
66
|
],
|
|
61
67
|
},
|
|
62
68
|
ensure: { usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
|
|
63
|
-
sync: {
|
|
64
|
-
|
|
69
|
+
sync: {
|
|
70
|
+
usage: "ocx sync [--restart-codex]",
|
|
71
|
+
summary: "Fetch provider models and inject them into Codex config.",
|
|
72
|
+
details: [
|
|
73
|
+
"After writing the catalog, warns if long-lived Codex app-server processes are still running.",
|
|
74
|
+
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
"sync-cache": {
|
|
78
|
+
usage: "ocx sync-cache [--restart-codex]",
|
|
79
|
+
summary: "Refresh Codex's model cache from the active catalog.",
|
|
80
|
+
details: [
|
|
81
|
+
"Warns when Codex app-server processes still hold an in-memory model list.",
|
|
82
|
+
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
65
85
|
status: { usage: "ocx status", summary: "Check proxy server status." },
|
|
66
86
|
doctor: { usage: "ocx doctor", summary: "Diagnose environment/network issues (paths, WSL /mnt, proxy env, ChatGPT reachability)." },
|
|
67
87
|
debug: {
|
|
@@ -184,6 +204,20 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
184
204
|
"Claude Code settings: ocx claude config <status|set> ...",
|
|
185
205
|
],
|
|
186
206
|
},
|
|
207
|
+
opencode: {
|
|
208
|
+
usage: "ocx opencode [opencode args...]",
|
|
209
|
+
summary: "Launch opencode wired to the proxy (runtime provider config).",
|
|
210
|
+
details: [
|
|
211
|
+
"Ensures the proxy is running, then execs `opencode` with the generated `provider.opencodex`",
|
|
212
|
+
"block injected through OpenCode's inline runtime layer (`OPENCODE_CONFIG_CONTENT`). Any",
|
|
213
|
+
"existing inline config in the environment is preserved and only `provider.opencodex` is",
|
|
214
|
+
"overwritten for this launch.",
|
|
215
|
+
"Global/project opencode.json may be read to warn about an existing provider.opencodex",
|
|
216
|
+
"override; on-disk files are never modified.",
|
|
217
|
+
"Routed models appear in the model picker as opencodex/<provider>/<model>.",
|
|
218
|
+
"Stop using `ocx opencode` and plain `opencode` behaves exactly as before.",
|
|
219
|
+
],
|
|
220
|
+
},
|
|
187
221
|
restart: {
|
|
188
222
|
usage: "ocx restart",
|
|
189
223
|
summary: "Stop the proxy and restart it (background). Equivalent to stop + ensure.",
|
|
@@ -232,8 +266,9 @@ Usage:
|
|
|
232
266
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
|
|
233
267
|
ocx tray <sub> Windows status tray (install|start|stop|status|uninstall)
|
|
234
268
|
ocx ensure Ensure the proxy is running and Codex config/cache are current
|
|
235
|
-
ocx sync
|
|
236
|
-
ocx sync-cache
|
|
269
|
+
ocx sync [--restart-codex] Fetch models from providers and inject into Codex config
|
|
270
|
+
ocx sync-cache [--restart-codex]
|
|
271
|
+
Refresh Codex's model cache from the active catalog
|
|
237
272
|
ocx status Check proxy server status
|
|
238
273
|
ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
|
|
239
274
|
ocx debug <scope> provider/usage/injection/claude on|off|status|reset
|
|
@@ -256,6 +291,7 @@ Usage:
|
|
|
256
291
|
ocx config <sub> Validated configuration show/get/set/import/export
|
|
257
292
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
258
293
|
ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
|
|
294
|
+
ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)
|
|
259
295
|
ocx help [command] Show help
|
|
260
296
|
ocx --version | -v Print version
|
|
261
297
|
|
package/src/cli/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { rmSync } from "node:fs";
|
|
4
3
|
import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
5
4
|
import { stripGrokConfig } from "../grok/inject";
|
|
6
5
|
import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
|
|
@@ -31,7 +30,7 @@ import { stopProxy } from "../lib/process-control";
|
|
|
31
30
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
32
31
|
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
33
32
|
import { startupHealthSummary } from "../codex/autostart-health";
|
|
34
|
-
import { drainAndShutdown, startServer } from "../server";
|
|
33
|
+
import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
|
|
35
34
|
import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
|
|
36
35
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
37
36
|
import { installShellHook, uninstallShellHook } from "../server/system-env";
|
|
@@ -43,6 +42,7 @@ import { maybeShowUpdatePrompt } from "../update/notify";
|
|
|
43
42
|
import { syncModelsToCodex } from "../codex/sync";
|
|
44
43
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
45
44
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
45
|
+
import { removeOwnedConfigState } from "../lib/config-ownership";
|
|
46
46
|
|
|
47
47
|
const args = process.argv.slice(2);
|
|
48
48
|
const command = args[0];
|
|
@@ -227,24 +227,40 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
227
227
|
let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
|
|
228
228
|
|
|
229
229
|
let cleaned = false;
|
|
230
|
+
let cleanupSucceeded = true;
|
|
230
231
|
const syncCleanup = () => {
|
|
231
|
-
if (cleaned) return;
|
|
232
|
+
if (cleaned) return cleanupSucceeded;
|
|
232
233
|
cleaned = true;
|
|
233
234
|
try { guardian.stop(); } catch { /* best-effort */ }
|
|
234
235
|
try { historyGuardian?.stop(); } catch { /* best-effort */ }
|
|
235
|
-
|
|
236
|
+
// Dashboard drain-and-restart (#563) must not tear down injection: the replacement
|
|
237
|
+
// process expects Codex/Grok/env fences to still be in place.
|
|
238
|
+
const recycling = isRecyclingForExit();
|
|
239
|
+
if (!recycling) {
|
|
240
|
+
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
241
|
+
}
|
|
236
242
|
removePid(process.pid);
|
|
237
243
|
removeRuntimePort(process.pid);
|
|
238
|
-
if (!process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
|
|
239
|
-
try {
|
|
244
|
+
if (!recycling && !process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
|
|
245
|
+
try {
|
|
246
|
+
const restored = restoreNativeCodex();
|
|
247
|
+
if (!restored.success) {
|
|
248
|
+
cleanupSucceeded = false;
|
|
249
|
+
console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
cleanupSucceeded = false;
|
|
253
|
+
console.error(`⚠️ Native Codex restore failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
254
|
+
}
|
|
240
255
|
}
|
|
241
256
|
// Same ownership rule as `ocx stop`: if the installed service belongs to another home, the
|
|
242
257
|
// Grok fence is shared state we must not remove — that service keeps running and would be
|
|
243
258
|
// left pointing nowhere. This guard also covers signal-driven exits, which is the path that
|
|
244
259
|
// would otherwise bypass handleStop's gate entirely.
|
|
245
|
-
if (!process.env.OCX_SERVICE && serviceEnvironmentOwnedHere()) {
|
|
260
|
+
if (!recycling && !process.env.OCX_SERVICE && serviceEnvironmentOwnedHere()) {
|
|
246
261
|
try { stripGrokConfig(); } catch { /* best-effort restore */ }
|
|
247
262
|
}
|
|
263
|
+
return cleanupSucceeded;
|
|
248
264
|
};
|
|
249
265
|
|
|
250
266
|
let shuttingDown = false;
|
|
@@ -269,8 +285,8 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
269
285
|
try {
|
|
270
286
|
await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
271
287
|
} finally {
|
|
272
|
-
syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
273
|
-
process.exit(0);
|
|
288
|
+
const restored = syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
289
|
+
process.exit(restored ? 0 : 1);
|
|
274
290
|
}
|
|
275
291
|
})();
|
|
276
292
|
};
|
|
@@ -288,7 +304,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
288
304
|
// Auto-install .zshrc hook (idempotent — skips if already present).
|
|
289
305
|
installShellHook();
|
|
290
306
|
|
|
291
|
-
await maybeShowStarPrompt(); // once-only
|
|
307
|
+
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
292
308
|
await syncModelsToCodex(port).catch(() => {});
|
|
293
309
|
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
294
310
|
historyGuardian = startHistoryMigrationGuardian();
|
|
@@ -499,7 +515,11 @@ async function handleStop() {
|
|
|
499
515
|
}
|
|
500
516
|
if (!ownershipBlocked) {
|
|
501
517
|
const r = restoreNativeCodex();
|
|
502
|
-
console.log(`↩️ ${r.message}`);
|
|
518
|
+
if (r.success) console.log(`↩️ ${r.message}`);
|
|
519
|
+
else {
|
|
520
|
+
stopFailed = true;
|
|
521
|
+
console.error(`⚠️ ${r.message}`);
|
|
522
|
+
}
|
|
503
523
|
}
|
|
504
524
|
// revertSystemEnv is NOT gated: it carries its own ownership check and concerns launchctl
|
|
505
525
|
// user env, not CODEX_HOME. Safety net for when the daemon's syncCleanup didn't run (SIGKILL).
|
|
@@ -588,7 +608,13 @@ async function handleUninstall() {
|
|
|
588
608
|
|
|
589
609
|
if (failures.length === 0) {
|
|
590
610
|
await runStep("opencodex config removed", () => {
|
|
591
|
-
|
|
611
|
+
const result = removeOwnedConfigState(getConfigDir());
|
|
612
|
+
if (result.status === "absent") return false;
|
|
613
|
+
if (result.status === "removed") return true;
|
|
614
|
+
const residual = result.residualPaths.length > 0
|
|
615
|
+
? ` Residual path(s): ${result.residualPaths.join(", ")}`
|
|
616
|
+
: "";
|
|
617
|
+
throw new Error(`${result.status} uninstall: ${result.reason ?? "config state was not removed"}.${residual}`);
|
|
592
618
|
});
|
|
593
619
|
} else {
|
|
594
620
|
console.error("Leaving opencodex config/backups in place so the failed restore step can be retried.");
|
|
@@ -716,19 +742,40 @@ switch (command) {
|
|
|
716
742
|
console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
|
|
717
743
|
process.exit(1);
|
|
718
744
|
}
|
|
719
|
-
await syncModelsToCodex(live.port);
|
|
745
|
+
const synced = await syncModelsToCodex(live.port);
|
|
746
|
+
if (!synced.ok) {
|
|
747
|
+
process.exitCode = 1;
|
|
748
|
+
console.error("Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.");
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
720
751
|
const target = collectOrcaCodexHomeDiagnostic();
|
|
721
752
|
console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
|
|
722
753
|
break;
|
|
723
754
|
}
|
|
724
|
-
|
|
725
|
-
|
|
755
|
+
let r: { success: boolean; message: string };
|
|
756
|
+
try {
|
|
757
|
+
r = restoreNativeCodex();
|
|
758
|
+
} catch (err) {
|
|
759
|
+
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
760
|
+
}
|
|
761
|
+
if (r.success) console.log(`✅ ${r.message}`);
|
|
762
|
+
else {
|
|
763
|
+
console.error(`⚠️ ${r.message}`);
|
|
764
|
+
process.exitCode = 1;
|
|
765
|
+
}
|
|
726
766
|
try {
|
|
727
767
|
const g = stripGrokConfig();
|
|
728
768
|
if (g.changed) console.log(`✅ ${g.message}`);
|
|
729
|
-
else if (!g.ok)
|
|
769
|
+
else if (!g.ok) {
|
|
770
|
+
console.error(`⚠️ ${g.message}`);
|
|
771
|
+
process.exitCode = 1;
|
|
772
|
+
}
|
|
730
773
|
} catch { /* best-effort */ }
|
|
731
|
-
|
|
774
|
+
if (r.success) {
|
|
775
|
+
console.log("Plain `codex` now runs natively (no proxy). Switch back with: ocx restore back");
|
|
776
|
+
} else {
|
|
777
|
+
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
|
778
|
+
}
|
|
732
779
|
break;
|
|
733
780
|
}
|
|
734
781
|
case "recover-history":
|
|
@@ -767,7 +814,20 @@ switch (command) {
|
|
|
767
814
|
break;
|
|
768
815
|
}
|
|
769
816
|
case "sync": {
|
|
770
|
-
|
|
817
|
+
const restartCodex = args.slice(1).includes("--restart-codex");
|
|
818
|
+
const synced = await syncModelsToCodex((await findLiveProxy())?.port);
|
|
819
|
+
if (!synced.ok) {
|
|
820
|
+
process.exitCode = 1;
|
|
821
|
+
console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
|
|
822
|
+
}
|
|
823
|
+
// Only warn/restart when a catalog or models_cache write actually happened. This is
|
|
824
|
+
// deliberately not an `else`: refreshCodexModelCatalog runs before injectCodexConfig,
|
|
825
|
+
// so a sync can fail (`ok: false`) after the catalog was already rewritten — which is
|
|
826
|
+
// exactly when a long-lived app-server is holding the stale list.
|
|
827
|
+
if (synced.catalogWritten || synced.cacheSynced) {
|
|
828
|
+
const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
|
|
829
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
830
|
+
}
|
|
771
831
|
break;
|
|
772
832
|
}
|
|
773
833
|
case "v2": {
|
|
@@ -776,8 +836,13 @@ switch (command) {
|
|
|
776
836
|
break;
|
|
777
837
|
}
|
|
778
838
|
case "sync-cache": {
|
|
839
|
+
const restartCodex = args.slice(1).includes("--restart-codex");
|
|
779
840
|
const { invalidateCodexModelsCache } = await import("../codex/catalog");
|
|
780
|
-
|
|
841
|
+
// Only warn/restart when models_cache was actually rewritten from a readable catalog.
|
|
842
|
+
if (invalidateCodexModelsCache()) {
|
|
843
|
+
const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
|
|
844
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
845
|
+
}
|
|
781
846
|
break;
|
|
782
847
|
}
|
|
783
848
|
case "gui": {
|
|
@@ -1005,6 +1070,10 @@ switch (command) {
|
|
|
1005
1070
|
break;
|
|
1006
1071
|
}
|
|
1007
1072
|
process.exit(await cmdClaude(args.slice(1)));
|
|
1073
|
+
}
|
|
1074
|
+
case "opencode": {
|
|
1075
|
+
const { cmdOpencode } = await import("./opencode");
|
|
1076
|
+
process.exit(await cmdOpencode(args.slice(1)));
|
|
1008
1077
|
}
|
|
1009
1078
|
case "help":
|
|
1010
1079
|
case "--help":
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inline yes/no selector for interactive CLI prompts.
|
|
5
|
+
*
|
|
6
|
+
* Both choices are drawn on one line: the user moves between them with the
|
|
7
|
+
* arrow keys (or Tab), confirms with Enter, or answers straight away with
|
|
8
|
+
* `y`/`n`. Escape and Ctrl-C resolve to "no", so backing out never counts as
|
|
9
|
+
* consent.
|
|
10
|
+
*
|
|
11
|
+
* The highlighted choice is caller-supplied and is what a bare Enter returns.
|
|
12
|
+
* Whatever the default, the selector always shows which side is highlighted, so
|
|
13
|
+
* Enter never does something the screen did not already say it would.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface InteractiveConfirmOptions {
|
|
17
|
+
/** Question text rendered before the choices. May contain ANSI styling. */
|
|
18
|
+
question: string;
|
|
19
|
+
/** Which choice starts highlighted, and therefore what a bare Enter returns. */
|
|
20
|
+
defaultYes: boolean;
|
|
21
|
+
/** Key hint shown after the choices. */
|
|
22
|
+
hint?: string;
|
|
23
|
+
/** Overridable for tests; defaults to the process streams. */
|
|
24
|
+
input?: NodeJS.ReadStream;
|
|
25
|
+
output?: NodeJS.WriteStream;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const REVERSE = "\x1b[7m";
|
|
29
|
+
const DIM = "\x1b[2m";
|
|
30
|
+
const RESET = "\x1b[0m";
|
|
31
|
+
const CLEAR_LINE = "\r\x1b[K";
|
|
32
|
+
|
|
33
|
+
const KEY_ENTER = new Set(["\r", "\n"]);
|
|
34
|
+
const KEY_YES_SIDE = new Set(["\x1b[D", "\x1b[A", "\x1bOD", "\x1bOA"]); // left / up
|
|
35
|
+
const KEY_NO_SIDE = new Set(["\x1b[C", "\x1b[B", "\x1bOC", "\x1bOB"]); // right / down
|
|
36
|
+
const KEY_ESCAPE = "\x1b";
|
|
37
|
+
const KEY_INTERRUPT = "\x03";
|
|
38
|
+
const KEY_TAB = "\t";
|
|
39
|
+
|
|
40
|
+
function renderChoices(question: string, yes: boolean, hint: string): string {
|
|
41
|
+
const yesLabel = yes ? `${REVERSE} Yes ${RESET}` : `${DIM} Yes ${RESET}`;
|
|
42
|
+
const noLabel = yes ? `${DIM} No ${RESET}` : `${REVERSE} No ${RESET}`;
|
|
43
|
+
return `${CLEAR_LINE}${question} ${yesLabel} ${noLabel} ${DIM}${hint}${RESET}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderAnswer(question: string, yes: boolean): string {
|
|
47
|
+
return `${CLEAR_LINE}${question} ${yes ? "Yes" : "No"}\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Fallback for terminals without raw mode: a plain typed answer. */
|
|
51
|
+
async function readlineConfirm(
|
|
52
|
+
options: InteractiveConfirmOptions,
|
|
53
|
+
input: NodeJS.ReadStream,
|
|
54
|
+
output: NodeJS.WriteStream,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
const suffix = options.defaultYes ? "[Y/n]" : "[y/N]";
|
|
57
|
+
const rl = createInterface({ input, output });
|
|
58
|
+
try {
|
|
59
|
+
const answer = (await rl.question(`${options.question} ${suffix} `)).trim().toLowerCase();
|
|
60
|
+
if (answer === "") return options.defaultYes;
|
|
61
|
+
return answer === "y" || answer === "yes";
|
|
62
|
+
} finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ask a yes/no question with an inline arrow-key selector. Resolves to the
|
|
69
|
+
* user's choice; never throws for input handling and always restores the
|
|
70
|
+
* terminal mode it changed.
|
|
71
|
+
*/
|
|
72
|
+
export async function interactiveConfirm(options: InteractiveConfirmOptions): Promise<boolean> {
|
|
73
|
+
const input = options.input ?? process.stdin;
|
|
74
|
+
const output = options.output ?? process.stdout;
|
|
75
|
+
const hint = options.hint ?? "←/→ move · y/n · enter";
|
|
76
|
+
|
|
77
|
+
// Raw mode is what makes single-keypress navigation possible. Without it
|
|
78
|
+
// (pipes, some CI shells, Windows consoles without a TTY) fall back to a
|
|
79
|
+
// typed answer rather than silently swallowing the question.
|
|
80
|
+
if (typeof input.setRawMode !== "function") {
|
|
81
|
+
return await readlineConfirm(options, input, output);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return await new Promise<boolean>(resolve => {
|
|
85
|
+
let yes = options.defaultYes;
|
|
86
|
+
const wasRaw = input.isRaw === true;
|
|
87
|
+
const hadOtherReaders = input.listenerCount("data") > 0;
|
|
88
|
+
|
|
89
|
+
const paint = () => {
|
|
90
|
+
output.write(renderChoices(options.question, yes, hint));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const finish = (answer: boolean, interrupted: boolean) => {
|
|
94
|
+
input.off("data", onData);
|
|
95
|
+
if (!wasRaw) input.setRawMode(false);
|
|
96
|
+
if (!hadOtherReaders) input.pause();
|
|
97
|
+
output.write(renderAnswer(options.question, answer));
|
|
98
|
+
resolve(answer);
|
|
99
|
+
// A Ctrl-C during the prompt is still a Ctrl-C: hand it back to the
|
|
100
|
+
// process so the normal shutdown path runs instead of being eaten here.
|
|
101
|
+
if (interrupted) process.kill(process.pid, "SIGINT");
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onData = (chunk: Buffer | string) => {
|
|
105
|
+
const key = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
106
|
+
if (key === KEY_INTERRUPT) return finish(false, true);
|
|
107
|
+
if (key === KEY_ESCAPE) return finish(false, false);
|
|
108
|
+
if (KEY_ENTER.has(key)) return finish(yes, false);
|
|
109
|
+
const lower = key.toLowerCase();
|
|
110
|
+
if (lower === "y") return finish(true, false);
|
|
111
|
+
if (lower === "n") return finish(false, false);
|
|
112
|
+
if (KEY_YES_SIDE.has(key)) {
|
|
113
|
+
yes = true;
|
|
114
|
+
paint();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (KEY_NO_SIDE.has(key) || key === KEY_TAB) {
|
|
118
|
+
yes = key === KEY_TAB ? !yes : false;
|
|
119
|
+
paint();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
input.setRawMode(true);
|
|
125
|
+
input.resume();
|
|
126
|
+
input.on("data", onData);
|
|
127
|
+
paint();
|
|
128
|
+
});
|
|
129
|
+
}
|