From 30a661823fb6952dd3ca85f645e8d4857682466a Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Tue, 5 May 2026 08:21:26 +0200 Subject: [PATCH] Prefer ghx when proxying GitHub CLI calls Guardex already centralizes GitHub CLI execution through GH_BIN. This keeps the explicit GUARDEX_GH_BIN override intact, but lets installed ghx act as the default cache proxy before falling back to gh. Status now shows the active proxy command so automation can verify what will run. Constraint: ghx must remain optional and must not replace explicit GUARDEX_GH_BIN values Rejected: Add a Guardex-owned ghx binary alias | that would conflict with the real ghx package and make install ownership unclear Confidence: high Scope-risk: narrow Directive: Keep GitHub CLI selection centralized in src/context.js; do not reintroduce local process.env.GUARDEX_GH_BIN fallbacks Tested: rtk test node --test test/status.test.js test/doctor.test.js Not-tested: live ghx daemon cache behavior --- README.md | 2 +- src/cli/main.js | 1 + src/context.js | 18 ++++++++++++++++-- src/doctor/index.js | 5 +++-- test/status.test.js | 29 +++++++++++++++++++++++++++-- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b29cdf0b..53a0a200 100644 --- a/README.md +++ b/README.md @@ -388,7 +388,7 @@ Install repo skills with `npx skills add recodee/gitguardex`; `npx skills add re | [**cavekit**](https://github.com/JuliusBrussee/cavekit) — `npx skills add JuliusBrussee/cavekit` | Spec-driven build loop with `spec`, `build`, `check`, `caveman`, `backprop` skills bundled in. | [![stars](https://img.shields.io/github/stars/JuliusBrussee/cavekit?style=social)](https://github.com/JuliusBrussee/cavekit) | | [**caveman**](https://github.com/JuliusBrussee/caveman) — `npx skills add JuliusBrussee/caveman` | Ultra-compressed response mode for Claude / Codex. Less output-token churn on long reviews and debug loops. | [![stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=social)](https://github.com/JuliusBrussee/caveman) | | [**codex-account-switcher**](https://github.com/recodeecom/codex-account-switcher-cli) — `npm i -g @imdeadpool/codex-account-switcher` | Multi-identity Codex account switcher. Auto-registers accounts on `codex login`; switch with one command. | [![stars](https://img.shields.io/github/stars/recodeecom/codex-account-switcher-cli?style=social)](https://github.com/recodeecom/codex-account-switcher-cli) | -| [**GitHub CLI (`gh`)**](https://github.com/cli/cli) — see [cli.github.com](https://cli.github.com/) | Required for PR / merge automation. `gx branch finish --via-pr --wait-for-merge` depends on it. | [![stars](https://img.shields.io/github/stars/cli/cli?style=social)](https://github.com/cli/cli) | +| [**GitHub CLI (`gh`)**](https://github.com/cli/cli) — see [cli.github.com](https://cli.github.com/) | Required for PR / merge automation. `gx branch finish --via-pr --wait-for-merge` depends on it. If `ghx` is on `PATH`, Guardex uses that GitHub CLI cache proxy automatically; set `GUARDEX_GH_BIN=gh` to force direct `gh`. | [![stars](https://img.shields.io/github/stars/cli/cli?style=social)](https://github.com/cli/cli) | --- diff --git a/src/cli/main.js b/src/cli/main.js index 85f4d5b9..c7206000 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -1957,6 +1957,7 @@ function collectServicesSnapshot() { ...requiredSystemTools.map((tool) => ({ name: tool.name, displayName: tool.displayName || tool.name, + command: tool.command, status: tool.status, })), ]; diff --git a/src/context.js b/src/context.js index da650d22..09524bcb 100644 --- a/src/context.js +++ b/src/context.js @@ -61,11 +61,24 @@ const OPTIONAL_LOCAL_COMPANION_TOOLS = [ installArgs: ['skills', 'add', 'JuliusBrussee/caveman'], }, ]; -const GH_BIN = process.env.GUARDEX_GH_BIN || 'gh'; +function commandAvailable(command) { + const result = cp.spawnSync(command, ['--version'], { stdio: 'ignore' }); + return result.status === 0; +} + +function resolveGithubCliBin(env = process.env) { + const explicit = String(env.GUARDEX_GH_BIN || '').trim(); + if (explicit) { + return explicit; + } + return commandAvailable('ghx') ? 'ghx' : 'gh'; +} + +const GH_BIN = resolveGithubCliBin(); const REQUIRED_SYSTEM_TOOLS = [ { name: 'gh', - displayName: 'GitHub (gh)', + displayName: GH_BIN === 'ghx' ? 'GitHub (ghx proxy)' : 'GitHub (gh)', command: GH_BIN, installHint: 'https://cli.github.com/', }, @@ -698,6 +711,7 @@ module.exports = { GLOBAL_TOOLCHAIN_SERVICES, GLOBAL_TOOLCHAIN_PACKAGES, OPTIONAL_LOCAL_COMPANION_TOOLS, + resolveGithubCliBin, GH_BIN, REQUIRED_SYSTEM_TOOLS, MAINTAINER_RELEASE_REPO, diff --git a/src/doctor/index.js b/src/doctor/index.js index 4240aa91..b27e5f4d 100644 --- a/src/doctor/index.js +++ b/src/doctor/index.js @@ -3,6 +3,7 @@ const { path, TOOL_NAME, SHORT_TOOL_NAME, + GH_BIN, LOCK_FILE_RELATIVE, REQUIRED_MANAGED_REPO_FILES, OMX_SCAFFOLD_DIRECTORIES, @@ -442,7 +443,7 @@ function finishDoctorSandboxBranch(blocked, metadata, options = {}) { }; } - const ghBin = process.env.GUARDEX_GH_BIN || 'gh'; + const ghBin = GH_BIN; if (!isCommandAvailable(ghBin)) { return { status: 'skipped', @@ -890,7 +891,7 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) { const originAvailable = hasOriginRemote(repoRoot); const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim()); - const ghBin = process.env.GUARDEX_GH_BIN || 'gh'; + const ghBin = GH_BIN; const ghAvailable = originAvailable && (explicitGhBin || originRemoteLooksLikeGithub(repoRoot)) && diff --git a/test/status.test.js b/test/status.test.js index c71761fb..2a2adb7b 100644 --- a/test/status.test.js +++ b/test/status.test.js @@ -142,7 +142,7 @@ echo "unexpected gh args: $*" >&2 exit 1 `); - const result = runNodeWithEnv([], repoDir, { + const result = runNodeWithEnv(['status', '--verbose'], repoDir, { GUARDEX_GH_BIN: fakeGh.fakePath, }); assert.equal(result.status, 0, result.stderr || result.stdout); @@ -150,6 +150,31 @@ exit 1 }); +test('status prefers ghx as the GitHub CLI proxy when no explicit gh binary is set', () => { + const repoDir = initRepo(); + const fakeGhx = createFakeBin('ghx', ` +if [[ "$1" == "--version" ]]; then + echo "ghx version 1.0.0" + exit 0 +fi +echo "unexpected ghx args: $*" >&2 +exit 1 +`); + + const result = runNodeWithEnv(['status', '--target', repoDir, '--json'], repoDir, { + PATH: `${fakeGhx.fakeBin}:${process.env.PATH}`, + }); + + assert.equal(result.status, 0, result.stderr || result.stdout); + const payload = JSON.parse(result.stdout); + const ghService = payload.services.find((service) => service.name === 'gh'); + assert.ok(ghService, 'GitHub CLI service should be included in status payload'); + assert.equal(ghService.displayName, 'GitHub (ghx proxy)'); + assert.equal(ghService.command, 'ghx'); + assert.equal(ghService.status, 'active'); +}); + + test('warning-only degraded status avoids zero-error wording and points humans at doctor', () => { const repoDir = initRepo(); @@ -484,7 +509,7 @@ echo "unexpected npm args: $*" >&2 exit 1 `); - const result = runNodeWithEnv(['status', '--target', targetDir], targetDir, { + const result = runNodeWithEnv(['status', '--target', targetDir, '--verbose'], targetDir, { GUARDEX_NPM_BIN: fakeNpm, GUARDEX_HOME_DIR: fakeHome, });