Skip to content

Fix Codex Desktop CLI discovery#24

Merged
fhajjej-ship-it merged 1 commit into
mainfrom
codex/codex-cli-resolution
Jul 11, 2026
Merged

Fix Codex Desktop CLI discovery#24
fhajjej-ship-it merged 1 commit into
mainfrom
codex/codex-cli-resolution

Conversation

@fhajjej-ship-it

@fhajjej-ship-it fhajjej-ship-it commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • discover the Codex executable bundled with Codex Desktop when codex is absent from a normal terminal PATH
  • use the same resolution behavior in loop-it run and loop-it doctor, while preserving explicit CLI and environment overrides
  • add regression coverage, documentation, and aligned 0.3.20 package/plugin metadata

Verification

  • npm run check — passed
  • npm run smoke:public-install — passed against the current public baseline
  • npm publish --dry-run --access public — passed, including the prepublish check
  • sanitized-PATH lookup resolved /Applications/ChatGPT.app/Contents/Resources/codex

Notes

  • Root cause: Codex Desktop bundles the CLI outside the PATH inherited by a normal terminal, while Loop It 0.3.19 invoked bare codex.
  • The public @latest package remains 0.3.19 until the v0.3.20 GitHub release publishes this change.

Summary by CodeRabbit

  • New Features

    • Added automatic discovery of the Codex CLI bundled with Codex Desktop on macOS when codex is not on PATH.
    • Added --codex-bin and LOOP_IT_CODEX_BIN options for explicit executable overrides.
    • Updated doctor reporting to show Codex availability and discovery sources.
  • Documentation

    • Expanded usage, installation, examples, and changelog guidance for Codex discovery and overrides.
  • Tests

    • Added coverage for Codex Desktop discovery during execution and diagnostics.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared Codex CLI discovery with explicit, environment, PATH, and macOS Codex Desktop fallbacks. Run execution and doctor diagnostics use the resolver, with smoke tests, documentation, help text, and release metadata updated for version 0.3.20.

Changes

Codex executable discovery

Layer / File(s) Summary
Codex CLI resolution
skills/loop-it/scripts/lib/codex-cli.mjs
Adds ordered Codex executable resolution for explicit overrides, LOOP_IT_CODEX_BIN, PATH entries, and desktop application locations.
Execution and doctor integration
skills/loop-it/scripts/run-loop.mjs, skills/loop-it/scripts/doctor.mjs, bin/loop-it.mjs, package.json
Uses the resolver for Codex execution and doctor checks, reports desktop discovery, documents --codex-bin, and syntax-checks the new module.
Discovery smoke coverage
tests/run-proof-smoke.mjs, tests/smoke-install.mjs
Adds fake Desktop CLI scenarios for execution and doctor JSON reporting, including isolated package validation.
Release metadata and documentation
.claude-plugin/plugin.json, .codex-plugin/plugin.json, .cursor-plugin/plugin.json, package.json, CHANGELOG.md, README.md, docs/examples.md, docs/install.md
Bumps versions to 0.3.20 and documents discovery, overrides, doctor behavior, and the release fix.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant RunLoop as run-loop.mjs
  participant Resolver as resolveCodexCli
  participant Codex as Codex CLI
  User->>RunLoop: run --execute codex
  RunLoop->>Resolver: resolve requested and environment options
  Resolver-->>RunLoop: Codex binary and source
  RunLoop->>Codex: execute proof with resolved binary
  Codex-->>RunLoop: proof result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing Codex Desktop CLI discovery.
Description check ✅ Passed The description is mostly complete and includes summary, verification, and notes, but it omits the template's Release impact section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/codex-cli-resolution

Comment @coderabbitai help to get the list of available commands.

@fhajjej-ship-it
fhajjej-ship-it merged commit 9f5f72c into main Jul 11, 2026
3 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
skills/loop-it/scripts/run-loop.mjs (1)

1229-1229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Help text wording inconsistent across CLI entry points.

This line reads "Codex executable override; otherwise discover PATH or Codex Desktop", while the equivalent --codex-bin description in doctor.mjs (line 553) and bin/loop-it.mjs (line 180) both read "Override PATH and Codex Desktop executable discovery". Aligning the phrasing across all three help outputs improves consistency for users running --help on different subcommands.

