2026-06-17 Repository implementation Issue gh-forgejo-shim-b95

Fixed Codex GitHub CLI availability probes for Forgejo hosts

The shim now answers Forgejo-scoped gh auth status, gh auth token, and gh api user probes itself, while GitHub repositories still delegate to the real GitHub CLI.

Summary

Codex.app was again reporting GitHub CLI unavailable for Forgejo and GitHub workflows. The repo and machine checks showed the GUI PATH and installed shim were visible, but a newer availability-style probe fails when GH_HOST points at a Forgejo host and the shim delegates auth/API commands to the real gh. This change makes those host-scoped auth probes part of the Forgejo compatibility surface.

Changes Made

Forgejo Auth Routing

Added host-scoped routing for gh auth status and gh auth token when GH_HOST, --hostname, or the current repo resolves to an allowlisted Forgejo host.

API Identity Probe

Added support for the common gh api user probe, including --jq .login, so tools can confirm the active Forgejo identity without the real GitHub CLI rejecting the host.

Machine-Readable Status

Implemented gh auth status --json hosts with a GitHub-shaped hosts envelope and a shim-specific token source.

Version Signal

Bumped package metadata from 0.1.0 to 0.1.1 so the installed gfj version can distinguish this regression fix.

Context

The important failing repro was not basic PATH discovery. gfj doctor already reported a good GUI PATH, installed shim, real gh, fj, and Forgejo auth. The red path was a constrained invocation such as GH_HOST=git.dirtydishes.dev gh auth status: before this change, the shim delegated auth to the real GitHub CLI, which tried to validate Forgejo auth as if it were GitHub Enterprise auth and returned a failure.

Important Implementation Details

Relevant Diff Snippets

Focused unified diff fallback. It shows the routing surface, auth/API handlers, version bump, and regression tests without embedding the full file diff.

diff --git a/src/gh_forgejo_shim/routing.py b/src/gh_forgejo_shim/routing.py
@@
-from .config import Config, load_config
+from .config import Config, load_config, normalize_host
@@
 SUPPORTED_PR_COMMANDS = {"checks", "checkout", "co", "comment", "create", "diff", "list", "new", "status", "view"}
 SUPPORTED_ISSUE_COMMANDS = {"create", "list", "ls", "new", "view"}
 SUPPORTED_REPO_COMMANDS = {"view"}
+SUPPORTED_AUTH_COMMANDS = {"status", "token"}
@@
 class RouteDecision:
     kind: str
     reason: str
     repo: RepoRef | None = None
+    host: str | None = None
@@
+def _is_supported_auth_command(argv: list[str]) -> bool:
+    return len(argv) >= 2 and argv[0] == "auth" and argv[1] in SUPPORTED_AUTH_COMMANDS
+
+def _is_supported_api_command(argv: list[str]) -> bool:
+    return len(argv) >= 2 and argv[0] == "api" and _api_endpoint(argv[1:]) in {"user", "/user"}
@@
+    if _is_supported_auth_command(argv):
+        return _decide_host_route(
+            argv,
+            config=config,
+            env=values,
+            cwd=cwd,
+            host=_hostname_arg(argv[2:], allow_short=True) or values.get("GH_HOST"),
+        )
+
+    if _is_supported_api_command(argv):
+        return _decide_host_route(
+            argv,
+            config=config,
+            env=values,
+            cwd=cwd,
+            host=_hostname_arg(argv[1:], allow_short=False) or values.get("GH_HOST"),
+        )
@@
+def run_forgejo_auth(
+    argv: list[str],
+    host: str,
+    token: str | None,
+    *,
+    stdout: TextIO,
+    stderr: TextIO,
+) -> int:
+    command = argv[1]
+    rest = argv[2:]
+    if command == "status":
+        parsed = _parse_auth_status_args(rest)
+        if token is None:
+            _print_missing_auth_status(host, stderr)
+            return 1
+        data = _auth_status_data(host, token, show_token=parsed.show_token)
+        if parsed.json_fields:
+            _print_json_or_jq(_filter_selected_fields(data, parsed.json_fields), parsed.jq, stdout, template=parsed.template)
+        else:
+            _print_auth_status_text(host, token if parsed.show_token else None, stdout)
+        return 0
@@
+def run_forgejo_api(
+    argv: list[str],
+    host: str,
+    client: ForgejoClient,
+    *,
+    stdout: TextIO,
+) -> int:
+    parsed = _parse_api_args(argv[1:])
+    if parsed.endpoint not in {"user", "/user"}:
+        raise ValueError(f"unsupported Forgejo api endpoint: {parsed.endpoint}")
+    user = client.get_current_user(host)
+    if parsed.silent:
+        return 0
+    _print_json_or_jq(user, parsed.jq, stdout, template=parsed.template)
+    return 0

diff --git a/tests/test_routing.py b/tests/test_routing.py
@@
+    def test_forgejo_auth_status_uses_shim_auth_instead_of_real_gh(self) -> None:
+        out = io.StringIO()
+        code = run_gh(
+            ["auth", "status"],
+            env={
+                "FJ_SHIM_HOSTS": "git.example.com",
+                "FJ_SHIM_REAL_GH": "/missing/gh",
+                "FJ_SHIM_TOKEN": "secret-token",
+                "GH_HOST": "git.example.com",
+                "PATH": "",
+            },
+            stdout=out,
+            stderr=io.StringIO(),
+        )
+
+        self.assertEqual(code, 0)
+        self.assertIn("git.example.com", out.getvalue())
+        self.assertIn("Logged in", out.getvalue())
@@
+    def test_forgejo_api_user_probe_uses_forgejo_client(self) -> None:
+        out = io.StringIO()
+        code = run_gh(
+            ["api", "user", "--jq", ".login"],
+            env={
+                "FJ_SHIM_HOSTS": "git.example.com",
+                "FJ_SHIM_REAL_GH": "/missing/gh",
+                "FJ_SHIM_TOKEN": "secret-token",
+                "GH_HOST": "git.example.com",
+                "PATH": "",
+            },
+            stdout=out,
+            stderr=io.StringIO(),
+            client_factory=lambda token: FakeClient(),
+        )
+
+        self.assertEqual(code, 0)
+        self.assertEqual(out.getvalue(), "alice\n")

diff --git a/pyproject.toml b/pyproject.toml
@@
-version = "0.1.0"
+version = "0.1.1"

Expected Impact for End-Users

Codex.app should stop treating Forgejo-hosted workspaces as having an unavailable GitHub CLI when it probes auth or identity through GH_HOST. GitHub repositories should continue to use the real GitHub CLI and should keep reporting normal GitHub auth and repository metadata.

Validation

Issues, Limitations, and Mitigations

Follow-up Work

No new follow-up issue is required for this fix. A useful future improvement would be a dedicated compatibility test script that runs the common Codex probes against the installed shim, but the current regression tests cover the failing auth/API seams.