Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ ocx start [--port 10100] # start the proxy in the foreground
ocx stop # stop + restore native Codex
ocx service [install|start|stop|status|uninstall|remove] # background service
ocx codex-shim install # start the proxy on demand whenever `codex` launches
ocx health [--json] # check immediate proxy liveness
ocx ready [--json] [--wait [--timeout <seconds>]] # check post-sync readiness
ocx status # is the proxy running?
ocx gui # open the web dashboard
ocx provider <...> # manage providers (list/add/edit/test/remove)
Expand All @@ -151,6 +153,27 @@ ocx update [--tag preview] # update opencodex
Unpinned starts may pick another free port if the preferred one is busy; an explicit `--port`
never hops. Full reference: [CLI docs](https://opencodex.me/reference/cli/).

### Health and readiness

`GET /healthz` reports immediate proxy liveness. The unauthenticated `GET /readyz` endpoint reports
post-sync readiness with the sanitized JSON identity `{service, version, uptime, pid, port, status}`.
It returns `200` when `status` is `ready`; `pending` and terminal `failed` return `503` with
`Retry-After: 1`.

`ocx ready [--json] [--wait [--timeout <seconds>]]` performs one probe by default. `--wait` polls
for up to 45 seconds by default, but exits immediately when it observes terminal `failed`;
`--timeout <seconds>` sets a 1–300 second limit, requires `--wait`, and accepts only positive integers. CLI `--json` output is
`{ready, status, pid, port}`, where `status` is `ready`, `pending`, `failed`, or `unreachable`.

| Exit | Result |
| --- | --- |
| `0` | Ready |
| `1` | Not ready: pending, failed, timeout, or unreachable |
| `64` | Invalid arguments |

An older proxy without `/readyz` fails closed as `unreachable` with exit 1, while `ocx health`
remains compatible.

### Autostart: service vs shim

Use the **service** (`ocx service`) for an always-on proxy that restarts on crash. Use the
Expand Down
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/ja/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ ocx status --json

稼働中のプロキシの ID を確認します。ヒューマン出力は PID/ポートをレポートします。 `--json` は `{ok, pid, port}` を出力します。このコマンドは正常な場合のみ 0 で終了し、それ以外の場合は 1 で終了するため、サービス プローブに適しています。

### `ocx ready [--json] [--wait [--timeout <seconds>]]`

認証不要の `GET /readyz` エンドポイントで同期後の準備状態を確認します。準備完了時は `200`、
`pending` または終端状態の `failed` では `Retry-After: 1` とともに `503` を返します。HTTP の
サニタイズ済み識別フィールドは `{service, version, uptime, pid, port, status}` です。`/readyz` がない
旧プロキシは `unreachable` として fail-closed し、`/healthz` は readiness ではなく別の liveness 確認です。
デフォルトでは 1 回だけ probe します。`--wait` は準備完了または timeout まで polling しますが、
終端 `failed` を確認すると即座に終了します。デフォルト timeout は 45 秒で、`--timeout <seconds>` には
`--wait` が必要です(1〜300 秒の正の整数)。CLI JSON は
`{ready, status, pid, port}` を出力し、`status` は `ready`、`pending`、`failed`、`unreachable` の
いずれかです。終了コードは ready が 0、not-ready/pending/failed/timeout/unreachable が 1、
不正な引数が 64 です。

### `ocx doctor`

読み取り専用環境と接続の診断を実行します: 状態パスとファイル システム タイプ、WSL デュアル インストール、プロキシ環境/構成、ChatGPT の到達可能性、Codex プラグインとプロジェクト設定の警告、保留中の履歴の移行。 Codex のアプリとホームのターゲット設定セクションでは、Windows Orca ランタイムとホームの狭い不一致も検出し、該当する場合はサービスの移行について説明します。この診断によって表示されるパスでは、OS ユーザー名が編集されます。医師は修復ヒントを出力しますが、適用しません。
Expand Down
12 changes: 12 additions & 0 deletions docs-site/src/content/docs/ko/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ ocx status --json
`{ok, pid, port}`를 내보냅니다. 이 명령은 정상일 때만 종료 코드 0을, 그렇지 않으면 1을 반환하므로
서비스 프로브에 적합합니다.

### `ocx ready [--json] [--wait [--timeout <seconds>]]`

인증이 필요 없는 `GET /readyz` 엔드포인트로 동기화 후 준비 상태를 확인합니다. 준비되면 `200`,
`pending` 또는 종단 상태인 `failed`이면 `Retry-After: 1`과 함께 `503`을 반환합니다. HTTP의 정제된
식별 필드는 `{service, version, uptime, pid, port, status}`입니다. `/readyz`가 없는 이전 프록시는
`unreachable`로 fail-closed하며, `/healthz`는 준비 상태가 아닌 별도의 liveness 확인입니다. 기본값은 한 번의
probe이며, `--wait`는 준비 또는 timeout까지 polling하지만 종단 `failed`를 확인하면 즉시 종료합니다.
기본 timeout은 45초이며, `--timeout <seconds>`는 `--wait`와 함께 써야 하고 양의 정수인 1~300초 범위를 받습니다. CLI JSON은
`{ready, status, pid, port}`를 출력하며 `status`는 `ready`, `pending`, `failed`,
`unreachable` 중 하나입니다. 종료 코드는 ready가 0, not-ready/pending/failed/timeout/unreachable이
1, 잘못된 인수가 64입니다.

### `ocx doctor`

읽기 전용 환경 및 연결 진단을 실행합니다. 상태 경로와 파일시스템 유형, WSL 이중 설치, 프록시
Expand Down
12 changes: 12 additions & 0 deletions docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ tokens, authorization headers, request content, emails, and account identities.
Identity-check the live proxy. Human output reports PID/port; `--json` emits `{ok, pid, port}`. The
command exits 0 only when healthy and 1 otherwise, making it suitable for service probes.

### `ocx ready [--json] [--wait [--timeout <seconds>]]`

Check post-sync readiness through the unauthenticated `GET /readyz` endpoint. It returns `200` when
ready, or `503` with `Retry-After: 1` for `pending` and terminal `failed`. Its sanitized HTTP identity
is `{service, version, uptime, pid, port, status}`. Old proxies without `/readyz` fail closed as
`unreachable`; `/healthz` is separate liveness, not readiness. The command performs one probe by
default; `--wait` polls until ready or timeout, but exits immediately when it observes the terminal `failed` state. The
default timeout is 45 seconds; `--timeout <seconds>` requires `--wait` and accepts positive integer seconds from 1–300.
CLI JSON emits `{ready, status, pid, port}`, where `status` is `ready`, `pending`, `failed`, or
`unreachable`. Exit codes are 0 for ready; 1 for not-ready, pending, failed, timeout, or
unreachable; and 64 for invalid arguments.

### `ocx doctor`

Run read-only environment and connectivity diagnostics: state paths and filesystem type, WSL dual
Expand Down
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/ru/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ Identity-check живого прокси. Текстовый вывод сооб
`{ok, pid, port}`. Команда завершается кодом 0 только когда прокси здоров, и 1 во всех остальных
случаях, поэтому подходит для service probe.

### `ocx ready [--json] [--wait [--timeout <seconds>]]`

Проверяет готовность после синхронизации через не требующий аутентификации `GET /readyz`. При
готовности возвращается `200`; для `pending` и терминального `failed` возвращается `503` с
`Retry-After: 1`. Санитизированные поля HTTP-ответа: `{service, version, uptime, pid, port, status}`.
Старые прокси без `/readyz` fail-closed как `unreachable`; `/healthz` — отдельная проверка liveness,
а не готовности. По умолчанию команда выполняет одну пробу. `--wait` опрашивает до готовности или
тайм-аута, но при терминальном `failed` завершается немедленно. Тайм-аут по умолчанию — 45 секунд;
`--timeout <seconds>` требует `--wait` и принимает целые положительные значения 1–300 секунд. CLI JSON выдаёт
`{ready, status, pid, port}`, где `status` — `ready`, `pending`, `failed` или
`unreachable`. Коды завершения: 0 — готово; 1 — не готово, pending, failed, тайм-аут или
недоступность; 64 — недопустимые аргументы.

### `ocx doctor`

Запускает read-only диагностику среды и связности: пути состояний и тип файловой системы,
Expand Down
10 changes: 10 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ ocx status --json

对正在运行的代理做身份校验。人类可读输出报告 PID/端口;`--json` 输出 `{ok, pid, port}`。只有在健康时该命令才以 0 退出,否则以 1 退出,因此适合用作服务探针。

### `ocx ready [--json] [--wait [--timeout <seconds>]]`

通过无需认证的 `GET /readyz` 端点检查同步后的就绪状态。就绪时返回 `200`;状态为 `pending` 或
终态 `failed` 时返回 `503`,并带有 `Retry-After: 1`。HTTP 仅返回经脱敏的身份字段
`{service, version, uptime, pid, port, status}`。不支持 `/readyz` 的旧代理会按 `unreachable` 失败关闭;
`/healthz` 是独立的存活检查,不是就绪检查。默认只探测一次;`--wait` 会轮询到就绪或超时,但遇到终态
`failed` 会立即退出。默认超时为 45 秒;`--timeout <seconds>` 必须与 `--wait` 一起使用,取值范围为 1–300 秒的正整数。CLI JSON
输出 `{ready, status, pid, port}`,其中 `status` 为 `ready`、`pending`、`failed` 或
`unreachable`。退出码:就绪为 0;未就绪、pending、failed、超时或无法连接为 1;参数无效为 64。

### `ocx doctor`

运行只读的环境与连通性诊断:状态路径和文件系统类型、WSL 双重安装、代理环境/配置、ChatGPT 可达性、Codex 插件和项目配置警告,以及待处理的历史迁移。Codex app-home 定位部分也会检测狭义的 Windows Orca 运行时 home 不匹配,并在适用时解释服务迁移。此诊断展示的路径会对操作系统用户名进行脱敏。Doctor 会输出修复提示,但不会自动应用。
Expand Down
14 changes: 14 additions & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,19 @@ const helpEntries: Record<string, HelpEntry> = {
summary: "Check proxy health. Exits 0 if healthy, 1 otherwise.",
details: ["Use --json for structured output: {ok, pid, port}."],
},
ready: {
usage: "ocx ready [--json] [--wait [--timeout <seconds>]]",
summary: "Check post-sync readiness. Exits 0 only when ready.",
details: [
"Exact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.",
"Its sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.",
"Default is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.",
"--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).",
"--timeout requires --wait and accepts a positive integer (1..300).",
"--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.",
"Invalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.",
],
},
};

