Skip to content

feat(codex-discovery): let callers scope install candidate paths - #32

Merged
Harold Hunt (huntharo) merged 2 commits into
mainfrom
claude/gifted-faraday-e44e19
Aug 18, 2026
Merged

feat(codex-discovery): let callers scope install candidate paths#32
Harold Hunt (huntharo) merged 2 commits into
mainfrom
claude/gifted-faraday-e44e19

Conversation

@huntharo

@huntharo Harold Hunt (huntharo) commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Problem

Four tests in packages/codex-discovery/test/codex-discovery.test.ts fail on any dev machine with a real Codex CLI installed at a well-known path:

  • parses and exposes a selected 0.139.0 version with trailing punctuation
  • surfaces a too-old binary as a candidate with failureReason codex_too_old (not dropped)
  • throws CodexCliNotInstalledError (not a raw ENOENT) when nothing is found
  • throws a too-old error (not CodexCliNotInstalledError) when only an old binary exists

Those tests call discovery with platform: "linux", and getCodexInstallCandidatePaths("linux") hardcodes /usr/local/bin/codex among the auto-candidates — a path shared with the macOS list. On a host with a real install there (reproduced against codex-cli 0.146.1 on macOS), the real binary is discovered and, since auto candidates sort newest-first, outranks the temp-dir shim each test just wrote. The two "nothing is installed" cases resolve instead of throwing.

This is not a regression — the tests fail identically on a pristine checkout of main. There was simply no way for a caller to control the auto-candidate set.

Change

Adds an optional CodexInstallCandidateOptions seam, accepted by both discoverCodexCommands and resolveCodexCommand:

  • installCandidatePathsreplaces the platform list (spread getCodexInstallCandidatePaths into your own list to extend instead); defaults to getCodexInstallCandidatePaths(platform, homeDir)
  • homeDir — expands the user-local entries of the default list; defaults to os.homedir()

Both are optional and default to today's behavior, so existing callers (codex-oneshot-client.ts, codex-thread-client.ts) are unchanged. getCodexInstallCandidatePaths and the new type are now exported so hosts can extend the default list.

While making the helper public, three latent problems in it were fixed:

  • A blank homeDir (a host writing process.env.HOME ?? "", or os.homedir() itself when HOME is set but empty) produced relative entries like .local/bin/codex, which discovery resolved — and executed — against process.cwd(). Reproduced selecting a repo-local script over the real install. Now falls back to the real home.
  • The helper joined user-local paths with the host's path, so its platform argument was only honoured when it matched process.platform. Now joins with the target platform's rules.
  • Its doc claimed list order decides which install wins; selection is by version, with order only breaking ties. Corrected, along with CodexCliNotInstalledError's doc and default message, which claimed a search that a narrowed installCandidatePaths scoped out.

resolveCodexCommand now forwards options by spread rather than field-by-field, so env, platform, the probe budget/signal from #31 and the install-candidate options all reach discovery without being re-listed — and anything added later does too.

Note for reviewers

One test change goes beyond the fix. returns auto candidates newest-first and honors env > config > auto priority wrote two application shims but never passed them to discovery, so its only assertion ran against an undefined candidate and could not fail. It now feeds both shims in oldest-first and asserts the whole snapshot's order.

Verification

pnpm typecheck, pnpm build, pnpm test, and all three lint targets pass. Verified by mutation — each of these is now caught, where three previously slipped through green:

Mutation Before After
default list dropped (?? []) survived killed
homeDir forward dropped survived killed
[] falls through to the defaults survived killed
blank-homeDir guard removed n/a killed
platform-aware join reverted n/a killed

Rebased onto #31. Its probe-budget rework (DEFAULT_COMMAND_VERSION_TIMEOUT_MS 2s → 10s) also resolves the slow-spawn flakiness an earlier revision of this description flagged: the full suite now passes under parallel load on the machine where it previously failed.

🤖 Generated with Claude Code

@huntharo
Harold Hunt (huntharo) force-pushed the claude/gifted-faraday-e44e19 branch 2 times, most recently from b7322fb to 3258a9d Compare August 18, 2026 03:41
Discovery probed a hardcoded list of well-known install locations on top
of PATH, with no way to override it. `/usr/local/bin/codex` appears on
both the macOS and Linux lists, so on any dev machine with a real Codex
CLI installed there, that binary was discovered and — since auto
candidates sort newest-first — outranked the temp-dir shims the tests
had just written. Four tests failed as a result, including the two
"nothing is installed" cases, which resolved instead of throwing.

Add an optional `installCandidatePaths` / `homeDir` seam to
`discoverCodexCommands` and `resolveCodexCommand`, defaulting to
`getCodexInstallCandidatePaths(platform, homeDir)` so existing callers
are unaffected. Export `getCodexInstallCandidatePaths` and the new
`CodexInstallCandidateOptions` type from the package entry point so
hosts can extend the default list rather than replace it.

Tests now scope auto candidates to their own fixtures, plus three new
tests covering the seam. Also makes "returns auto candidates
newest-first" meaningful: it wrote two application shims but never
passed them to discovery, so its only assertion ran against an
undefined candidate and could not fail.
Review follow-ups on the seam added in the previous commit.

Correctness:
- `getCodexInstallCandidatePaths` resolved `homeDir` through a default
  parameter, which fires only on `undefined`. Now that the value is
  caller-supplied public API, a blank string (a host writing
  `process.env.HOME ?? ""`, or `os.homedir()` itself when HOME is set but
  empty) produced relative entries like `.local/bin/codex` that discovery
  resolved — and executed — against `process.cwd()`.
- The same helper joined user-local paths with the host's `path`, so the
  `platform` argument was only honoured when it matched `process.platform`.
  It now joins with the target platform's rules.
- Its doc comment claimed list order decides which install wins; selection
  is by version, with order only breaking ties. Corrected, and the
  `CodexCliNotInstalledError` doc and default message no longer claim a
  search that a narrowed `installCandidatePaths` scoped out.

API:
- `resolveCodexCommand` forwards options by spread instead of field-by-field,
  so a future `CodexInstallCandidateOptions` field cannot be silently
  dropped, and its `platform` now accepts an explicit `undefined` like its
  siblings under `exactOptionalPropertyTypes`.
- Option docs state that an explicit list replaces rather than extends the
  defaults, that entries must be absolute, and why `homeDir` has nothing to
  act on once a list is supplied. Test-suite rationale no longer ships in
  the published `.d.ts`.

Tests:
- "defaults the auto candidates to the platform install list" only asserted
  that a fresh mkdtemp path was absent, so it passed even with the default
  list replaced by `[]`. Replaced with a positive test that plants a shim
  under a temp `homeDir`, and that also covers `resolveCodexCommand`'s
  `homeDir` forward — previously deletable with the suite still green.
- Added coverage that an explicit list replaces rather than appends to the
  defaults, which was indistinguishable on a runner with no Codex installed.
- The ordering assertion now covers the whole snapshot instead of a
  source-filtered subset, and the win32 branch and blank-homeDir fallback
  are pinned directly.

Verified by mutation: dropping the default list, dropping either forward,
making `[]` fall through to the defaults, removing the blank-homeDir guard,
and reverting the platform-aware join are each now caught.
@huntharo
Harold Hunt (huntharo) force-pushed the claude/gifted-faraday-e44e19 branch from 3258a9d to 989198f Compare August 18, 2026 03:42
@huntharo
Harold Hunt (huntharo) merged commit 73690eb into main Aug 18, 2026
2 checks passed
@huntharo
Harold Hunt (huntharo) deleted the claude/gifted-faraday-e44e19 branch August 18, 2026 03:43
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