Skip to content

Detect

detect

Stdlib-only detection of external tools and third-party auth state.

Tool/auth probing (:func:detect_tool, :func:detect_auth) depends on the standard library and pydantic only — no AXM package is imported at module load, so this layer runs as the bootstrap probe before the rest of AXM is installable. The git-identity check (:func:detect_git_identity) additionally resolves the central axm-config store ([git].default) to know whether a committer identity exists; that import is deferred to the function body so the module stays importable on a machine where axm-config is not yet present. The value is never read, only its presence. All detection is strictly read-only: it inspects an exit code or the existence of a credential/store entry, and never reads the token or identity value.

AuthStatus

Bases: BaseModel

Frozen read-only auth state for a third-party binary.

Carries the command to recover from logged_out (login_cmd) but NEVER a token value — detection is existence/exit-code only.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class AuthStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen read-only auth state for a third-party binary.

    Carries the command to recover from ``logged_out`` (``login_cmd``) but
    NEVER a token value — detection is existence/exit-code only.
    """

    tool: str
    state: AuthState
    login_cmd: str | None = None

GhConfigStatus

Bases: BaseModel

Frozen verdict on whether gh carries a base configuration.

configured when gh config get git_protocol exits 0, unconfigured otherwise, not_installed when the gh binary is absent. The config value itself is never read.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class GhConfigStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen verdict on whether ``gh`` carries a base configuration.

    ``configured`` when ``gh config get git_protocol`` exits 0, ``unconfigured``
    otherwise, ``not_installed`` when the ``gh`` binary is absent. The config
    value itself is never read.
    """

    state: GhConfigState

GitIdentityStatus

Bases: BaseModel

Frozen verdict on whether a git committer identity is resolvable.

state is decided from the presence of a [git].default store entry or the exit code of git config --get user.email — the identity value itself is never read.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class GitIdentityStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen verdict on whether a git committer identity is resolvable.

    ``state`` is decided from the *presence* of a ``[git].default`` store entry
    or the exit code of ``git config --get user.email`` — the identity value
    itself is never read.
    """

    state: GitIdentityState

ToolStatus

Bases: BaseModel

Frozen result of probing a single external tool on PATH.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class ToolStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen result of probing a single external tool on ``PATH``."""

    name: str
    state: ToolState
    version: str | None = None
    path: str | None = None

detect_auth(tool)

Report read-only auth state for a third-party binary.

gh is probed via the exit code of gh auth status; credential-file tools (claude, codex) via the existence of their credential file under ~ — the file is never opened, so no token is ever read.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_auth(tool: str) -> AuthStatus:
    """Report read-only auth state for a third-party binary.

    ``gh`` is probed via the exit code of ``gh auth status``; credential-file
    tools (``claude``, ``codex``) via the *existence* of their credential file
    under ``~`` — the file is never opened, so no token is ever read.
    """
    login_cmd = _LOGIN_CMDS.get(tool)
    if tool == "gh":
        state = _detect_gh_auth()
    elif tool in _CRED_FILES:
        # On darwin the token may live in the login Keychain; check it first,
        # then fall back to the credential file. Either source present ->
        # logged_in, so a file-backed session (container, CI, CLAUDE_CONFIG_DIR,
        # a locked/absent Keychain) is never mis-reported as logged_out.
        keychain_ok = (
            sys.platform == "darwin"
            and tool in _KEYCHAIN_SERVICES
            and _detect_keychain_auth(_KEYCHAIN_SERVICES[tool]) == "logged_in"
        )
        state = "logged_in" if keychain_ok or _cred_file_present(tool) else "logged_out"
    else:
        # Unknown auth tool: when its binary IS on PATH, "not_installed" would
        # be misleading — we simply cannot verify its login, so report
        # logged_out (recovery hint follows if known). Only when the binary is
        # absent is "not_installed" the honest state.
        state = "logged_out" if shutil.which(tool) is not None else "not_installed"
    return AuthStatus(
        tool=tool,
        state=state,
        login_cmd=login_cmd if state == "logged_out" else None,
    )

detect_gh_config()

Report whether gh carries a base config, value-free.

Probes the exit code of gh config get git_protocol (its stdout is captured and discarded). gh absent → not_installed; any OSError / SubprocessError degrades to unconfigured. This is distinct from and additional to the gh auth status login check.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_gh_config() -> GhConfigStatus:
    """Report whether ``gh`` carries a base config, value-free.

    Probes the exit code of ``gh config get git_protocol`` (its stdout is
    captured and discarded). ``gh`` absent → ``not_installed``; any
    ``OSError`` / ``SubprocessError`` degrades to ``unconfigured``. This is
    distinct from and additional to the ``gh auth status`` login check.
    """
    if shutil.which("gh") is None:
        return GhConfigStatus(state="not_installed")
    try:
        proc = subprocess.run(
            ["gh", "config", "get", "git_protocol"],  # noqa: S607 - controlled binary
            capture_output=True,
            text=True,
            timeout=_VERSION_TIMEOUT_S,
            check=False,
        )
    except (OSError, subprocess.SubprocessError):
        return GhConfigStatus(state="unconfigured")
    state: GhConfigState = "configured" if proc.returncode == 0 else "unconfigured"
    return GhConfigStatus(state=state)

detect_git_identity()

Report whether a git committer identity is resolvable, value-free.

Cheapest source first: a truthy [git].default in the axm-config store means an identity exists. Otherwise fall back to the exit code of git config --get user.email (its stdout — the email — is captured and discarded, never returned). Any missing binary / OSError / SubprocessError degrades to unconfigured without raising.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_git_identity() -> GitIdentityStatus:
    """Report whether a git committer identity is resolvable, value-free.

    Cheapest source first: a truthy ``[git].default`` in the ``axm-config``
    store means an identity exists. Otherwise fall back to the exit code of
    ``git config --get user.email`` (its stdout — the email — is captured and
    discarded, never returned). Any missing binary / ``OSError`` /
    ``SubprocessError`` degrades to ``unconfigured`` without raising.
    """
    import axm_config

    if axm_config.get("git", "default", default=None):
        return GitIdentityStatus(state="configured")
    if shutil.which("git") is None:
        return GitIdentityStatus(state="unconfigured")
    try:
        proc = subprocess.run(
            ["git", "config", "--get", "user.email"],  # noqa: S607 - controlled binary
            capture_output=True,
            text=True,
            timeout=_VERSION_TIMEOUT_S,
            check=False,
        )
    except (OSError, subprocess.SubprocessError):
        return GitIdentityStatus(state="unconfigured")
    state: GitIdentityState = "configured" if proc.returncode == 0 else "unconfigured"
    return GitIdentityStatus(state=state)

detect_tool(name)

Probe name on PATH and parse <name> --version.

Returns present with the parsed version string when found, absent otherwise. Never raises on a missing or misbehaving tool.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_tool(name: str) -> ToolStatus:
    """Probe ``name`` on ``PATH`` and parse ``<name> --version``.

    Returns ``present`` with the parsed version string when found, ``absent``
    otherwise. Never raises on a missing or misbehaving tool.
    """
    path = shutil.which(name)
    if path is None:
        return ToolStatus(name=name, state="absent")
    return ToolStatus(
        name=name,
        state="present",
        version=_probe_version(name),
        path=path,
    )