Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 원본 복원
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion docs-site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <port>]`
### `ocx start [--port <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`
Expand Down
36 changes: 32 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function printUsage() {

Usage:
ocx init Interactive setup (provider + Codex config injection)
ocx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
ocx start [--port <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
Expand Down Expand Up @@ -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 <port>]\n\nStart the proxy server and sync models to Codex.");
console.log("Usage: ocx start [--port <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.");
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -183,7 +187,7 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
return selected;
}

async function handleStart(options: { block?: boolean } = {}) {
async function handleStart(options: { block?: boolean; background?: boolean } = {}) {
const existingPid = readPid();
if (existingPid) {
const config = loadConfig();
Expand All @@ -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);

Expand Down Expand Up @@ -390,7 +418,7 @@ switch (command) {
break;
}
case "start":
await handleStart();
await handleStart({ background: parseBackgroundFlag() });
break;
case "stop":
handleStop();
Expand Down
34 changes: 34 additions & 0 deletions tests/cli-background.test.ts
Original file line number Diff line number Diff line change
@@ -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 <port>] [-b|--background]");
expect(result.stderr).toBe("");
});
});