From 1bf07e8c39f791e931eeaf33627961606a664757 Mon Sep 17 00:00:00 2001 From: flyingsquirrel0419 Date: Wed, 24 Jun 2026 23:31:21 +0900 Subject: [PATCH] feat(cli): add --background / -b flag to ocx start Runs the proxy as a detached child process so the terminal can be closed without killing the server. Uses OCX_BACKGROUND_SPAWNED to prevent recursive spawns. Updates CLI help, READMEs, docs-site CLI reference, and adds tests. --- README.ko.md | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- docs-site/src/content/docs/reference/cli.md | 7 +++- src/cli.ts | 36 ++++++++++++++++++--- tests/cli-background.test.ts | 34 +++++++++++++++++++ 6 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 tests/cli-background.test.ts diff --git a/README.ko.md b/README.ko.md index 014f7964f34..9e94af3fe98 100644 --- a/README.ko.md +++ b/README.ko.md @@ -126,7 +126,7 @@ codex -m "xai/grok-4" "이 PR을 리뷰해 줘" ```bash ocx init # 대화형 설정 -ocx start [--port 10100] # 프록시 시작; 포트가 사용 중이면 빈 포트로 자동 전환 +ocx start [--port 10100] [-b|--background] # 프록시 시작; --background로 터미널 분리; 포트가 사용 중이면 빈 포트로 자동 전환 ocx stop # 프록시 중지 + Codex 원래 설정 복원 ocx restore # 중지 없이 복원 (별칭: ocx eject) ocx uninstall # service/shim/config 제거 + Codex 원본 복원 diff --git a/README.md b/README.md index 82053d38e44..1928cbdb826 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Plus DeepSeek, Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging ```bash ocx init # interactive setup -ocx start [--port 10100] # start the proxy; falls back to a free port if busy +ocx start [--port 10100] [-b|--background] # start the proxy; --background detaches from terminal; falls back to a free port if busy ocx stop # stop + restore native Codex ocx restore # restore without stopping (alias: ocx eject) ocx uninstall # remove service/shim/config and restore native Codex diff --git a/README.zh-CN.md b/README.zh-CN.md index 14cde395a41..76f18cce019 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -120,7 +120,7 @@ codex -m "deepseek/deepseek-r1" "分析这个性能瓶颈" ```bash ocx init # 交互式初始化 -ocx start [--port 10100] # 启动代理 +ocx start [--port 10100] [-b|--background] # 启动代理;--background 脱离终端 ocx stop # 停止并恢复原生 Codex 配置 ocx restore # 仅恢复,不停止(别名:ocx eject) ocx sync # 刷新模型列表 + 重新注入 Codex diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 689f78ed003..843bfa17674 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -13,15 +13,20 @@ Interactive setup wizard. Prompts for a provider (preset or custom), API key (li default model, and proxy port; saves `~/.opencodex/config.json`; and optionally injects the proxy into `$CODEX_HOME/config.toml` (default `~/.codex/config.toml`). -### `ocx start [--port ]` +### `ocx start [--port ] [-b|--background]` Start the proxy server (default port `10100`). Writes a PID file and refuses to start a second instance. On start it syncs each provider's models into Codex's catalog. On shutdown it restores native Codex — unless it was launched as a managed service (`OCX_SERVICE=1`). +Use `--background` (or `-b`) to detach from the terminal; the proxy keeps running after you close +the shell. This is useful on remote servers where you do not want to install a full systemd unit. + ```bash ocx start ocx start --port 8080 +ocx start -b +ocx start --port 8080 --background ``` ### `ocx stop` diff --git a/src/cli.ts b/src/cli.ts index 7c2263e0563..582a80ac8b3 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,7 +17,7 @@ function printUsage() { Usage: ocx init Interactive setup (provider + Codex config injection) - ocx start [--port ] Start the proxy server (auto-syncs models to Codex) + ocx start [--port ] [-b|--background] Start the proxy server (auto-syncs models to Codex) ocx stop Stop the proxy AND restore native Codex (plain codex works again) ocx restore Restore native Codex without stopping (alias: eject) ocx recover-history --legacy-openai @@ -51,7 +51,7 @@ function printSubcommandUsage(name: string | undefined): void { console.log("Usage: ocx init\n\nInteractive setup for providers and Codex config injection."); break; case "start": - console.log("Usage: ocx start [--port ]\n\nStart the proxy server and sync models to Codex."); + console.log("Usage: ocx start [--port ] [-b|--background]\n\nStart the proxy server and sync models to Codex.\nUse --background to detach from the terminal."); break; case "stop": console.log("Usage: ocx stop\n\nStop the proxy and restore native Codex config."); @@ -141,6 +141,10 @@ function parsePortOption(): number | undefined { return port; } +function parseBackgroundFlag(): boolean { + return args.includes("-b") || args.includes("--background"); +} + function healthHost(hostname?: string): string { return !hostname || hostname === "0.0.0.0" || hostname === "::" ? "127.0.0.1" : hostname; } @@ -183,7 +187,7 @@ async function chooseListenPort(requestedPort?: number): Promise { return selected; } -async function handleStart(options: { block?: boolean } = {}) { +async function handleStart(options: { block?: boolean; background?: boolean } = {}) { const existingPid = readPid(); if (existingPid) { const config = loadConfig(); @@ -197,6 +201,30 @@ async function handleStart(options: { block?: boolean } = {}) { const requestedPort = parsePortOption(); const port = await chooseListenPort(requestedPort); + // Avoid recursive background spawns: the detached child just runs foreground. + if (options.background && !process.env.OCX_BACKGROUND_SPAWNED) { + const spawnArgs = [process.argv[1], "start"]; + if (requestedPort !== undefined) { + spawnArgs.push("--port", String(port)); + } + const child = spawn(process.execPath, spawnArgs, { + detached: true, + stdio: "ignore", + env: { ...process.env, OCX_BACKGROUND_SPAWNED: "1" }, + }); + child.unref(); + + const healthyPort = await waitForProxy(10_000); + if (!healthyPort) { + console.error("❌ Proxy did not become healthy in the background."); + process.exit(1); + } + const config = loadConfig(); + const serverPid = readPid(); + console.log(`✅ Proxy running in background on port ${config.port ?? healthyPort}${serverPid ? ` (PID ${serverPid})` : ""}.`); + return; + } + const server = startServer(port); writePid(process.pid); @@ -390,7 +418,7 @@ switch (command) { break; } case "start": - await handleStart(); + await handleStart({ background: parseBackgroundFlag() }); break; case "stop": handleStop(); diff --git a/tests/cli-background.test.ts b/tests/cli-background.test.ts new file mode 100644 index 00000000000..ac12f813f7c --- /dev/null +++ b/tests/cli-background.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = dirname(fileURLToPath(new URL("../package.json", import.meta.url))); +const cliPath = join(repoRoot, "src", "cli.ts"); + +describe("CLI background flag", () => { + test("start --help advertises --background option", () => { + const result = spawnSync(process.execPath, [cliPath, "start", "--help"], { + cwd: repoRoot, + env: { ...process.env }, + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("[-b|--background]"); + expect(result.stdout).toContain("Use --background to detach from the terminal"); + expect(result.stderr).toBe(""); + }); + + test("main help advertises background flag for start", () => { + const result = spawnSync(process.execPath, [cliPath, "--help"], { + cwd: repoRoot, + env: { ...process.env }, + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("ocx start [--port ] [-b|--background]"); + expect(result.stderr).toBe(""); + }); +});