Skip to content

Commit 618a42a

Browse files
authored
feat: introduce npm module for codex-responses-api-proxy (#4417)
This PR expands `.github/workflows/rust-release.yml` so that it also builds and publishes the `npm` module for `@openai/codex-responses-api-proxy` in addition to `@openai/codex`. Note both `npm` modules are similar, in that they each contain a single `.js` file that is a thin launcher around the appropriate native executable. (Since we have a minimal dependency on Node.js, I also lowered the minimum version from 20 to 16 and verified that works on my machine.) As part of this change, we tighten up some of the docs around `codex-responses-api-proxy` and ensure the details regarding protecting the `OPENAI_API_KEY` in memory match the implementation. To test the `npm` build process, I ran: ``` ./codex-cli/scripts/build_npm_package.py --package codex-responses-api-proxy --version 0.43.0-alpha.3 ``` which stages the `npm` module for `@openai/codex-responses-api-proxy` in a temp directory, using the binary artifacts from https://github.com/openai/codex/releases/tag/rust-v0.43.0-alpha.3.
1 parent a9d54b9 commit 618a42a

File tree

13 files changed

+389
-66
lines changed

13 files changed

+389
-66
lines changed

.github/workflows/rust-release.yml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,17 +218,30 @@ jobs:
218218
219219
# build_npm_package.py requires DotSlash when staging releases.
220220
- uses: facebook/install-dotslash@v2
221-
- name: Stage npm package
221+
- name: Stage codex CLI npm package
222222
env:
223223
GH_TOKEN: ${{ github.token }}
224224
run: |
225225
set -euo pipefail
226226
TMP_DIR="${RUNNER_TEMP}/npm-stage"
227227
./codex-cli/scripts/build_npm_package.py \
228+
--package codex \
228229
--release-version "${{ steps.release_name.outputs.name }}" \
229230
--staging-dir "${TMP_DIR}" \
230231
--pack-output "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.tgz"
231232
233+
- name: Stage responses API proxy npm package
234+
env:
235+
GH_TOKEN: ${{ github.token }}
236+
run: |
237+
set -euo pipefail
238+
TMP_DIR="${RUNNER_TEMP}/npm-stage-responses"
239+
./codex-cli/scripts/build_npm_package.py \
240+
--package codex-responses-api-proxy \
241+
--release-version "${{ steps.release_name.outputs.name }}" \
242+
--staging-dir "${TMP_DIR}" \
243+
--pack-output "${GITHUB_WORKSPACE}/dist/npm/codex-responses-api-proxy-npm-${{ steps.release_name.outputs.name }}.tgz"
244+
232245
- name: Create GitHub Release
233246
uses: softprops/action-gh-release@v2
234247
with:
@@ -271,7 +284,7 @@ jobs:
271284
- name: Update npm
272285
run: npm install -g npm@latest
273286

274-
- name: Download npm tarball from release
287+
- name: Download npm tarballs from release
275288
env:
276289
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
277290
run: |
@@ -283,6 +296,10 @@ jobs:
283296
--repo "${GITHUB_REPOSITORY}" \
284297
--pattern "codex-npm-${version}.tgz" \
285298
--dir dist/npm
299+
gh release download "$tag" \
300+
--repo "${GITHUB_REPOSITORY}" \
301+
--pattern "codex-responses-api-proxy-npm-${version}.tgz" \
302+
--dir dist/npm
286303
287304
# No NODE_AUTH_TOKEN needed because we use OIDC.
288305
- name: Publish to npm
@@ -296,7 +313,14 @@ jobs:
296313
tag_args+=(--tag "${NPM_TAG}")
297314
fi
298315
299-
npm publish "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${VERSION}.tgz" "${tag_args[@]}"
316+
tarballs=(
317+
"codex-npm-${VERSION}.tgz"
318+
"codex-responses-api-proxy-npm-${VERSION}.tgz"
319+
)
320+
321+
for tarball in "${tarballs[@]}"; do
322+
npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}"
323+
done
300324
301325
update-branch:
302326
name: Update latest-alpha-cli branch