function packageVersion(): string {
Expand Down Expand Up @@ -290,6 +303,7 @@ Usage:
ocx restart Stop and restart the proxy
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|threads)
ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
ocx ready [--json] [--wait [--timeout <s>]] Check post-sync readiness (exit 0 only when ready)
ocx provider <sub> Providers, connectivity, quota, and selected models
ocx account <sub> Accounts, login/reauth, key pools, and quota controls
ocx models <sub> Live/custom models, visibility, context, and shadow calls
Expand Down
52 changes: 50 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { installCrashGuards } from "../lib/crash-guard";
import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
import { createReadinessGate } from "../server/readiness";
import { parseReadyArgs, runReady, type ReadyArgs } from "./ready";
import { stopProxy } from "../lib/process-control";
import { loadServiceTokenFromFile } from "../lib/service-secrets";
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
Expand Down Expand Up @@ -69,6 +71,23 @@ if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
process.exit(0);
}

// P1: pre-parse `ocx ready` and reject invalid arguments with exit 64 BEFORE
// maybeAutoRestoreCodexShim (or any discovery/probe/filesystem-capable global
// preflight) runs. `ready --help` / `help ready` already exited above, so this
// only sees ready args without a help flag. Valid args are stashed so the
// switch dispatch can call runReady without a second parse.
let readyArgs: ReadyArgs | undefined;
if (command === "ready") {
const parsed = parseReadyArgs(args.slice(1));
if (!parsed.ok) {
console.error("Usage: ocx ready [--json] [--wait [--timeout <seconds>]]");
console.error(" --timeout requires --wait; <seconds> must be a positive integer (1..300).");
console.error(" Default wait timeout is 45 seconds.");
process.exit(parsed.code);
}
readyArgs = parsed.args;
}

