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.

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."""

    tool: str
    state: AuthState
    login_cmd: str | None = None
    declaration_consulted: bool = Field(
        default=False,
        description="a declaration was discovered and consulted to produce this state",
    )

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 auth state through a package declaration when one is installed.

A tool without a declaration degrades to presence detection: an installed binary has an undetermined auth state because its session cannot be verified.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_auth(tool: str) -> AuthStatus:
    """Report auth state through a package declaration when one is installed.

    A tool without a declaration degrades to presence detection: an installed
    binary has an undetermined auth state because its session cannot be verified.
    """
    declaration = load_auth_declarations().get(tool)
    if declaration is not None:
        declared_state = _detect_declared_auth(declaration)
        return AuthStatus(
            tool=tool,
            state=declared_state,
            login_cmd=(
                declaration.login_command if declared_state == "logged_out" else None
            ),
            declaration_consulted=True,
        )

    state: AuthState = (
        "undetermined" if shutil.which(tool) is not None else "not_installed"
    )
    return AuthStatus(
        tool=tool,
        state=state,
        declaration_consulted=False,
    )

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

load_auth_declarations()

Discover authentication declarations without making vault a bootstrap import.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def load_auth_declarations() -> dict[str, AuthDependencySpec]:
    """Discover authentication declarations without making vault a bootstrap import."""
    try:
        from importlib.metadata import entry_points

        from axm_vault import AuthDependencySpec, groups_from_provider
    except ImportError:
        return {}

    declarations: dict[str, AuthDependencySpec] = {}
    for endpoint in entry_points(group="axm.credentials"):
        try:
            provider = endpoint.load()
        except Exception:  # noqa: BLE001, S112 - isolate a broken distribution
            continue
        # Same all-or-nothing validation as the vault catalog: a rejected
        # contribution (raising, non-iterable, junk item) contributes nothing.
        groups, rejection = groups_from_provider(endpoint.name, provider)
        if rejection is not None:
            continue
        for group in groups:
            for declaration in group.auth_dependencies:
                if isinstance(declaration, AuthDependencySpec):
                    declarations[declaration.name] = declaration
    return declarations