codex-cli/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ The hardening mechanism Codex uses depends on your OS:
208208
| Requirement | Details |
209209
| --------------------------- | --------------------------------------------------------------- |
210210
| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** |
211-
| Node.js | **22 or newer** (LTS recommended) |
211+
| Node.js | **16 or newer** (Node 20 LTS recommended) |
212212
| Git (optional, recommended) | 2.23+ for built-in PR helpers |
213213
| RAM | 4-GB minimum (8-GB recommended) |
214214

@@ -513,7 +513,7 @@ Codex runs model-generated commands in a sandbox. If a proposed command or file
513513
<details>
514514
<summary>Does it work on Windows?</summary>
515515

516-
Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) - Codex has been tested on macOS and Linux with Node 22.
516+
Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) - Codex is regularly tested on macOS and Linux with Node 20+, and also supports Node 16.
517517

518518
</details>
519519

codex-cli/bin/codex.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22
// Unified entry point for the Codex CLI.
33

4+
import { spawn } from "node:child_process";
45
import { existsSync } from "fs";
56
import path from "path";
67
import { fileURLToPath } from "url";
@@ -68,7 +69,6 @@ const binaryPath = path.join(archRoot, "codex", codexBinaryName);
6869
// executing. This allows us to forward those signals to the child process
6970
// and guarantees that when either the child terminates or the parent
7071
// receives a fatal signal, both processes exit in a predictable manner.
71-
const { spawn } = await import("child_process");
7272