maybeAutoRestoreCodexShim(command, args);

function parsePortOption(): number | undefined {
Expand Down Expand Up @@ -197,11 +216,16 @@ async function handleStart(options: { block?: boolean } = {}) {
// between the probe and Bun.serve. Soft starts may re-pick; hard-pinned `--port` retries
// the same port only (never hop — that was the remaining PR #152 gap).
let port = await chooseListenPort(requestedPort);
// One private readiness gate for this startServer invocation, captured by the
// listener's closure. handleStart owns it and transitions it after the
// post-startup sync settles. A second startServer in the same process would
// get its own gate and could never reset/mutate this one.
const readinessGate = createReadinessGate();
let server: ReturnType<typeof startServer>;
const localAttestationSecret = createLocalAttestationSecret();
for (let attempt = 0; ; attempt++) {
try {
server = startServer(port, { localAttestationSecret });
server = startServer(port, { localAttestationSecret, readinessGate });
// Prewarm the live provider model cache as soon as the port is bound so the
// first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
// instead of racing duplicate upstream /models fetches.
Expand Down Expand Up @@ -321,7 +345,12 @@ async function handleStart(options: { block?: boolean } = {}) {
installShellHook();

await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
const startupSync = await syncCodexOnStartIfEnabled(port, config);
// Post-startup sync drives the readiness gate AND the #1046 stale app-server
// warning. `syncCodexOnStartIfEnabled` respects the Codex integration toggle
// (OFF → no sync) and reports whether anything was written; the readiness gate
// observes the real sync outcome (ok/warning) so /readyz never advertises a
// half-synced proxy as ready while /healthz stays live.
const startupSync = await syncCodexOnStartIfEnabled(port, config, undefined, readinessGate);
// #1046: one warning per startup, after BOTH writes. The server's cache
// invalidation happens first and the catalog sync second, so the mtime is only
// final here — and neither write site warns on its own, or a boot that hits
Expand Down Expand Up @@ -757,6 +786,17 @@ async function handleRecoverHistory() {
console.log(`Recovered ${r.rows} legacy thread(s) to openai (${r.files} rollout file(s) updated).`);
}

/**
* `ocx ready` — arguments are pre-parsed above (before
* maybeAutoRestoreCodexShim) so invalid usage exits 64 before any global
* preflight. This handler only runs the dependency-injected runner in ./ready
* and exits with the returned code; it performs no parsing and no I/O of its
* own. The full behavior is unit-testable without spawning a subprocess.
*/
async function handleReady(args: ReadyArgs): Promise<never> {
process.exit(await runReady(args));
}

switch (command) {
case "init":
case "setup": {
Expand Down Expand Up @@ -1016,6 +1056,14 @@ switch (command) {
}
process.exit(live ? 0 : 1);
}
case "ready":
// Fail-closed impossible-state guard: readyArgs is populated by the
// preparse block before maybeAutoRestoreCodexShim, so reaching here
// without it means dispatch diverged. Refuse with code 64 and perform
// NO I/O (no discovery/probe). process.exit is `never`, narrowing below.
if (!readyArgs) process.exit(64);
await handleReady(readyArgs);
break;
case "provider": {
const { handleProviderCommand } = await import("./provider");
await handleProviderCommand(args.slice(1));
Expand Down
Loading
Loading