✏️ Suggested alignment
-  --codex-bin <path>        Codex executable override; otherwise discover PATH or Codex Desktop
+  --codex-bin <path>        Override PATH and Codex Desktop executable discovery
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/loop-it/scripts/run-loop.mjs` at line 1229, Update the --codex-bin
help text in run-loop.mjs to use the same “Override PATH and Codex Desktop
executable discovery” wording already used by doctor.mjs and bin/loop-it.mjs.
tests/smoke-install.mjs (1)

1216-1231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

env override drops the rest of the process environment.

Passing env: { HOME: desktopHome, PATH: [...] } to run() replaces the child's entire environment (spawnSync does not merge with process.env when env is supplied), unlike the equivalent block added in tests/run-proof-smoke.mjs (line ~231) which spreads ...process.env first. This makes the child process here more fragile to missing ambient vars than its sibling test, for no apparent isolation benefit beyond HOME/PATH.

🔧 Suggested fix for consistency with run-proof-smoke.mjs
   ], {
     env: {
+      ...process.env,
       HOME: desktopHome,
       PATH: [dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].join(delimiter),
     },
   }).stdout);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/smoke-install.mjs` around lines 1216 - 1231, Update the environment
override passed to run() in the desktopReady smoke-test setup to preserve the
parent environment by spreading process.env before applying the custom HOME and
PATH values. Keep the existing HOME and PATH overrides unchanged and match the
established pattern used by the sibling smoke test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/loop-it/scripts/lib/codex-cli.mjs`:
- Line 1: Update executable-candidate validation in the codex CLI resolution
flow to require both execute access and a non-directory filesystem entry. Apply
this check consistently to PATH candidates and desktop candidates before
selecting them, so directory entries are skipped and resolution continues to the
next candidate.
- Around line 43-55: Update desktopCandidates so CODEX_DESKTOP_RELATIVE_PATHS
are resolved and included only when process.platform is "darwin"; keep the
absolute macOS application paths and deduplication behavior unchanged, and
ensure non-macOS platforms return no desktop candidates.
- Around line 12-18: Update the source selection in the resolver around
requested so it reflects the normalized override used to choose the binary, not
the raw options.requested truthiness. Label the result as "argument" only when
the normalized argument value is selected; otherwise preserve "environment" for
the environment-derived binary.

In `@tests/run-proof-smoke.mjs`:
- Around line 375-397: Update createDesktopCodexFallback so the generated
desktopCodex stub uses CommonJS require() instead of top-level import
statements, preserving its existing file, path, test-replacement, and
output-writing behavior.

---

Nitpick comments:
In `@skills/loop-it/scripts/run-loop.mjs`:
- Line 1229: Update the --codex-bin help text in run-loop.mjs to use the same
“Override PATH and Codex Desktop executable discovery” wording already used by
doctor.mjs and bin/loop-it.mjs.

In `@tests/smoke-install.mjs`:
- Around line 1216-1231: Update the environment override passed to run() in the
desktopReady smoke-test setup to preserve the parent environment by spreading
process.env before applying the custom HOME and PATH values. Keep the existing
HOME and PATH overrides unchanged and match the established pattern used by the
sibling smoke test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c004288b-8278-46e1-a107-36f6a055c62c

📥 Commits

Reviewing files that changed from the base of the PR and between ec9d889 and 45f3215.

📒 Files selected for processing (14)
  • .claude-plugin/plugin.json
  • .codex-plugin/plugin.json
  • .cursor-plugin/plugin.json
  • CHANGELOG.md
  • README.md
  • bin/loop-it.mjs
  • docs/examples.md
  • docs/install.md
  • package.json
  • skills/loop-it/scripts/doctor.mjs
  • skills/loop-it/scripts/lib/codex-cli.mjs
  • skills/loop-it/scripts/run-loop.mjs
  • tests/run-proof-smoke.mjs
  • tests/smoke-install.mjs

@@ -0,0 +1,82 @@
import { accessSync, constants } from "node:fs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject directories as executable candidates.

accessSync(path, constants.X_OK) can succeed for a traversable directory on POSIX. A directory named codex in PATH or a desktop candidate can therefore be selected, causing downstream spawnSync execution to fail instead of trying another candidate.

Proposed fix
-import { accessSync, constants } from "node:fs";
+import { accessSync, constants, statSync } from "node:fs";

 function isExecutable(path) {
   try {
+    if (!statSync(path).isFile()) {
+      return false;
+    }
     accessSync(path, constants.X_OK);
     return true;
   } catch {
     return false;
   }
 }

Also applies to: 71-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/loop-it/scripts/lib/codex-cli.mjs` at line 1, Update
executable-candidate validation in the codex CLI resolution flow to require both
execute access and a non-directory filesystem entry. Apply this check
consistently to PATH candidates and desktop candidates before selecting them, so
directory entries are skipped and resolution continues to the next candidate.