7373
function getUpdatedPath(newDirs) {
7474
const pathSep = process.platform === "win32" ? ";" : ":";

codex-cli/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"type": "module",
99
"engines": {
10-
"node": ">=20"
10+
"node": ">=16"
1111
},
1212
"files": [
1313
"bin",

codex-cli/scripts/build_npm_package.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
SCRIPT_DIR = Path(__file__).resolve().parent
1414
CODEX_CLI_ROOT = SCRIPT_DIR.parent
1515
REPO_ROOT = CODEX_CLI_ROOT.parent
16+
RESPONSES_API_PROXY_NPM_ROOT = REPO_ROOT / "codex-rs" / "responses-api-proxy" / "npm"
1617
GITHUB_REPO = "openai/codex"
1718

1819
# The docs are not clear on what the expected value/format of
@@ -23,6 +24,12 @@
2324

2425
def parse_args() -> argparse.Namespace:
2526
parser = argparse.ArgumentParser(description="Build or stage the Codex CLI npm package.")
27+
parser.add_argument(
28+
"--package",
29+
choices=("codex", "codex-responses-api-proxy"),
30+
default="codex",
31+
help="Which npm package to stage (default: codex).",
32+
)
2633
parser.add_argument(
2734
"--version",
2835
help="Version number to write to package.json inside the staged package.",
@@ -63,6 +70,7 @@ def parse_args() -> argparse.Namespace:
6370
def main() -> int:
6471
args = parse_args()
6572

73+
package = args.package
6674
version = args.version
6775
release_version = args.release_version
6876
if release_version:
@@ -76,7 +84,7 @@ def main() -> int:
7684
staging_dir, created_temp = prepare_staging_dir(args.staging_dir)
7785

7886
try:
79-
stage_sources(staging_dir, version)
87+
stage_sources(staging_dir, version, package)
8088

8189
workflow_url = args.workflow_url
8290
resolved_head_sha: str | None = None
@@ -100,16 +108,23 @@ def main() -> int:
100108
if not workflow_url:
101109
raise RuntimeError("Unable to determine workflow URL for native binaries.")
102110

103-
install_native_binaries(staging_dir, workflow_url)
111+
install_native_binaries(staging_dir, workflow_url, package)
104112

105113
if release_version:
106114
staging_dir_str = str(staging_dir)
107-
print(
108-
f"Staged version {version} for release in {staging_dir_str}\n\n"
109-
"Verify the CLI:\n"
110-
f" node {staging_dir_str}/bin/codex.js --version\n"
111-
f" node {staging_dir_str}/bin/codex.js --help\n\n"
112-
)
115+
if package == "codex":
116+
print(
117+
f"Staged version {version} for release in {staging_dir_str}\n\n"
118+
"Verify the CLI:\n"
119+
f" node {staging_dir_str}/bin/codex.js --version\n"
120+
f" node {staging_dir_str}/bin/codex.js --help\n\n"
121+
)
122+
else:
123+
print(
124+
f"Staged version {version} for release in {staging_dir_str}\n\n"
125+
"Verify the responses API proxy:\n"
126+
f" node {staging_dir_str}/bin/codex-responses-api-proxy.js --help\n\n"
127+
)
113128
else:
114129
print(f"Staged package in {staging_dir}")
115130

@@ -136,20 +151,34 @@ def prepare_staging_dir(staging_dir: Path | None) -> tuple[Path, bool]:
136151
return temp_dir, True
137152

138153

139-
def stage_sources(staging_dir: Path, version: str) -> None:
154+
def stage_sources(staging_dir: Path, version: str, package: str) -> None:
140155
bin_dir = staging_dir / "bin"
141156
bin_dir.mkdir(parents=True, exist_ok=True)
142157

143-
shutil.copy2(CODEX_CLI_ROOT / "bin" / "codex.js", bin_dir / "codex.js")
144-
rg_manifest = CODEX_CLI_ROOT / "bin" / "rg"
145-
if rg_manifest.exists():
146-
shutil.copy2(rg_manifest, bin_dir / "rg")
158+
if package == "codex":
159+
shutil.copy2(CODEX_CLI_ROOT / "bin" / "codex.js", bin_dir / "codex.js")
160+
rg_manifest = CODEX_CLI_ROOT / "bin" / "rg"
161+
if rg_manifest.exists():
162+
shutil.copy2(rg_manifest, bin_dir / "rg")
163+
164+
readme_src = REPO_ROOT / "README.md"
165+
if readme_src.exists():
166+
shutil.copy2(readme_src, staging_dir / "README.md")
147167

148-
readme_src = REPO_ROOT / "README.md"
149-
if readme_src.exists():
150-
shutil.copy2(readme_src, staging_dir / "README.md")
168+
package_json_path = CODEX_CLI_ROOT / "package.json"
169+
elif package == "codex-responses-api-proxy":
170+
launcher_src = RESPONSES_API_PROXY_NPM_ROOT / "bin" / "codex-responses-api-proxy.js"
171+
shutil.copy2(launcher_src, bin_dir / "codex-responses-api-proxy.js")
151172

152-
with open(CODEX_CLI_ROOT / "package.json", "r", encoding="utf-8") as fh:
173+
readme_src = RESPONSES_API_PROXY_NPM_ROOT / "README.md"
174+
if readme_src.exists():
175+
shutil.copy2(readme_src, staging_dir / "README.md")
176+
177+
package_json_path = RESPONSES_API_PROXY_NPM_ROOT / "package.json"
178+
else:
179+
raise RuntimeError(f"Unknown package '{package}'.")
180+
181+
with open(package_json_path, "r", encoding="utf-8") as fh:
153182
package_json = json.load(fh)
154183
package_json["version"] = version
155184

@@ -158,8 +187,20 @@ def stage_sources(staging_dir: Path, version: str) -> None:
158187
out.write("\n")
159188

160189

161-
def install_native_binaries(staging_dir: Path, workflow_url: str) -> None:
162-
cmd = ["./scripts/install_native_deps.py", "--workflow-url", workflow_url, str(staging_dir)]
190+
def install_native_binaries(staging_dir: Path, workflow_url: str, package: str) -> None:
191+
package_components = {
192+
"codex": ["codex", "rg"],
193+
"codex-responses-api-proxy": ["codex-responses-api-proxy"],
194+
}
195+
196+
components = package_components.get(package)
197+
if components is None:
198+
raise RuntimeError(f"Unknown package '{package}'.")
199+
200+
cmd = ["./scripts/install_native_deps.py", "--workflow-url", workflow_url]
201+
for component in components:
202+
cmd.extend(["--component", component])
203+
cmd.append(str(staging_dir))
163204
subprocess.check_call(cmd, cwd=CODEX_CLI_ROOT)
164205

165206

0 commit comments

Comments
 (0)