Ctrl K
ComunidadGuías…

butler/tools/butlerignore.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Parse ``.butlerignore`` patterns (Cursor ``.cursorignore`` subset)."""

from __future__ import annotations

import fnmatch
import logging
import os
from pathlib import Path

from butler.defaults.env_defaults import SECRETS_PATH_DEFAULT

logger = logging.getLogger(__name__)

_IGNORE_FILENAMES = (".butlerignore",)
_PROTECTED_WRITE_GLOBS = (
    ".butler/*.json",
    ".butler/**/*.json",
    ".git/config",
    ".git/hooks/**",
    ".git/info/attributes",
    ".env",
    ".env.*",
)


def _ignore_file_candidates(workspace: Path) -> list[Path]:
    ws = workspace.resolve(strict=False)
    return [ws / name for name in _IGNORE_FILENAMES] + [ws / ".butler" / ".butlerignore"]


def load_ignore_patterns(workspace: Path | None = None) -> tuple[str, ...]:
    """Return normalized glob patterns from workspace ignore files."""
    from butler.tools.butlerignore_ops import current_workspace_root_safe

    if workspace is None:
        workspace = current_workspace_root_safe()
    if workspace is None:
        return ()

    patterns: list[str] = []
    for path in _ignore_file_candidates(workspace):
        if not path.is_file():
            continue
        try:
            for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
                raw = line.strip()
                if not raw or raw.startswith("#"):
                    continue
                if raw.startswith("!"):
                    continue
                patterns.append(raw.replace("\\", "/"))
        except OSError as exc:
            logger.debug("butlerignore read skipped %s: %s", path, exc)
    return tuple(patterns)


def _rel_posix(path: Path, workspace: Path) -> str | None:
    try:
        rel = path.resolve(strict=False).relative_to(workspace.resolve(strict=False))
    except ValueError:
        return None
    return rel.as_posix()


def matches_ignore_pattern(rel_posix: str, pattern: str) -> bool:
    pat = pattern.strip().replace("\\", "/")
    if not pat:
        return False
    if pat.endswith("/"):
        pat = pat.rstrip("/") + "/**"
    if "**" in pat:
        return fnmatch.fnmatch(rel_posix, pat) or fnmatch.fnmatch(
            rel_posix, pat.replace("**", "*")
        )
    if "/" not in pat:
        base = rel_posix.split("/")[-1]
        return fnmatch.fnmatch(base, pat) or fnmatch.fnmatch(rel_posix, pat)
    return fnmatch.fnmatch(rel_posix, pat)


def is_butlerignored(path: Path, *, workspace: Path | None = None) -> bool:
    """True when ``path`` matches ``.butlerignore`` under ``workspace``."""
    from butler.tools.butlerignore_ops import current_workspace_root_safe

    if workspace is None:
        workspace = current_workspace_root_safe()
    if workspace is None:
        return False
    rel = _rel_posix(path, workspace)
    if rel is None:
        return False
    for pattern in load_ignore_patterns(workspace):
        if matches_ignore_pattern(rel, pattern):
            return True
    return False


def is_protected_write_path(path: Path, *, workspace: Path | None = None) -> bool:
    """Hard-coded write-protected paths (Cursor sandbox protected paths subset)."""
    from butler.tools.butlerignore_ops import current_workspace_root_safe

    if workspace is None:
        workspace = current_workspace_root_safe()
    if workspace is None:
        return False
    rel = _rel_posix(path, workspace)
    if rel is None:
        return False
    for pattern in _PROTECTED_WRITE_GLOBS:
        if matches_ignore_pattern(rel, pattern):
            return True
    return is_butlerignored(path, workspace=workspace)


def credential_mask_paths() -> list[str]:
    """Host credential paths masked inside OS sandbox (tmpfs overlay)."""
    home = Path.home()
    candidates = [
        home / ".ssh",
        home / ".aws" / "credentials",
        home / ".config" / "gcloud",
        home / ".docker" / "config.json",
    ]
    env_path = os.getenv("BUTLER_SECRETS_PATH", SECRETS_PATH_DEFAULT).strip()
    if env_path:
        candidates.append(Path(env_path).expanduser())
    out: list[str] = []
    for path in candidates:
        try:
            resolved = str(path.expanduser().resolve(strict=False))
        except OSError:
            continue
        if Path(resolved).exists() and resolved not in out:
            out.append(resolved)
    return out


__all__ = [
    "credential_mask_paths",
    "is_butlerignored",
    "is_protected_write_path",
    "load_ignore_patterns",
    "matches_ignore_pattern",
]