diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2c3c2002583..a15d56d4042 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -110,20 +110,129 @@ jobs:
if-no-files-found: error
retention-days: 7
+ package-standalone:
+ needs: validate-dispatch
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ target: bun-linux-x64
+ smoke: true
+ - os: macos-latest
+ target: bun-darwin-arm64
+ smoke: true
+ - os: macos-latest
+ target: bun-darwin-x64
+ smoke: false
+ - os: windows-latest
+ target: bun-windows-x64
+ smoke: true
+ - os: ubuntu-latest
+ target: bun-linux-arm64
+ smoke: false
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 25
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+
+ - name: Setup project Bun
+ uses: ./.github/actions/setup-project-bun
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Build dashboard
+ run: bun run build:gui
+
+ - name: Build standalone binary
+ run: bun run build:standalone --target ${{ matrix.target }}
+
+ - name: Smoke test standalone binary
+ if: matrix.smoke && runner.os != 'Windows'
+ shell: bash
+ run: |
+ set -euo pipefail
+ binary="dist/standalone/${{ matrix.target }}/ocx"
+ "$binary" --version
+ OPENCODEX_HOME="$RUNNER_TEMP/ocx-home" "$binary" start --port 10177 >"$RUNNER_TEMP/ocx.log" 2>&1 &
+ pid=$!
+ trap 'kill "$pid" 2>/dev/null || true' EXIT
+ for _ in $(seq 1 30); do curl -fsS http://127.0.0.1:10177/healthz && break || sleep 1; done
+ curl -fsS http://127.0.0.1:10177/healthz
+ test "$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:10177/)" = 200
+
+ - name: Smoke test standalone binary (Windows)
+ if: matrix.smoke && runner.os == 'Windows'
+ shell: pwsh
+ run: |
+ $binary = "dist/standalone/${{ matrix.target }}/ocx.exe"
+ & $binary --version
+ $env:OPENCODEX_HOME = Join-Path $env:RUNNER_TEMP "ocx-home"
+ $process = Start-Process -FilePath $binary -ArgumentList "start", "--port", "10177" -PassThru
+ try {
+ for ($i = 0; $i -lt 30; $i++) {
+ try { Invoke-WebRequest -UseBasicParsing http://127.0.0.1:10177/healthz | Out-Null; break } catch { Start-Sleep -Seconds 1 }
+ }
+ Invoke-WebRequest -UseBasicParsing http://127.0.0.1:10177/healthz | Select-Object -ExpandProperty Content
+ Invoke-WebRequest -UseBasicParsing http://127.0.0.1:10177/ | Out-Null
+ } finally { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue }
+
+ - name: Archive standalone release
+ shell: bash
+ env:
+ RELEASE_VERSION: ${{ inputs.version }}
+ STANDALONE_TARGET: ${{ matrix.target }}
+ run: |
+ set -euo pipefail
+ cd "dist/standalone/$STANDALONE_TARGET"
+ if [[ "$RUNNER_OS" == "Windows" ]]; then
+ powershell -NoProfile -Command 'Compress-Archive -Path ocx.exe,gui -DestinationPath ("../../ocx-{0}-{1}.zip" -f $env:RELEASE_VERSION,$env:STANDALONE_TARGET) -Force'
+ else
+ tar -czf "../../ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" ocx gui
+ fi
+ cd ../../..
+ if [[ "$RUNNER_OS" == "Windows" ]]; then sha256sum "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.zip" > "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256"
+ else sha256sum "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" > "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256"
+ fi
+
+ - name: Upload standalone release
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: standalone-${{ matrix.target }}
+ path: |
+ dist/ocx-*.tar.gz
+ dist/ocx-*.zip
+ dist/ocx-*.sha256
+ if-no-files-found: error
+ retention-days: 7
+
attach-macos:
runs-on: ubuntu-latest
- needs: [publish, package-macos]
+ needs: [publish, package-macos, package-standalone]
if: ${{ inputs.dry-run != true }}
timeout-minutes: 10
permissions:
contents: write
steps:
- - name: Download the packaged asset
+ - name: Download the macOS packaged asset
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: macos-release
path: dist/release
+ - name: Download standalone packaged assets
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: standalone-*
+ merge-multiple: true
+ path: dist/release
+
- name: Verify the checksum before uploading
run: |
cd dist/release
diff --git a/docs-site/src/content/docs/getting-started/installation.md b/docs-site/src/content/docs/getting-started/installation.md
index c5a7d36578e..9ff7e5a72c6 100644
--- a/docs-site/src/content/docs/getting-started/installation.md
+++ b/docs-site/src/content/docs/getting-started/installation.md
@@ -49,6 +49,19 @@ ocx --version
opencodex --version
```
+## Standalone binary (no npm)
+
+Release downloads also include a standalone `ocx` binary for supported macOS, Linux, and Windows
+targets. It includes the Bun runtime and dashboard, so npm, Node, and a separate Bun installation
+are not required. Download the archive for your platform, extract it, and run:
+
+```bash
+./ocx --version
+./ocx start
+```
+
+The extracted `gui/dist` directory must stay beside the binary so `GET /` can serve the dashboard.
+
### Release channels
The stable `latest` channel already includes GPT-5.6 Sol/Terra/Luna catalog support for ChatGPT,
diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md
index 042eb183045..7734c9f2761 100644
--- a/docs-site/src/content/docs/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/getting-started/quickstart.md
@@ -5,6 +5,11 @@ description: Configure your first provider and route OpenAI Codex through openco
This guide takes you from a fresh install to running Codex against a non-OpenAI model.
+## Standalone binary (no npm)
+
+You can also use a release archive containing the `ocx` binary and Bun runtime without npm.
+Extract it with its `gui/dist` directory beside the binary, then run `./ocx start`.
+
## 1. Run the setup wizard
```bash
diff --git a/docs-site/src/content/docs/ja/getting-started/installation.md b/docs-site/src/content/docs/ja/getting-started/installation.md
index a62daede0a6..49b50f7d37d 100644
--- a/docs-site/src/content/docs/ja/getting-started/installation.md
+++ b/docs-site/src/content/docs/ja/getting-started/installation.md
@@ -43,6 +43,19 @@ ocx --version
opencodex --version
```
+## スタンドアロンバイナリ(npm 不要)
+
+リリースには、対応する macOS、Linux、Windows 向けのスタンドアロン `ocx` バイナリも含まれます。
+Bun ランタイムとダッシュボードが含まれるため、npm、Node、別途の Bun インストールは必要ありません。
+お使いの環境向けのアーカイブをダウンロードして展開し、次のように実行します。
+
+```bash
+./ocx --version
+./ocx start
+```
+
+ダッシュボードを提供するため、展開した `gui/dist` ディレクトリはバイナリの隣に置いたままにしてください。
+
### 配布チャネル
安定チャネルの `latest` にも ChatGPT、OpenAI API キー、OpenRouter、実験段階の Cursor 経路のための
diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md
index f9184dfc5f6..9c2d25a93e4 100644
--- a/docs-site/src/content/docs/ja/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md
@@ -5,6 +5,11 @@ description: 最初のプロバイダーを構成し、3 つのコマンドで O
このガイドでは、新規インストールから非 OpenAI モデルに対して Codex を実行するまでを説明します。
+## スタンドアロンバイナリ(npm 不要)
+
+npm を使わず、Bun ランタイムを含むリリースアーカイブの `ocx` バイナリも利用できます。
+`gui/dist` ディレクトリをバイナリの隣に置いて展開し、`./ocx start` を実行してください。
+
## 1. セットアップウィザードを実行します
```bash
diff --git a/docs-site/src/content/docs/ko/getting-started/installation.md b/docs-site/src/content/docs/ko/getting-started/installation.md
index 70a62f86099..146f25bb01c 100644
--- a/docs-site/src/content/docs/ko/getting-started/installation.md
+++ b/docs-site/src/content/docs/ko/getting-started/installation.md
@@ -43,6 +43,19 @@ ocx --version
opencodex --version
```
+## 독립 실행형 바이너리(npm 없음)
+
+릴리스에는 지원되는 macOS, Linux, Windows용 독립 실행형 `ocx` 바이너리도 포함됩니다.
+Bun 런타임과 대시보드가 포함되어 있으므로 npm, Node 또는 별도의 Bun 설치가 필요하지 않습니다.
+플랫폼에 맞는 아카이브를 다운로드해 압축을 풀고 다음과 같이 실행하세요.
+
+```bash
+./ocx --version
+./ocx start
+```
+
+대시보드를 제공하려면 압축을 푼 `gui/dist` 디렉터리를 바이너리 옆에 그대로 두어야 합니다.
+
### 배포 채널
안정화 채널인 `latest`에도 ChatGPT, OpenAI API 키, OpenRouter, 실험 단계의 Cursor 경로를 위한
diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md
index f1a179649b7..7e92511d56a 100644
--- a/docs-site/src/content/docs/ko/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md
@@ -5,6 +5,11 @@ description: 첫 프로바이더를 설정하고 명령어 세 개로 OpenAI Cod
이 가이드는 새로 설치한 상태에서 OpenAI가 아닌 모델로 Codex를 실행하기까지의 과정을 안내합니다.
+## 독립 실행형 바이너리(npm 없음)
+
+npm 없이 Bun 런타임이 포함된 릴리스 아카이브의 `ocx` 바이너리를 사용할 수도 있습니다.
+`gui/dist` 디렉터리를 바이너리 옆에 둔 채 압축을 풀고 `./ocx start`를 실행하세요.
+
## 1. 설정 마법사 실행
```bash
diff --git a/docs-site/src/content/docs/ru/getting-started/installation.md b/docs-site/src/content/docs/ru/getting-started/installation.md
index a1f3724a4ba..ca0fe20b3e4 100644
--- a/docs-site/src/content/docs/ru/getting-started/installation.md
+++ b/docs-site/src/content/docs/ru/getting-started/installation.md
@@ -45,6 +45,19 @@ ocx --version
opencodex --version
```
+## Автономный бинарный файл (без npm)
+
+В релиз входят автономные бинарные файлы `ocx` для поддерживаемых macOS, Linux и Windows.
+Они содержат рантайм Bun и дашборд, поэтому npm, Node и отдельная установка Bun не нужны.
+Скачайте архив для своей платформы, распакуйте его и выполните:
+
+```bash
+./ocx --version
+./ocx start
+```
+
+Чтобы дашборд был доступен, оставьте распакованный каталог `gui/dist` рядом с бинарным файлом.
+
### Каналы релизов
Стабильный канал `latest` уже включает поддержку каталога GPT-5.6 Sol/Terra/Luna для маршрутов
diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md
index 1b086c7db2c..088843f0f96 100644
--- a/docs-site/src/content/docs/ru/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md
@@ -5,6 +5,11 @@ description: Настройте первого провайдера и напр
Это руководство проводит от чистой установки до запуска Codex с моделью не от OpenAI.
+## Автономный бинарный файл (без npm)
+
+Можно также использовать архив с бинарным файлом `ocx` и рантаймом Bun без npm.
+Распакуйте его, оставив каталог `gui/dist` рядом с бинарным файлом, и выполните `./ocx start`.
+
## 1. Запустите мастер настройки
```bash
diff --git a/docs-site/src/content/docs/zh-cn/getting-started/installation.md b/docs-site/src/content/docs/zh-cn/getting-started/installation.md
index eb5b02ec948..335debd845d 100644
--- a/docs-site/src/content/docs/zh-cn/getting-started/installation.md
+++ b/docs-site/src/content/docs/zh-cn/getting-started/installation.md
@@ -42,6 +42,18 @@ ocx --version
opencodex --version
```
+## 独立二进制文件(无需 npm)
+
+发布包还包含适用于 macOS、Linux 和 Windows 的独立 `ocx` 二进制文件。
+它内置 Bun 运行时和仪表盘,因此无需安装 npm、Node 或单独的 Bun。下载适合你平台的压缩包,解压后运行:
+
+```bash
+./ocx --version
+./ocx start
+```
+
+为了让仪表盘可用,请将解压后的 `gui/dist` 目录保留在二进制文件旁边。
+
### 发布渠道
稳定的 `latest` 渠道已经包含 ChatGPT、OpenAI API key、OpenRouter 以及实验性 Cursor 路由所需的
diff --git a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
index e965b159a6b..91ac4ba047b 100644
--- a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
@@ -5,6 +5,11 @@ description: 配置你的第一个 provider,并在三条命令内让 OpenAI Co
本指南将带你从全新安装,一路走到用一个非 OpenAI 模型运行 Codex。
+## 独立二进制文件(无需 npm)
+
+你也可以使用包含 Bun 运行时的发布压缩包中的 `ocx`,无需 npm。
+解压时将 `gui/dist` 目录保留在二进制文件旁边,然后运行 `./ocx start`。
+
## 1. 运行设置向导
```bash
diff --git a/package.json b/package.json
index 26fce207d75..4b6912dfa6f 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"build:remote-workspace-helper": "cargo build --release --locked --manifest-path native/remote-workspace-helper/Cargo.toml",
"test:remote-workspace-helper": "cargo test --locked --manifest-path native/remote-workspace-helper/Cargo.toml",
"build:macos": "bash scripts/build-macos-app.sh",
+ "build:standalone": "bun scripts/build-standalone.ts",
"package:macos": "bash scripts/package-macos-release.sh",
"test:macos": "swift run --package-path app MenuBarCoreTests && swift run --package-path app MenuBarUITests",
"prepare:package": "bun scripts/prepare-package.ts",
diff --git a/scripts/build-standalone.ts b/scripts/build-standalone.ts
new file mode 100644
index 00000000000..7b71df0bc43
--- /dev/null
+++ b/scripts/build-standalone.ts
@@ -0,0 +1,53 @@
+import { createHash } from "node:crypto";
+import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { join, resolve } from "node:path";
+
+const targets = new Set([
+ "bun-darwin-arm64",
+ "bun-darwin-x64",
+ "bun-windows-x64",
+ "bun-linux-x64",
+ "bun-linux-arm64",
+]);
+
+function hostTarget(): string {
+ const platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux";
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
+ return `bun-${platform}-${arch}`;
+}
+
+function argumentValue(name: string): string | undefined {
+ const index = Bun.argv.indexOf(name);
+ return index >= 0 ? Bun.argv[index + 1] : undefined;
+}
+
+const target = argumentValue("--target") ?? hostTarget();
+if (!targets.has(target)) {
+ throw new Error(`Unsupported standalone target: ${target}`);
+}
+
+const repoRoot = resolve(import.meta.dir, "..");
+const guiDist = join(repoRoot, "gui", "dist");
+if (!existsSync(join(guiDist, "index.html"))) {
+ throw new Error("gui/dist is missing; run `bun run build:gui` first");
+}
+
+const output = resolve(argumentValue("--out") ?? join(repoRoot, "dist", "standalone", target));
+mkdirSync(output, { recursive: true });
+const executable = join(output, target.startsWith("bun-windows-") ? "ocx.exe" : "ocx");
+const result = Bun.spawnSync([
+ process.execPath,
+ "build",
+ "--compile",
+ "--target",
+ target,
+ join(repoRoot, "src", "cli", "index.ts"),
+ "--outfile",
+ executable,
+], { stdout: "inherit", stderr: "inherit" });
+if (result.exitCode !== 0) process.exit(result.exitCode);
+
+cpSync(guiDist, join(output, "gui", "dist"), { recursive: true });
+const digest = createHash("sha256").update(readFileSync(executable)).digest("hex");
+writeFileSync(join(output, "SHA256SUMS"), `${digest} ${executable.split(/[\\/]/).pop()}\n`);
+console.log(`Built ${executable}`);
diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json
index f1e6f40d998..3a27b56cc51 100644
--- a/scripts/test-layout/layout.json
+++ b/scripts/test-layout/layout.json
@@ -168,6 +168,9 @@
},
"explicit": {
"macos-build-script.test.ts": "gui",
+ "standalone-build-script.test.ts": "gui",
+ "standalone-service.test.ts": "service",
+ "standalone.test.ts": "lib",
"server-combo-held-response.test.ts": "server",
"key-attribution.test.ts": "usage",
"provider-send-path-import.test.ts": "server",
diff --git a/src/cli/help.ts b/src/cli/help.ts
index da5450880b4..38a5c18da19 100644
--- a/src/cli/help.ts
+++ b/src/cli/help.ts
@@ -1,9 +1,5 @@
-import { readFileSync } from "node:fs";
-import { dirname, join } from "node:path";
-import { fileURLToPath } from "node:url";
import { findCommand } from "./registry";
-
-const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
+import { packageVersion as readPackageVersion } from "../lib/package-version";
/**
* Version of the `ocx` bundle this process is running from.
@@ -13,9 +9,7 @@ const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta
* rather than throwing; callers must treat that as "cannot compare", not as a mismatch.
*/
export function packageVersion(): string {
- const raw = readFileSync(join(repoRoot, "package.json"), "utf8");
- const parsed = JSON.parse(raw) as { version?: unknown };
- return typeof parsed.version === "string" ? parsed.version : "unknown";
+ return readPackageVersion();
}
export function printVersion(): void {
diff --git a/src/client/machine-listener.ts b/src/client/machine-listener.ts
index f0dd56b7656..ef8c4f43fae 100644
--- a/src/client/machine-listener.ts
+++ b/src/client/machine-listener.ts
@@ -1,4 +1,3 @@
-import { readFileSync } from "node:fs";
import type { Server } from "bun";
import { loadConfig } from "../config";
import { browserSecurityHeaders } from "../server/auth-cors";
@@ -16,11 +15,9 @@ import { readClientConnectionState } from "./state";
import { handleMachineApi, type HubReachability, type MachineApiDeps } from "./machine-api";
import { MACHINE_GUI_ORIGIN_HEADER, requireMachineAuth } from "./machine-auth";
import { relayHubManagementRequest } from "./hub-relay";
+import { packageVersion } from "../lib/package-version";
-const VERSION = (() => {
- try { return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string; }
- catch { return "0.0.0"; }
-})();
+const VERSION = packageVersion("0.0.0");
const GUI_SPA_PATHS = new Set([
"/dashboard", "/startup", "/providers", "/models", "/subagents",
"/logs", "/usage", "/storage", "/codex-set", "/integrations",
diff --git a/src/lib/bun-runtime.ts b/src/lib/bun-runtime.ts
index e5c7ed31d34..ffe5e0b53f2 100644
--- a/src/lib/bun-runtime.ts
+++ b/src/lib/bun-runtime.ts
@@ -14,6 +14,7 @@ import { createRequire } from "node:module";
import { realpathSync } from "node:fs";
import { dirname, join } from "node:path";
import { isRealBunBinary } from "./bun-binary-validator.mjs";
+import { isStandaloneBinary } from "./standalone";
export { isRealBunBinary };
@@ -38,10 +39,10 @@ export const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";
*/
export const BUN_RUNTIME_PATH_ENV = "OCX_BUN_RUNTIME_PATH";
-export type BunRuntimeSource = "override" | "bundled" | "process";
+export type BunRuntimeSource = "override" | "bundled" | "process" | "standalone";
/** The only provenance values any surface may accept off the wire or out of the env. */
-export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process"];
+export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process", "standalone"];
export type DurableBunRuntime = {
path: string;
@@ -165,6 +166,9 @@ function unmarkedDurableBunRuntime(): DurableBunRuntime {
}
export function durableBunRuntime(): DurableBunRuntime {
+ if (isStandaloneBinary()) {
+ return { path: process.execPath, source: "standalone", overrideEnv: BUN_OVERRIDE_ENV };
+ }
// A durable artifact must use the runtime selected BEFORE Bun auto-loaded a
// project dotenv. The Node launcher and owned service/shim launchers stamp the
// selected source/path pair; it is accepted only when it names this exact
diff --git a/src/lib/package-tree-integrity.ts b/src/lib/package-tree-integrity.ts
index 9b7d1e8429f..30372de183e 100644
--- a/src/lib/package-tree-integrity.ts
+++ b/src/lib/package-tree-integrity.ts
@@ -1,4 +1,5 @@
import { statSync } from "node:fs";
+import { isStandaloneBinary } from "./standalone";
export interface PackageTreeObservation {
readonly device: bigint;
@@ -96,6 +97,6 @@ export function createRuntimePackageTreeIntegrityGuard(
observe: ObservePackageTree = observePackageManifest,
now: () => number = Date.now,
): PackageTreeIntegrityGuard {
- if (installer === "source") return { status: () => ({ ok: true }) };
+ if (installer === "source" || isStandaloneBinary()) return { status: () => ({ ok: true }) };
return createPackageTreeIntegrityGuard(observe, now);
}
diff --git a/src/lib/package-version.ts b/src/lib/package-version.ts
new file mode 100644
index 00000000000..100f7e9bc0d
--- /dev/null
+++ b/src/lib/package-version.ts
@@ -0,0 +1,8 @@
+import pkg from "../../package.json" with { type: "json" };
+
+type PackageManifest = { version?: unknown };
+
+export function packageVersion(fallback = "unknown"): string {
+ const version = (pkg as PackageManifest).version;
+ return typeof version === "string" ? version : fallback;
+}
diff --git a/src/lib/standalone.ts b/src/lib/standalone.ts
new file mode 100644
index 00000000000..72cc9f36183
--- /dev/null
+++ b/src/lib/standalone.ts
@@ -0,0 +1,16 @@
+import { realpathSync } from "node:fs";
+import { dirname } from "node:path";
+
+/** Compiled Bun binaries expose their bundled module tree through the `$bunfs` marker. */
+export function isStandaloneBinary(): boolean {
+ return isStandaloneModuleUrl(import.meta.url);
+}
+
+export function isStandaloneModuleUrl(url: string): boolean {
+ return url.includes("/$bunfs/") || /^file:\/\/\/[A-Za-z]:\/~BUN\//.test(url);
+}
+
+/** Directory containing the compiled executable and its copied runtime assets. */
+export function standaloneRoot(): string {
+ return dirname(realpathSync(process.execPath));
+}
diff --git a/src/lib/winsw.ts b/src/lib/winsw.ts
index f1dc60ffbae..321e3ea8132 100644
--- a/src/lib/winsw.ts
+++ b/src/lib/winsw.ts
@@ -72,7 +72,7 @@ export interface WinswEntry {
bun: string;
/** Provenance of `bun`, resolved together with it so the two can never disagree. */
bunRuntimeSource: BunRuntimeSource;
- cli: string;
+ cli: string | null;
}
/**
@@ -118,7 +118,7 @@ export function buildWinswXml(entry: WinswEntry, env: NodeJS.ProcessEnv = proces
OpenCodex Proxy (native)
OpenCodex proxy running as a native Windows service (windowless, starts at boot).
${xmlEscape(entry.bun)}
- ${xmlEscape(`"${entry.cli}" start --port ${safeListenPort}`)}
+ ${xmlEscape(`${entry.cli ? `"${entry.cli}" ` : ""}start --port ${safeListenPort}`)}
${envLines.join("\n")}
${xmlEscape(winswLogDir())}
diff --git a/src/server/gui-static.ts b/src/server/gui-static.ts
index d128361c02b..9a6439f0971 100644
--- a/src/server/gui-static.ts
+++ b/src/server/gui-static.ts
@@ -2,15 +2,11 @@ import { existsSync, readFileSync, statSync } from "node:fs";
import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
import { browserSecurityHeaders } from "./auth-cors";
import type { GuiSessionBootstrap } from "./gui-session";
+import { packageVersion } from "../lib/package-version";
+import { isStandaloneBinary, standaloneRoot } from "../lib/standalone";
/** opencodex version, read from the packaged package.json (same source as the server bootstrap). */
-const VERSION = (() => {
- try {
- return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string;
- } catch {
- return "0.0.0";
- }
-})();
+const VERSION = packageVersion("0.0.0");
const MIME_TYPES: Record = {
".html": "text/html", ".js": "application/javascript", ".css": "text/css",
@@ -26,9 +22,11 @@ const HASHED_ASSET_PATTERN = /-[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9]+$/;
function findGuiDist(): string | null {
const candidates = [
+ process.env.OPENCODEX_GUI_DIST,
+ ...(isStandaloneBinary() ? [join(standaloneRoot(), "gui", "dist")] : []),
join(import.meta.dir, "..", "..", "gui", "dist"),
join(import.meta.dir, "..", "..", "..", "gui", "dist"),
- ];
+ ].filter((candidate): candidate is string => Boolean(candidate));
for (const c of candidates) {
if (existsSync(join(c, "index.html"))) return c;
}
diff --git a/src/server/management-api.ts b/src/server/management-api.ts
index dbd5d7345d6..63733717d9b 100644
--- a/src/server/management-api.ts
+++ b/src/server/management-api.ts
@@ -1,6 +1,5 @@
import { remoteWorkspaceEnabled } from "../remote-control/workspace-activation";
import { randomUUID } from "node:crypto";
-import { readFileSync } from "node:fs";
import type { CatalogModel } from "../codex/catalog";
import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../codex/catalog";
import {
@@ -86,15 +85,11 @@ import type { CatalogDisposition, ConvergeCodex } from "../codex/convergence-typ
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
import { managementBodyTooLargeResponse } from "./management/body";
import { handleSessionRoutes } from "./management/session-routes";
+import { packageVersion } from "../lib/package-version";
// installed npm version instead of a stale hardcode.
-export const VERSION = (() => {
- try {
- return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string;
- } catch {
- return "0.0.0";
- }
-})();
+const MANAGEMENT_VERSION_FALLBACK = "0.0.0";
+export const VERSION = packageVersion(MANAGEMENT_VERSION_FALLBACK);
const managementConvergenceBindings = new WeakMap