Comment on lines +12 to +18
const requested = nonEmpty(options.requested) || nonEmpty(env.LOOP_IT_CODEX_BIN) || "codex";

if (requested !== "codex") {
return {
bin: requested,
source: options.requested ? "argument" : "environment",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Base the source on the normalized override.

A truthy whitespace-only options.requested causes the environment binary to be selected while the result is incorrectly labeled "argument". This makes doctor’s reported source inconsistent with the actual resolver decision.

Proposed fix
-  const requested = nonEmpty(options.requested) || nonEmpty(env.LOOP_IT_CODEX_BIN) || "codex";
+  const requestedArgument = nonEmpty(options.requested);
+  const requestedEnvironment = nonEmpty(env.LOOP_IT_CODEX_BIN);
+  const requested = requestedArgument || requestedEnvironment || "codex";

...
-      source: options.requested ? "argument" : "environment",
+      source: requestedArgument ? "argument" : "environment",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const requested = nonEmpty(options.requested) || nonEmpty(env.LOOP_IT_CODEX_BIN) || "codex";
if (requested !== "codex") {
return {
bin: requested,
source: options.requested ? "argument" : "environment",
};
const requestedArgument = nonEmpty(options.requested);
const requestedEnvironment = nonEmpty(env.LOOP_IT_CODEX_BIN);
const requested = requestedArgument || requestedEnvironment || "codex";
if (requested !== "codex") {
return {
bin: requested,
source: requestedArgument ? "argument" : "environment",
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/loop-it/scripts/lib/codex-cli.mjs` around lines 12 - 18, Update the
source selection in the resolver around requested so it reflects the normalized
override used to choose the binary, not the raw options.requested truthiness.
Label the result as "argument" only when the normalized argument value is
selected; otherwise preserve "environment" for the environment-derived binary.

Comment on lines +43 to +55
function desktopCandidates(env) {
const home = nonEmpty(env.HOME) || homedir();
const candidates = CODEX_DESKTOP_RELATIVE_PATHS.map((parts) => resolve(home, ...parts));

if (process.platform === "darwin") {
candidates.push(
"/Applications/ChatGPT.app/Contents/Resources/codex",
"/Applications/Codex.app/Contents/Resources/codex"
);
}

return [...new Set(candidates)];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Only consider desktop candidates on macOS.

The home-relative candidates are built before the Darwin check, so Linux and Windows also treat ~/Applications/ChatGPT.app/.../codex and ~/Applications/Codex.app/.../codex as desktop binaries. If such a path exists, downstream execution will label it "desktop" and invoke it.

Proposed fix
 function desktopCandidates(env) {
+  if (process.platform !== "darwin") {
+    return [];
+  }
+
   const home = nonEmpty(env.HOME) || homedir();
   const candidates = CODEX_DESKTOP_RELATIVE_PATHS.map((parts) => resolve(home, ...parts));

-  if (process.platform === "darwin") {
-    candidates.push(
-      "/Applications/ChatGPT.app/Contents/Resources/codex",
-      "/Applications/Codex.app/Contents/Resources/codex"
-    );
-  }
+  candidates.push(
+    "/Applications/ChatGPT.app/Contents/Resources/codex",
+    "/Applications/Codex.app/Contents/Resources/codex"
+  );

   return [...new Set(candidates)];
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function desktopCandidates(env) {
const home = nonEmpty(env.HOME) || homedir();
const candidates = CODEX_DESKTOP_RELATIVE_PATHS.map((parts) => resolve(home, ...parts));
if (process.platform === "darwin") {
candidates.push(
"/Applications/ChatGPT.app/Contents/Resources/codex",
"/Applications/Codex.app/Contents/Resources/codex"
);
}
return [...new Set(candidates)];
}
function desktopCandidates(env) {
if (process.platform !== "darwin") {
return [];
}
const home = nonEmpty(env.HOME) || homedir();
const candidates = CODEX_DESKTOP_RELATIVE_PATHS.map((parts) => resolve(home, ...parts));
candidates.push(
"/Applications/ChatGPT.app/Contents/Resources/codex",
"/Applications/Codex.app/Contents/Resources/codex"
);
return [...new Set(candidates)];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/loop-it/scripts/lib/codex-cli.mjs` around lines 43 - 55, Update
desktopCandidates so CODEX_DESKTOP_RELATIVE_PATHS are resolved and included only
when process.platform is "darwin"; keep the absolute macOS application paths and
deduplication behavior unchanged, and ensure non-macOS platforms return no
desktop candidates.

Comment thread tests/run-proof-smoke.mjs
Comment on lines +375 to +397
function createDesktopCodexFallback() {
mkdirSync(dirname(desktopCodex), { recursive: true });
writeFileSync(
desktopCodex,
[
"#!/usr/bin/env node",
"import { readFileSync, writeFileSync } from 'node:fs';",
"import { resolve } from 'node:path';",
"",
"const testPath = resolve(process.cwd(), 'test.mjs');",
"const before = readFileSync(testPath, 'utf8');",
"writeFileSync(testPath, before.replace('assert.equal(total, 3);', 'assert.equal(total, 2);'));",
"const outputIndex = process.argv.indexOf('--output-last-message');",
"if (outputIndex !== -1 && process.argv[outputIndex + 1]) {",
" writeFileSync(resolve(process.cwd(), process.argv[outputIndex + 1]), 'Codex Desktop fallback fixed the test.\\n');",
"}",
"",
].join("\n"),
"utf8"
);
chmodSync(desktopCodex, 0o755);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Node.js enable module syntax detection (--experimental-detect-module) by default on Node 18.x?

💡 Result:

No, Node.js does not enable module syntax detection (--experimental-detect-module) by default on Node.js 18.x [1][2]. The --experimental-detect-module feature was introduced in later versions of Node.js (specifically added in v21.1.0 and v20.10.0) [3][4]. Because it was not present in the 18.x release line, it cannot be enabled or disabled by default in those versions [1][2]. Even in versions where the flag exists, it was originally disabled by default and only later enabled by default in much newer releases [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json / node engines ==\n'
git ls-files 'package.json' '**/package.json' | xargs -r -I{} sh -c "echo '--- {}'; sed -n '1,220p' '{}'" | sed -n '1,260p'

printf '\n== relevant test file slice ==\n'
sed -n '360,420p' tests/run-proof-smoke.mjs

printf '\n== nearby package markers for tests ==\n'
find . -path '*/package.json' -o -name '.node-version' -o -name '.nvmrc' -o -name '.tool-versions' | sed 's#^\./##' | sort

Repository: fhajjej-ship-it/loop-it

Length of output: 5180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the generated desktop CLI path is under a package scope with type=module
python3 - <<'PY'
from pathlib import Path
p = Path('tests/run-proof-smoke.mjs').resolve()
for parent in [p.parent, *p.parents]:
    pkg = parent / 'package.json'
    if pkg.exists():
        print(f'nearest package.json: {pkg}')
        print(pkg.read_text())
        break
else:
    print('nearest package.json: none')
PY

Repository: fhajjej-ship-it/loop-it

Length of output: 3103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate desktopCodex and the temp project layout around the smoke test
rg -n "desktopCodex|createDesktopCodexFallback|mkdtemp|tmpdir|package.json" tests/run-proof-smoke.mjs

printf '\n== top of file ==\n'
sed -n '1,120p' tests/run-proof-smoke.mjs

Repository: fhajjej-ship-it/loop-it

Length of output: 5462


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the helper that creates the sandboxed project to see whether it carries package.json/type=module.
sed -n '120,260p' tests/run-proof-smoke.mjs

Repository: fhajjej-ship-it/loop-it

Length of output: 5212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "desktopCodex|createDesktopCodexFallback|mkdtemp|tmpdir|package.json" tests/run-proof-smoke.mjs

printf '\n== top of file ==\n'
sed -n '1,120p' tests/run-proof-smoke.mjs

printf '\n== middle of file ==\n'
sed -n '120,260p' tests/run-proof-smoke.mjs

Repository: fhajjej-ship-it/loop-it

Length of output: 10535


Use CommonJS for the desktop fallback stub

tests/run-proof-smoke.mjs:375-397 writes desktopCodex under a temp HOME path, outside any package.json, so the top-level import depends on Node’s module-syntax detection and will fail on Node 18. Switch this fixture to require() or make it an explicit .mjs file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/run-proof-smoke.mjs` around lines 375 - 397, Update
createDesktopCodexFallback so the generated desktopCodex stub uses CommonJS
require() instead of top-level import statements, preserving its existing file,
path, test-replacement, and output-writing behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant