Commit 6a32c8ac authored by Hardik Zinzuvadiya's avatar Hardik Zinzuvadiya
Browse files

Phase 0: Add foundation files for v2.0.0 restructure

- constants.py: single source of truth for repo URLs, version (2.0.0),
  all paths via Path.home(), UI theme constants, PRIV_CMD auto-detection
- os_detect.py: OSInfo dataclass, auto-detect OS/distro/package manager,
  CURRENT_OS singleton, per-OS install command maps
- config.py: get_tools_dir(), load()/save() config.json, get_sudo_cmd()
- tools/__init__.py, tools/others/__init__.py: make proper Python packages
- IMPLEMENTATION.md: full 18-section restructuring plan (2350+ lines)
- LOG.md: 13-phase progress tracker
parent 7df27d83
Loading
Loading
Loading
Loading

config.py

0 → 100644
+43 −0
Original line number Diff line number Diff line
import json
import logging
from pathlib import Path
from typing import Any

from constants import USER_CONFIG_FILE, USER_TOOLS_DIR, DEFAULT_CONFIG

logger = logging.getLogger(__name__)


def load() -> dict[str, Any]:
    """Load config from disk, merging with defaults for any missing keys."""
    if USER_CONFIG_FILE.exists():
        try:
            on_disk = json.loads(USER_CONFIG_FILE.read_text())
            return {**DEFAULT_CONFIG, **on_disk}
        except (json.JSONDecodeError, OSError) as exc:
            logger.warning("Config file unreadable (%s), using defaults.", exc)
    return dict(DEFAULT_CONFIG)


def save(cfg: dict[str, Any]) -> None:
    """Write config to disk, creating parent directories if needed."""
    USER_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
    USER_CONFIG_FILE.write_text(json.dumps(cfg, indent=2, sort_keys=True))


def get_tools_dir() -> Path:
    """
    Return the directory where external tools are stored.
    Creates it if it does not exist.
    Always an absolute path — never relies on process CWD.
    """
    cfg = load()
    tools_dir = Path(cfg.get("tools_dir", str(USER_TOOLS_DIR))).expanduser().resolve()
    tools_dir.mkdir(parents=True, exist_ok=True)
    return tools_dir


def get_sudo_cmd() -> str:
    """Return 'doas' if available, else 'sudo'. Never hardcode 'sudo'."""
    import shutil
    return "doas" if shutil.which("doas") else "sudo"
 No newline at end of file

constants.py

0 → 100644
+65 −0
Original line number Diff line number Diff line
from pathlib import Path
import platform
import shutil as _shutil

# ── Repository ────────────────────────────────────────────────────────────────
REPO_OWNER   = "Z4nzu"
REPO_NAME    = "hackingtool"
REPO_URL     = f"https://github.com/{REPO_OWNER}/{REPO_NAME}.git"
REPO_WEB_URL = f"https://github.com/{REPO_OWNER}/{REPO_NAME}"

# ── Versioning ────────────────────────────────────────────────────────────────
VERSION         = "2.0.0"
VERSION_DISPLAY = f"v{VERSION}"

# ── Python requirement ────────────────────────────────────────────────────────
MIN_PYTHON = (3, 10)

# ── User-scoped paths (cross-platform, always computed at runtime) ─────────────
# NEVER hardcode /home/username — use Path.home() so it works for any user,
# including root (/root), regular users (/home/alice), macOS (/Users/alice).
USER_CONFIG_DIR  = Path.home() / f".{REPO_NAME}"
USER_TOOLS_DIR   = USER_CONFIG_DIR / "tools"
USER_CONFIG_FILE = USER_CONFIG_DIR / "config.json"
USER_LOG_FILE    = USER_CONFIG_DIR / f"{REPO_NAME}.log"

# ── System install paths (set per OS) ─────────────────────────────────────────
_system = platform.system()

if _system == "Darwin":
    # macOS — Homebrew convention
    APP_INSTALL_DIR = Path("/usr/local/share") / REPO_NAME
    APP_BIN_PATH    = Path("/usr/local/bin")   / REPO_NAME
elif _system == "Linux":
    APP_INSTALL_DIR = Path("/usr/share") / REPO_NAME
    APP_BIN_PATH    = Path("/usr/bin")   / REPO_NAME
else:
    # Fallback (Windows, FreeBSD, etc.)
    APP_INSTALL_DIR = USER_CONFIG_DIR / "app"
    APP_BIN_PATH    = USER_CONFIG_DIR / "bin" / REPO_NAME

# ── UI theme ──────────────────────────────────────────────────────────────────
THEME_PRIMARY  = "bold magenta"
THEME_BORDER   = "bright_magenta"
THEME_SUCCESS  = "bold green"
THEME_ERROR    = "bold red"
THEME_WARNING  = "bold yellow"
THEME_DIM      = "dim white"
THEME_ARCHIVED = "dim yellow"
THEME_URL      = "underline bright_blue"
THEME_ACCENT   = "bold cyan"

# ── Default config values ──────────────────────────────────────────────────────
DEFAULT_CONFIG: dict = {
    "tools_dir":      str(USER_TOOLS_DIR),
    "version":        VERSION,
    "theme":          "magenta",
    "show_archived":  False,
    "sudo_binary":    "sudo",
    "go_bin_dir":     str(Path.home() / "go" / "bin"),
    "gem_bin_dir":    str(Path.home() / ".gem" / "ruby"),
}

# ── Privilege escalation ───────────────────────────────────────────────────────
# Prefer doas if present (OpenBSD/some Linux setups), else sudo
PRIV_CMD = "doas" if _shutil.which("doas") else "sudo"
 No newline at end of file

os_detect.py

0 → 100644
+128 −0
Original line number Diff line number Diff line
import platform
import shutil
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class OSInfo:
    system: str                    # "linux", "macos", "windows", "unknown"
    distro_id: str        = ""     # "kali", "ubuntu", "arch", "fedora", etc.
    distro_like: str      = ""     # "debian", "rhel", etc. (from ID_LIKE)
    distro_version: str   = ""     # "2024.1", "22.04", etc.
    pkg_manager: str      = ""     # "apt-get", "pacman", "dnf", "brew", etc.
    is_root: bool         = False
    home_dir: Path        = field(default_factory=Path.home)
    is_wsl: bool          = False  # Windows Subsystem for Linux
    arch: str             = ""     # "x86_64", "aarch64", "arm64"


def detect() -> OSInfo:
    """
    Fully detect the current OS, distro, and available package manager.
    Never asks the user — entirely automatic.
    """
    import os

    system_raw = platform.system()
    system = system_raw.lower()
    if system == "darwin":
        system = "macos"

    info = OSInfo(
        system  = system,
        is_root = (os.geteuid() == 0) if hasattr(os, "geteuid") else False,
        home_dir = Path.home(),
        arch    = platform.machine(),
    )

    # ── Linux-specific ─────────────────────────────────────────────────────────
    if system == "linux":
        # Detect WSL
        try:
            info.is_wsl = "microsoft" in Path("/proc/version").read_text().lower()
        except (FileNotFoundError, PermissionError):
            pass

        # Read /etc/os-release (standard on all modern distros)
        os_release: dict[str, str] = {}
        for path in ("/etc/os-release", "/usr/lib/os-release"):
            try:
                for line in Path(path).read_text().splitlines():
                    k, _, v = line.partition("=")
                    os_release[k.strip()] = v.strip().strip('"')
                break
            except FileNotFoundError:
                continue

        info.distro_id      = os_release.get("ID", "").lower()
        info.distro_like    = os_release.get("ID_LIKE", "").lower()
        info.distro_version = os_release.get("VERSION_ID", "")

    # ── Package manager detection (in priority order) ──────────────────────────
    for mgr in ("apt-get", "pacman", "dnf", "zypper", "apk", "brew", "pkg"):
        if shutil.which(mgr):
            info.pkg_manager = mgr
            break

    return info


# Module-level singleton — computed once on import
CURRENT_OS: OSInfo = detect()


# ── Per-OS package manager commands ────────────────────────────────────────────
PACKAGE_INSTALL_CMDS: dict[str, str] = {
    "apt-get": "apt-get install -y {packages}",
    "pacman":  "pacman -S --noconfirm {packages}",
    "dnf":     "dnf install -y {packages}",
    "zypper":  "zypper install -y {packages}",
    "apk":     "apk add {packages}",
    "brew":    "brew install {packages}",
}

PACKAGE_UPDATE_CMDS: dict[str, str] = {
    "apt-get": "apt-get update -qq && apt-get upgrade -y",
    "pacman":  "pacman -Syu --noconfirm",
    "dnf":     "dnf upgrade -y",
    "zypper":  "zypper update -y",
    "apk":     "apk update && apk upgrade",
    "brew":    "brew update && brew upgrade",
}

# Core system packages needed per package manager
REQUIRED_PACKAGES: dict[str, list[str]] = {
    "apt-get": ["git", "python3-pip", "python3-venv", "curl", "wget",
                "ruby", "ruby-dev", "golang-go", "php", "default-jre-headless"],
    "pacman":  ["git", "python-pip", "curl", "wget",
                "ruby", "go", "php", "jre-openjdk-headless"],
    "dnf":     ["git", "python3-pip", "curl", "wget",
                "ruby", "golang", "php", "java-17-openjdk-headless"],
    "zypper":  ["git", "python3-pip", "curl", "wget", "ruby", "go", "php"],
    "brew":    ["git", "python3", "curl", "wget", "ruby", "go", "php"],
}


def install_packages(packages: list[str], os_info: OSInfo | None = None) -> bool:
    """Install system packages using the detected package manager."""
    import subprocess
    if os_info is None:
        os_info = CURRENT_OS

    mgr = os_info.pkg_manager
    if mgr not in PACKAGE_INSTALL_CMDS:
        print(f"[warning] Unknown package manager. Install manually: {packages}")
        return False

    cmd_template = PACKAGE_INSTALL_CMDS[mgr]
    pkg_str = " ".join(packages)
    cmd = cmd_template.format(packages=pkg_str)

    # Prepend privilege escalation only on Linux (brew on macOS doesn't need sudo)
    if os_info.system == "linux" and not os_info.is_root:
        from constants import PRIV_CMD
        cmd = f"{PRIV_CMD} {cmd}"

    result = subprocess.run(cmd, shell=True, check=False)
    return result.returncode == 0
 No newline at end of file

tools/__init__.py

0 → 100644
+0 −0

Empty file added.

+0 −0

Empty file added.