16 KiB
PR #2237: [codex-cli] Add ripgrep as a dependency for node environment
- URL: https://github.com/openai/codex/pull/2237
- Author: dylan-hurd-oai
- Created: 2025-08-12 18:25:23 UTC
- Updated: 2025-08-13 20:49:42 UTC
- Changes: +196/-5, Files changed: 5, Commits: 5
Description
Summary
Ripgrep is our preferred tool for file search. When users install via brew install codex, it's automatically installed as a dependency. We want to ensure that users running via an npm install also have this tool! Microsoft has already solved this problem for VS Code - let's not reinvent the wheel.
This approach of appending to the PATH directly might be a bit heavy-handed, but feels reasonably robust to a variety of environment concerns. Open to thoughts on better approaches here!
Testing
- confirmed this import approach works with
node -e "const { rgPath } = require('@vscode/ripgrep'); require('child_process').spawn(rgPath, ['--version'], { stdio: 'inherit' })" - Ran codex.js locally with
rguninstalled, asked it to runwhich rg. Output below:
⚡ Ran command which rg; echo $?
⎿ /Users/dylan.hurd/code/dh--npm-rg/node_modules/@vscode/ripgrep/bin/rg
0
codex
Re-running to confirm the path and exit code.
- Path: `/Users/dylan.hurd/code/dh--npm-rg/node_modules/@vscode/ripgrep/bin/rg`
- Exit code: `0`
Full Diff
diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js
index d92d8f2f4f..b22c5180c0 100755
--- a/codex-cli/bin/codex.js
+++ b/codex-cli/bin/codex.js
@@ -43,7 +43,7 @@ switch (platform) {
targetTriple = "x86_64-pc-windows-msvc.exe";
break;
case "arm64":
- // We do not build this today, fall through...
+ // We do not build this today, fall through...
default:
break;
}
@@ -65,9 +65,43 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch (err) {
+ return null;
+ }
+}
+
+async function resolveRgDir() {
+ const ripgrep = await tryImport("@vscode/ripgrep");
+ if (!ripgrep?.rgPath) {
+ return null;
+ }
+ return path.dirname(ripgrep.rgPath);
+}
+
+function getUpdatedPath(newDirs) {
+ const pathSep = process.platform === "win32" ? ";" : ":";
+ const existingPath = process.env.PATH || "";
+ const updatedPath = [
+ ...newDirs,
+ ...existingPath.split(pathSep).filter(Boolean),
+ ].join(pathSep);
+ return updatedPath;
+}
+
+const additionalDirs = [];
+const rgDir = await resolveRgDir();
+if (rgDir) {
+ additionalDirs.push(rgDir);
+}
+const updatedPath = getUpdatedPath(additionalDirs);
+
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: "inherit",
- env: { ...process.env, CODEX_MANAGED_BY_NPM: "1" },
+ env: { ...process.env, PATH: updatedPath, CODEX_MANAGED_BY_NPM: "1" },
});
child.on("error", (err) => {
@@ -120,4 +154,3 @@ if (childResult.type === "signal") {
} else {
process.exit(childResult.exitCode);
}
-
diff --git a/codex-cli/package-lock.json b/codex-cli/package-lock.json
new file mode 100644
index 0000000000..a1c840ade0
--- /dev/null
+++ b/codex-cli/package-lock.json
@@ -0,0 +1,119 @@
+{
+ "name": "@openai/codex",
+ "version": "0.0.0-dev",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@openai/codex",
+ "version": "0.0.0-dev",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@vscode/ripgrep": "^1.15.14"
+ },
+ "bin": {
+ "codex": "bin/codex.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@vscode/ripgrep": {
+ "version": "1.15.14",
+ "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.14.tgz",
+ "integrity": "sha512-/G1UJPYlm+trBWQ6cMO3sv6b8D1+G16WaJH1/DSqw32JOVlzgZbLkDxRyzIpTpv30AcYGMkCf5tUqGlW6HbDWw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "https-proxy-agent": "^7.0.2",
+ "proxy-from-env": "^1.1.0",
+ "yauzl": "^2.9.2"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ }
+ }
+}
diff --git a/codex-cli/package.json b/codex-cli/package.json
index c5464beae5..614ca1a832 100644
--- a/codex-cli/package.json
+++ b/codex-cli/package.json
@@ -16,5 +16,11 @@
"repository": {
"type": "git",
"url": "git+https://github.com/openai/codex.git"
+ },
+ "dependencies": {
+ "@vscode/ripgrep": "^1.15.14"
+ },
+ "devDependencies": {
+ "prettier": "^3.3.3"
}
}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000000..bb67c75258
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,33 @@
+{
+ "name": "codex-monorepo",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "codex-monorepo",
+ "devDependencies": {
+ "prettier": "^3.5.3"
+ },
+ "engines": {
+ "node": ">=22",
+ "pnpm": ">=9.0.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index c13e6cb314..606c323ff4 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,8 @@
"private": true,
"description": "Tools for repo-wide maintenance.",
"scripts": {
- "format": "prettier --check *.json *.md .github/workflows/*.yml",
- "format:fix": "prettier --write *.json *.md .github/workflows/*.yml"
+ "format": "prettier --check *.json *.md .github/workflows/*.yml **/*.js",
+ "format:fix": "prettier --write *.json *.md .github/workflows/*.yml **/*.js"
},
"devDependencies": {
"prettier": "^3.5.3"
Review Comments
codex-cli/bin/codex.js
- Created: 2025-08-12 20:59:40 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2271220497
@@ -65,9 +65,40 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch {
+ return null;
+ }
+}
+
+async function resolveRgPath() {
In deleting most of
codex-cli, I guess we lost Prettier coverage for this file...
- Created: 2025-08-12 21:03:35 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2271231764
@@ -65,9 +65,40 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch {
+ return null;
+ }
+}
+
+async function resolveRgPath() {
+ try {
+ const { rgPath } = await tryImport("@vscode/ripgrep");
+ return path.dirname(rgPath);
+ } catch(err) {
+ console.error('unable to import ripgrep', err);
+ }
+}
+
+function getUpdatedPath(newDirs) {
+ const pathSep = process.platform === "win32" ? ";" : ":";
+ const existingPath = process.env.PATH || process.env.Path || "";
I thought env vars are case-insensitive and that
process.envdoes some internalgetvoodoo to enforce that, so I don't believe you needprocess.env.Path, do you?
- Created: 2025-08-12 21:04:15 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2271233374
@@ -65,9 +65,40 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch {
+ return null;
+ }
+}
+
+async function resolveRgPath() {
+ try {
+ const { rgPath } = await tryImport("@vscode/ripgrep");
+ return path.dirname(rgPath);
To confirm:
rgis definitely the only file in that directory?
- Created: 2025-08-12 21:05:54 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2271237698
@@ -65,9 +65,40 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch {
+ return null;
+ }
+}
Can you verify this works on Node 20? I recently lowered the minimum version:
- Created: 2025-08-12 21:10:53 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2271253807
@@ -65,9 +65,40 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch {
+ return null;
+ }
+}
+
+async function resolveRgPath() {
+ try {
+ const { rgPath } = await tryImport("@vscode/ripgrep");
You already have try/catch in
tryImport(), so I don't think we should do it again here. Just check whether the result isnullor not.I worry we might swallow logical errors by accident.
- Created: 2025-08-12 22:43:33 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2271475227
@@ -65,9 +65,40 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch {
+ return null;
+ }
+}
+
+async function resolveRgPath() {
+ try {
+ const { rgPath } = await tryImport("@vscode/ripgrep");
+ return path.dirname(rgPath);
+ } catch(err) {
+ console.error('unable to import ripgrep', err);
+ }
+}
+
+function getUpdatedPath(newDirs) {
+ const pathSep = process.platform === "win32" ? ";" : ":";
+ const existingPath = process.env.PATH || process.env.Path || "";
I just tested on my Windows machine and
process.env.Pathandprocess.env.PATHare the same value.
- Created: 2025-08-13 15:26:30 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2273823865
@@ -65,9 +65,40 @@ const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
// receives a fatal signal, both processes exit in a predictable manner.
const { spawn } = await import("child_process");
+async function tryImport(moduleName) {
+ try {
+ // eslint-disable-next-line node/no-unsupported-features/es-syntax
+ return await import(moduleName);
+ } catch (err) {
+ console.error(`unable to import ${moduleName}`, err);
+ return null;
+ }
+}
+
+async function resolveRgPath() {
+ const { rgPath } = await tryImport("@vscode/ripgrep");
+ if (!rgPath) {
+ throw new Error("ripgrep not found");
+ }
+ return path.dirname(rgPath);
+}
+
+function getUpdatedPath(newDirs) {
+ const pathSep = process.platform === "win32" ? ";" : ":";
+ const existingPath = process.env.PATH || "";
+ const updatedPath = [
+ ...newDirs,
+ ...existingPath.split(pathSep).filter(Boolean),
+ ].join(pathSep);
+ return updatedPath;
+}
+
+const rgPath = await resolveRgPath();
+const updatedPath = getUpdatedPath([rgPath]);
Should we throw in this case? I feel like, if the user has installed with
--ignore-scriptsor the network request failed to fetchrgfor some reason, we should still let them run the CLI?
package.json
- Created: 2025-08-12 21:02:41 UTC | Link: https://github.com/openai/codex/pull/2237#discussion_r2271228887
@@ -21,5 +21,8 @@
"node": ">=22",
"pnpm": ">=9.0.0"
},
- "packageManager": "pnpm@10.8.1"
+ "packageManager": "pnpm@10.8.1",
+ "dependencies": {
+ "@vscode/ripgrep": "^1.15.14"
Note that this does not guarantee that
rggets installed because the npm package does not bundle the binaries itself, but downloads them viapostinstall:https://www.npmjs.com/package/@vscode/ripgrep
Because we expect our users to install via
npm -gand have network access at that time, this should be fine, but if they used--ignore-scriptsfor some reason, then it wouldn't end up getting fetched.