Skip to content

Commit 776b406

Browse files
committed
Merge branch 'task/ocx-status-diagnostics' into dev
2 parents 23f3996 + eef546c commit 776b406

6 files changed

Lines changed: 113 additions & 10 deletions

File tree

docs-site/src/content/docs/ko/reference/cli.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ ocx start --port 8080
3737

3838
### `ocx status`
3939

40-
프록시가 실행 중인지(그리고 해당 PID) 여부를 출력합니다.
40+
읽기 전용 진단 요약을 출력합니다: 프록시 PID, `/healthz` 연결 상태, 대시보드 URL,
41+
설정 파일 경로, 기본 프로바이더, Codex 자동 시작 설정, 서비스 상태, shim 상태.
4142

4243
## 모델 및 Codex
4344

docs-site/src/content/docs/reference/cli.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ catalog entries so plain `codex` works natively again. `eject` is an alias of `r
3737

3838
### `ocx status`
3939

40-
Print whether the proxy is running (and its PID) or not.
40+
Print a read-only diagnostic summary: proxy PID, `/healthz` reachability, dashboard URL,
41+
config path, default provider, Codex autostart setting, service state, and shim state.
4142

4243
## Models & Codex
4344

docs-site/src/content/docs/zh-cn/reference/cli.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ ocx start --port 8080
3232

3333
### `ocx status`
3434

35-
打印代理是否正在运行(及其 PID)。
35+
打印只读诊断摘要: 代理 PID、`/healthz` 可达性、仪表盘 URL、配置文件路径、默认
36+
provider、Codex 自动启动设置、服务状态和 shim 状态。
3637

3738
## 模型与 Codex
3839

src/cli.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { execFileSync, spawn } from "node:child_process";
33
import { rmSync } from "node:fs";
44
import { restoreNativeCodex } from "./codex-inject";
55
import { restoreLegacyOpenaiHistory } from "./codex-history-provider";
6-
import { codexAutoStartEnabled, getConfigDir, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
6+
import { codexAutoStartEnabled, getConfigDir, getConfigPath, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
77
import { findAvailablePort } from "./ports";
8-
import { serviceCommand, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
8+
import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
99
import { drainAndShutdown, startServer } from "./server";
1010
import { maybeShowStarPrompt } from "./star-prompt";
1111

@@ -365,13 +365,56 @@ async function handleUninstall() {
365365
console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @bitkyc08/opencodex");
366366
}
367367

368-
function handleStatus() {
368+
type HealthCheck = {
369+
ok: boolean;
370+
label: string;
371+
};
372+
373+
async function checkProxyHealth(port: number, hostname?: string): Promise<HealthCheck> {
374+
const url = `http://${healthHost(hostname)}:${port}/healthz`;
375+
const controller = new AbortController();
376+
const timer = setTimeout(() => controller.abort(), 800);
377+
try {
378+
const response = await fetch(url, { signal: controller.signal });
379+
if (!response.ok) return { ok: false, label: `${url} returned HTTP ${response.status}` };
380+
const body = await response.json().catch(() => null) as { version?: unknown; uptime?: unknown } | null;
381+
const version = typeof body?.version === "string" ? ` v${body.version}` : "";
382+
const uptime = typeof body?.uptime === "number" ? `, uptime ${Math.round(body.uptime)}s` : "";
383+
return { ok: true, label: `${url} ok${version}${uptime}` };
384+
} catch (error) {
385+
const reason = error instanceof Error && error.name === "AbortError" ? "timed out" : "unreachable";
386+
return { ok: false, label: `${url} ${reason}` };
387+
} finally {
388+
clearTimeout(timer);
389+
}
390+
}
391+
392+
async function handleStatus() {
393+
const config = loadConfig();
394+
const port = config.port ?? 10100;
369395
const pid = readPid();
370-
if (pid) {
371-
console.log(`✅ Proxy running (PID ${pid})`);
396+
const health = await checkProxyHealth(port, config.hostname);
397+
const proxyLabel = pid && health.ok
398+
? `running (PID ${pid})`
399+
: pid
400+
? `PID file points to PID ${pid}, but health check failed`
401+
: health.ok
402+
? "reachable, but PID file is missing or stale"
403+
: "not running";
404+
405+
if (pid || health.ok) {
406+
console.log(`✅ Proxy: ${proxyLabel}`);
372407
} else {
373-
console.log("❌ Proxy not running");
408+
console.log(`❌ Proxy: ${proxyLabel}`);
374409
}
410+
console.log(` Health: ${health.label}`);
411+
console.log(` Dashboard: http://localhost:${port}/`);
412+
console.log(` Config: ${getConfigPath()}`);
413+
console.log(` Default provider: ${config.defaultProvider}`);
414+
console.log(` Codex autostart: ${codexAutoStartEnabled(config) ? "enabled" : "disabled"}`);
415+
console.log(` Service: ${serviceStatusSummary()}`);
416+
const { codexShimStatus } = await import("./codex-shim");
417+
console.log(` ${codexShimStatus()}`);
375418
}
376419

377420
function handleRecoverHistory() {
@@ -411,7 +454,7 @@ switch (command) {
411454
await handleUninstall();
412455
break;
413456
case "status":
414-
handleStatus();
457+
await handleStatus();
415458
break;
416459
case "ensure":
417460
await handleEnsure();

src/service.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,26 @@ export function uninstallServiceIfInstalled(): boolean {
283283
return false;
284284
}
285285

286+
export function serviceStatusSummary(): string {
287+
if (process.platform === "darwin") {
288+
if (!existsSync(plistPath())) return "not installed";
289+
const status = statusLaunchd();
290+
return status ? "installed (launchd)" : "installed, not loaded";
291+
}
292+
if (process.platform === "win32") {
293+
const status = statusWindows();
294+
return status ? "installed (Task Scheduler)" : "not installed";
295+
}
296+
if (process.platform === "linux") {
297+
if (existsSync("/.dockerenv")) return "unsupported in Docker";
298+
if (!isSystemd()) return "unsupported: systemd not found";
299+
if (!existsSync(unitPath())) return "not installed";
300+
const status = statusSystemd();
301+
return status ? "installed (systemd user)" : "installed, not running";
302+
}
303+
return `unsupported on ${process.platform}`;
304+
}
305+
286306
export function serviceCommand(sub?: string): void {
287307
const ops = platformOps();
288308
if (!ops) {

tests/cli-help.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@ const repoRoot = dirname(fileURLToPath(new URL("../package.json", import.meta.ur
99
const cliPath = join(repoRoot, "src", "cli.ts");
1010

1111
describe("CLI subcommand help", () => {
12+
test("status prints diagnostics without starting the proxy", () => {
13+
const opencodexHome = mkdtempSync(join(tmpdir(), "ocx-status-"));
14+
try {
15+
const configPath = join(opencodexHome, "config.json");
16+
writeFileSync(configPath, JSON.stringify({
17+
port: 9,
18+
providers: {
19+
openai: {
20+
adapter: "openai-responses",
21+
baseUrl: "https://chatgpt.com/backend-api/codex",
22+
authMode: "forward",
23+
},
24+
},
25+
defaultProvider: "openai",
26+
codexAutoStart: false,
27+
}), "utf8");
28+
29+
const result = spawnSync(process.execPath, [cliPath, "status"], {
30+
cwd: repoRoot,
31+
env: { ...process.env, OPENCODEX_HOME: opencodexHome },
32+
encoding: "utf8",
33+
});
34+
35+
expect(result.status).toBe(0);
36+
expect(result.stdout).toContain("Proxy:");
37+
expect(result.stdout).toContain("Health: http://127.0.0.1:9/healthz");
38+
expect(result.stdout).toContain("Dashboard: http://localhost:9/");
39+
expect(result.stdout).toContain(`Config: ${configPath}`);
40+
expect(result.stdout).toContain("Default provider: openai");
41+
expect(result.stdout).toContain("Codex autostart: disabled");
42+
expect(result.stdout).toContain("Service:");
43+
expect(result.stdout).toContain("Codex autostart shim");
44+
} finally {
45+
rmSync(opencodexHome, { recursive: true, force: true });
46+
}
47+
});
48+
1249
test("restore --help prints usage without mutating Codex config", () => {
1350
const codexHome = mkdtempSync(join(tmpdir(), "ocx-help-"));
1451
try {

0 commit comments

Comments
 (0)