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, });