From 6766a9ab13b09ecf008764e0ddaf26170d08700d Mon Sep 17 00:00:00 2001 From: Dhara Pandya Date: Mon, 6 Oct 2025 00:30:21 +0530 Subject: [PATCH] Implement reporting and update-security-db commands --- archpkg.egg-info/PKG-INFO | 349 +++++++++++++++++++ archpkg.egg-info/SOURCES.txt | 25 ++ archpkg.egg-info/dependency_links.txt | 1 + archpkg.egg-info/entry_points.txt | 2 + archpkg.egg-info/requires.txt | 4 + archpkg.egg-info/top_level.txt | 1 + archpkg/__main__.py | 37 ++ archpkg/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 147 bytes archpkg/__pycache__/__main__.cpython-312.pyc | Bin 0 -> 2027 bytes archpkg/__pycache__/paths.cpython-312.pyc | Bin 0 -> 1851 bytes archpkg/__pycache__/security.cpython-312.pyc | Bin 0 -> 2616 bytes archpkg/paths.py | 31 ++ archpkg/security.py | 37 ++ build/lib/archpkg/__main__.py | 37 ++ build/lib/archpkg/config.py | 100 ++++++ build/lib/archpkg/exceptions.py | 202 +++++++++++ build/lib/archpkg/logging_config.py | 243 +++++++++++++ build/lib/archpkg/paths.py | 31 ++ build/lib/archpkg/security.py | 37 ++ 19 files changed, 1137 insertions(+) create mode 100644 archpkg.egg-info/PKG-INFO create mode 100644 archpkg.egg-info/SOURCES.txt create mode 100644 archpkg.egg-info/dependency_links.txt create mode 100644 archpkg.egg-info/entry_points.txt create mode 100644 archpkg.egg-info/requires.txt create mode 100644 archpkg.egg-info/top_level.txt create mode 100644 archpkg/__main__.py create mode 100644 archpkg/__pycache__/__init__.cpython-312.pyc create mode 100644 archpkg/__pycache__/__main__.cpython-312.pyc create mode 100644 archpkg/__pycache__/paths.cpython-312.pyc create mode 100644 archpkg/__pycache__/security.cpython-312.pyc create mode 100644 archpkg/paths.py create mode 100644 archpkg/security.py create mode 100644 build/lib/archpkg/__main__.py create mode 100644 build/lib/archpkg/config.py create mode 100644 build/lib/archpkg/exceptions.py create mode 100644 build/lib/archpkg/logging_config.py create mode 100644 build/lib/archpkg/paths.py create mode 100644 build/lib/archpkg/security.py diff --git a/archpkg.egg-info/PKG-INFO b/archpkg.egg-info/PKG-INFO new file mode 100644 index 0000000..073730b --- /dev/null +++ b/archpkg.egg-info/PKG-INFO @@ -0,0 +1,349 @@ +Metadata-Version: 2.4 +Name: archpkg +Version: 0.1.0 +Summary: Your ArchPkg CLI tool +Author: AdmGenSameer +Author-email: AdmGenSameer +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: requests +Requires-Dist: rich +Requires-Dist: typer[all] +Requires-Dist: distro +Dynamic: author +Dynamic: license-file +Dynamic: requires-python + +# archpkg-helper + +A cross-distro command-line utility that helps you search for packages and generate install commands for native package managers (pacman, AUR, apt, dnf, flatpak, snap). It aims to make discovering and installing software on Linux simpler, regardless of your distribution. + +## Table of Contents + +- [About](#about) +- [Features](#features) +- [Quick Start (install.sh)](#quick-start-installsh) +- [Installation (Recommended: pipx)](#installation-recommended-pipx) +- [Alternative Installation (pip)](#alternative-installation-pip) +- [Usage](#usage) +- [File Structure](#file-structure) +- [Contributing](#contributing) +- [License](#license) + +## About + +archpkg-helper is designed to work across Linux distributions. While originally inspired by Arch Linux, it detects your system and generates appropriate install commands for common package managers. It’s suitable for both newcomers and experienced users who want a simpler way to search and install packages. + +## Features + +- Search for packages and generate install commands for: + - pacman (Arch), AUR, apt (Debian/Ubuntu), dnf (Fedora), flatpak, snap +- Cross-distro support (not limited to Arch) +- Clear, readable output and errors +- One-command setup via `install.sh` + +## Quick Start (install.sh) + +Install directly using the provided installer script. + +From a cloned repository: +```sh +git clone https://github.com/AdmGenSameer/archpkg-helper.git +cd archpkg-helper +bash install.sh +``` + +Or run directly from the web: +```sh +curl -fsSL https://raw.githubusercontent.com/AdmGenSameer/archpkg-helper/main/install.sh | bash +# or +wget -qO- https://raw.githubusercontent.com/AdmGenSameer/archpkg-helper/main/install.sh | bash +``` + +Notes: +- The installer ensures Python, pip, and pipx are available and installs the CLI via pipx. +- You may be prompted for sudo to install prerequisites on your distro. + +## Installation (Recommended: pipx) + +On Arch and many other distros, system Python may be “externally managed” (PEP 668), which prevents global pip installs. pipx installs Python CLIs into isolated environments and puts their executables on your PATH—this is the easiest, safest method. + +1) Install pipx +- Arch Linux: + ```sh + sudo pacman -S pipx + pipx ensurepath + ``` +- Debian/Ubuntu: + ```sh + sudo apt update + sudo apt install pipx + pipx ensurepath + ``` +- Fedora: + ```sh + sudo dnf install pipx + pipx ensurepath + ``` + +2) Install archpkg-helper with pipx +- Directly from GitHub: + ```sh + pipx install git+https://github.com/AdmGenSameer/archpkg-helper.git + ``` +- From a local clone: + ```sh + git clone https://github.com/AdmGenSameer/archpkg-helper.git + cd archpkg-helper + pipx install . + ``` + +Upgrade later with: +```sh +pipx upgrade archpkg-helper +``` + +Ensure your shell session has pipx’s bin path in PATH (pipx prints instructions after `pipx ensurepath`, typically `~/.local/bin`). + +## Alternative Installation (pip) + +If you prefer pip, install in user scope to avoid system conflicts: + +- From a local clone: + ```sh + git clone https://github.com/AdmGenSameer/archpkg-helper.git + cd archpkg-helper + python3 -m pip install --user . + ``` +- Directly from GitHub: + ```sh + python3 -m pip install --user git+https://github.com/AdmGenSameer/archpkg-helper.git + ``` + +If your distro enforces PEP 668 protections for global installs, you may see errors. You can bypass with: +```sh +python3 -m pip install --break-system-packages . +``` +However, using pipx is strongly recommended instead of breaking system protections. + +## Usage + +After installation, the CLI is available as `archpkg`. + +--- + +### Example Commands + +Here are some common commands for using the archpkg tool: + +#### 1. Search for a Package + +Search for a package across all supported package managers: + +```sh +archpkg search firefox +``` + + +This command will search for the `firefox` package across multiple package managers (e.g., pacman, AUR, apt). + +#### 2. Install a Package + +Once you have identified a package, use the install command to generate the correct installation command for your system: + +```sh +archpkg install firefox +``` + + +This will generate an appropriate installation command (e.g., `pacman -S firefox` for Arch-based systems). + +#### 3. Install a Package from AUR (Arch User Repository) + +To install from the AUR specifically: + +```sh +archpkg install vscode --source aur +``` + + +This installs `vscode` from the AUR. + +#### 4. Install a Package from Pacman + +To install a package directly using pacman (e.g., on Arch Linux): + +```sh +archpkg install firefox --source pacman +``` + + +#### 5. Remove a Package + +To generate commands to remove a package: + +```sh +archpkg remove firefox +``` + + +This will generate the command necessary to uninstall `firefox` from your system. + +--- + +### Optional Flags + +#### 1. `--source ` + +You can specify the package manager source using the `--source` flag. Supported sources include: + +- pacman (Arch Linux) +- aur (AUR) +- apt (Debian/Ubuntu) +- dnf (Fedora) +- flatpak (Flatpak) +- snap (Snap) + +For example, to install `vscode` from the AUR: + +```sh +archpkg install vscode --source aur +``` + + +#### 2. `--help` + +To view a list of available commands and options: + +```sh +archpkg --help +``` + + +#### 3. `--version` + +To check the installed version of archpkg: + +```sh +archpkg --version +``` +--- + +## 🏗️ Architecture + +The tool is structured as a **modular Python CLI** with: + +- 📝 **Command Parser** + Handles subcommands like `search`, `install`, `remove`. + +- 🔌 **Backend Adapters** + Provides an abstraction layer for each package manager: + - `pacman` (Arch Linux) + - `aur` (Arch User Repository) + - `apt` (Debian/Ubuntu) + - `dnf` (Fedora) + - `flatpak` + - `snap` + +- 🖥️ **System Detector** + Automatically detects your Linux distribution and selects the correct package manager. + +- ⚡ **Installer Script (`install.sh`)** + One-line setup that ensures Python, pip, and pipx are installed before deploying `archpkg`. + +This modular architecture makes the project **extensible** — new package managers can be added with minimal changes. + +--- + +## Tips for Beginners + +- **Start by Searching:** Before installing anything, try using the `archpkg search ` command to check if the package exists and where it can be installed from. + +```sh +archpkg search firefox +``` + + +This will list all available versions of Firefox across supported sources. + +- **Understand Sources and Flags:** By default, archpkg will try to find packages from the most common sources. If you prefer to use a specific source (e.g., AUR or pacman), you can specify it using the `--source` flag. + +```sh +archpkg install vscode --source aur +``` + + +- **Keep It Simple with Installation:** Once you find the package you want, use the `archpkg install ` command to generate the installation command for your system. + +- **Removal Commands:** Don’t forget that archpkg can also generate commands for removing installed packages. For example: + +```sh +archpkg remove firefox +``` + + +- **Auto-detect Your Package Manager:** If you’re unsure which package manager your distro uses, The archpkg-helper tool can automatically detect your system, making it easier to get started without manual configuration. + +- **Handle Permission Errors with sudo:** If you encounter permission errors, try using `sudo` (superuser privileges) for commands that require administrative rights, especially when installing prerequisites or system packages. + + +--- + +## File Structure + +Top-level layout of this repository: +``` +archpkg-helper/ +├── archpkg/ # Core Python package code (CLI and logic) +├── install.sh # One-command installer script (uses pipx) +├── pyproject.toml # Build/metadata configuration +├── setup.py # Packaging configuration (entry points, deps) +├── LICENSE # Project license (Apache 2.0) +├── README.md # Project documentation (this file) +├── build/ # Build artifacts (may appear after builds) +├── __pycache__/ # Python bytecode cache (auto-generated) +├── archpkg_helper.egg-info/ # Packaging metadata (auto-generated) +└── archpy.egg-info/ # Packaging metadata (auto-generated) +``` + +Some metadata/build directories are generated during packaging and may not be present in fresh clones. + +## Notes + + - The installer ensures Python, pip, and pipx are available. + - You may be prompted for sudo. + +## Contributing + +Contributions are welcome! Please: +1. Fork the repository +2. Create a feature branch: `git checkout -b feature-branch` +3. Make your changes and commit: `git commit -m "Describe your changes"` +4. Push to your fork: `git push origin feature-branch` +5. Open a Pull Request + +Report bugs or request features via the [issue tracker](https://github.com/AdmGenSameer/archpkg-helper/issues). +--- + +## 🛣️ Roadmap + +Here’s what’s planned for future releases of **archpkg-helper**: + +- 🔧 **Add support for `zypper` (openSUSE)** + Extend backend adapters to cover openSUSE users. + +- ⚡ **Caching layer for faster searches** + Improve performance by reducing repeated lookups across package managers. + +- 💻 **Interactive mode (`archpkg interactive`)** + A guided, menu-driven interface to search, choose a package source, and install/remove easily. + +- 🖼️ **GUI frontend (future idea)** + Build a graphical user interface on top of the CLI for desktop users who prefer point-and-click. + +--- + +## License + +This project is licensed under the [Apache License 2.0](./LICENSE). diff --git a/archpkg.egg-info/SOURCES.txt b/archpkg.egg-info/SOURCES.txt new file mode 100644 index 0000000..de09776 --- /dev/null +++ b/archpkg.egg-info/SOURCES.txt @@ -0,0 +1,25 @@ +LICENSE +README.md +pyproject.toml +setup.py +archpkg/__init__.py +archpkg/__main__.py +archpkg/cli.py +archpkg/command_gen.py +archpkg/config.py +archpkg/exceptions.py +archpkg/logging_config.py +archpkg/paths.py +archpkg/search_apt.py +archpkg/search_aur.py +archpkg/search_dnf.py +archpkg/search_flatpak.py +archpkg/search_pacman.py +archpkg/search_snap.py +archpkg/security.py +archpkg.egg-info/PKG-INFO +archpkg.egg-info/SOURCES.txt +archpkg.egg-info/dependency_links.txt +archpkg.egg-info/entry_points.txt +archpkg.egg-info/requires.txt +archpkg.egg-info/top_level.txt \ No newline at end of file diff --git a/archpkg.egg-info/dependency_links.txt b/archpkg.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/archpkg.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/archpkg.egg-info/entry_points.txt b/archpkg.egg-info/entry_points.txt new file mode 100644 index 0000000..00d9865 --- /dev/null +++ b/archpkg.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +archpkg = archpkg.cli:app diff --git a/archpkg.egg-info/requires.txt b/archpkg.egg-info/requires.txt new file mode 100644 index 0000000..bb6ef61 --- /dev/null +++ b/archpkg.egg-info/requires.txt @@ -0,0 +1,4 @@ +requests +rich +typer[all] +distro diff --git a/archpkg.egg-info/top_level.txt b/archpkg.egg-info/top_level.txt new file mode 100644 index 0000000..a12aa12 --- /dev/null +++ b/archpkg.egg-info/top_level.txt @@ -0,0 +1 @@ +archpkg diff --git a/archpkg/__main__.py b/archpkg/__main__.py new file mode 100644 index 0000000..5aa814b --- /dev/null +++ b/archpkg/__main__.py @@ -0,0 +1,37 @@ +import argparse +from archpkg.security import SecurityDB + +db = SecurityDB() + +parser= argparse.ArgumentParser() +subparsers= parser.add_subparsers(dest="command") + +install_parser= subparsers.add_parser("install") +install_parser.add_argument("package") + +report_parser= subparsers.add_parser("report") +report_parser.add_argument("package", help="Package to report as suspicious") +report_parser.add_argument("--reason", default="Suspicious package") + +update_parser= subparsers.add_parser("update-security-db") + +args=parser.parse_args() + +if args.command == "install": + status= db.check_package(args.package) + if status=="blocked": + print(f"{args.package} is blocked (unsafe)") + elif status=="safe": + print(f"{args.package} is verified and safe") + else: + print(f"{args.package} is not verified") + confirm= input("Do you want to contribute? [y/N]") + if confirm.lower() == "y": + pass + else: + print("Installation aborted.") +elif args.command =="report": + db.report_package(args.package, args.reason) + print(f"Reported {args.package} as suspicious") +elif args.command=="update-security-db": + db.update_security_db() diff --git a/archpkg/__pycache__/__init__.cpython-312.pyc b/archpkg/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55ddfb747bd7288464d58bc7b6dd1b4814462b37 GIT binary patch literal 147 zcmX@j%ge<81Qx3vWq|0%AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdWu%{*SE8S+A6lGR zRIKk2pr2TjoKcXSuA7mXQ;=E&tW#p(^QR}rU}&lVE@2&aR?~E(L;0e}KWE!bKRTOi93x-Z)Z z1~FN|_Y(~UkLe?-X|1t{&BjT@myl^A!YG_g`VpeR5J6DjKXR}HvDSsUI_H4garh|E z%!%`KMnz!PF@FWsxnrSSjmL8jl0JRe-iwID?6B`A8f11R0MMVHV0M)Fe2F=4_!iOH zH4`WSMUUT$@6z2F{XKxurvbvDK#{_%*lV!jud@EPLgFeGBYR*sskoLz$?O#A-IruD zu*d6#DpGC*m?S)})6P3%J;oB=S<25xw9Z?=-Wh`MrGH*iA!Zm=3F`hAHQA4qK2j zJMtECLOLpFx>*`oZ8{`VlbS9`HYCF`3Q9)NEW=H)jdFz&H#Mar+0fKd;z2tqwbEe} z3(=-*<;+sTDnQvpQ%0+wDY)UV8#yhrgJ7vgQVgkONE4Q7$eU==pGLh}eDC^VNz%!rr6MH>$7zIFU7 z29A)~MwuN9@@}iMt}lcSZd!)J6m&&39Y#?L7XA?Mt~M1t?=U&-A#Pr5mur6(?v0(+ z5z}P+G~DMnIN5*sdPFoD{4ADT`0cf7V{dk za_F{>e^bEyfg5}H{X9rf)bAkr2N=I(qF=3-0~g%LE?4J9s@zD88!d+#e4@^eR{7Dx zqRo%i__xZDi`YOtHdc*|9V)fhOj&FIu?_~SVDMn&6x_Io_Sd7=tI_L+pVp$&WuXCt zIvA*efrEMb%@1raa0)*3eG7K#Jl1V1?b{3EOwh36-Axp}nDsXb;cY;@lZ`s}YV? zdcPTdB{XM?x6Zh?FR#LG7fUs6(PD(YOs0Fzc4s>~vpF+s z*-c4mLLww4Kw{D+YQmGzz(Ze&CSG2B+1e88K@+09@U~hK5}y3e><+!iKbikr{>%KA z@1I{%sa*)jyz^Iaf*|y_JcOjQHyh_+vxq37*hW*W6;EL}E4ETrr&Nrn%G82F$z?J{ z+WQFXBQ$y$O+{IZ#%5neh{qMgqPN;^F{y z^^p&PSarY1^z*022J&vXVq2!;>v`8HSTj1cILrGkujvJs>n6_^E3-3$q63tTlM}hO z$H$B_6Yrg_#|B#lBG%T)M7T^?D6S`B%=y^jt|QbL<_l7E%M7ZewJZ#I^kjJYlWex^ z_}P5+LyvJUd**Dm?QEdP>b#P*Lwxn0(#K3_w(t+ z(v8xhbHi!8IPyLDA#o@1o%W-(a%^I?=j>|N`wQ=`YduSSE83w3IkW**Xj|iC`GUqB zfX&v%JuNeC7gMPp+fvLz)wXpRrtZ5zbv>4^a?7vj)b!11(_{KOlM^|;VA*W2-Wf^< z==5O8bDazhQu!9~4)`ul0TX*Q5wf&9fhgP(q1{n&> zwwIHDpDEYGzk_ah1MI~srsU+*VgD9r>wi03tOUq{`QV9`!f1~0GaX7*HLsy@d2eRDka0+<~on(6`Huc>z2$0NY1c)LJt`EvUUMcZ61Ar zzEZ*|kdaPql_#Gi??=PP!*dEo7r;A4pJgtp7tkEer0PSb9k0sS_RgL`3uI%s9$0k}l86}E_!XJN!dGzi`k#H}z#5Q$bnXti7skqWmQKck9R8=N9ZS;4H@ zz9B&`aKV$k(sjKB=uXSAwjr8lKrCDBeGLr2oI1Fg>R%XJC)%gEPjbJJ-WAe&bI&qq zklrrMs^mS$cIf z^+FTj#PH4GKa!o-#;=ZFw{O1njd?dY^jL-K2k}&+b9nj4a$h6;`da*WGm4VC7jlnO zlt?!^Ut9iYdAyMxU5mf5u5~Zu9&2C{xO?Kscz;Uwo#;@1?ueY|jfuEnlugSq3@D)d zKl6TIviW||wx)x>Vldo6b%T_piDQr)quWIk_WY1mF(^L`Z3%>9l D1W&?; literal 0 HcmV?d00001 diff --git a/archpkg/__pycache__/security.cpython-312.pyc b/archpkg/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4f3ee74065301622b0d83c447886c76e22a59fa GIT binary patch literal 2616 zcmbVO&2JM&6rb4-+p*U`a7su?)TN|U+r}|*L2Ut5B#PRiN)}RuR9sc7&CWP>vi|7o zI@DH>a>xNBDu{Y8NLBHv3Jr(;13hr=rBRBown3`eN)@+2kRoyFn_aJM0+ndT@|!pF z=DnHu&70rs&+&K^LHYZqKk|Z%(4V{!6sfIjzXwVK5k#{quweUmU$Y|J=&KXYrA%+$-24<YDUj9vkw>HGPz zGyQ33HxNZxOi)%Jco}6yB2XzO5OG<`%3LQBv?Z8c%7#b?MubTpi7ex6q!!kqZuHak zOw&^)UqDbopos9_z5}9xoQ`d+Jz^2*ojwNu^2;KcYi}{=;Wfd*#!sM2*a-)=xk#XO z>qWfQjtet%;sUhr3!-3DLr2gAvhhWL`U1l6tD&#N3+N)&gp7-s7Gm7j4Y%=grE-Oq zTxrfOgS5&9aV26n#%;lsY-;6PoU{4DRrP5)RV+CvGj-mk%uaoLE@dz?Uzwjx=BZVo zENG^JBO0&N+=#9hN(D#P>xnL&<3T6K(mn%X32h#DYvaJ!W$CK28SlS7+8A3IYl<83 z(Jh4gMpyCX;GsrsrPl2KX7Td$)w4Sy^zLxeBQBoWF$nVgp~fl+OSX}tZg|Ej zoAVT~_nJzn?C1-W6>kbFCacSjJ^Rrd9F_>PDrCW*NR{WJ41Gk254&55LInrEMo!J$&rn&cxIwW&jB&Zt_B8!{?z54;0dpqki)+4HdZK2=rq z!QSJ??Vvr>gZ8tgesHtj1p#nJlxuxg`tC=D*CWF>F02Y0k+<(h((93QYw)$!f#FB8 zcs%+9iG8uH2rs(oCfb}m7Lj?XpLCns9K(k%CS=@aOh}g~jn%vVb_4aF_`WI2i zQm>q^0F6`C$x{`$#jsSn{hq1QQ{$acj26od)j8vzR7t^f)EsqyF^HOD6TlFDPv7&cYgCO?ut(Rh6m)Td7(fk z8CTC)hDq~fi%`~f9nSx!;_Sv4%*!V)m4E7}fYhd^VaT=3jyDG;}j z7tP}y#%a$JJag;`5X Path: + """Return the XDG-compliant config directory for archpkg.""" + config_home= Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) + return config_home / "archpkg" +def get_security_db_path()-> Path: + """Return the full path to the security database JSON file.""" + return get_config_dir() / "security-db.json" +def init_security_db(): + """Ensure the security DB directory and file exist.""" + db_path= get_security_db_path() + db_path.parent.mkdir(parents=True, exist_ok=True) + + if not db_path.exists(): + default_data={ + "last_updated": None, + "safe": ["yay","paru"], + "unsafe": ["malicoius-pkg"], + "reports": [] + } + with open(db_path, "w") as f: + json.dump(default_data, f, indent=2) + print(f"Created new security database at {db_path}") + else: + print(f"Security database already exists at {db_path}") + return db_path +if __name__== "__main__": + init_security_db() \ No newline at end of file diff --git a/archpkg/security.py b/archpkg/security.py new file mode 100644 index 0000000..96bd269 --- /dev/null +++ b/archpkg/security.py @@ -0,0 +1,37 @@ +import json +from pathlib import Path +from .paths import get_security_db_path + +class SecurityDB: + def __init__(self): + self.db_path= get_security_db_path() + with open(self.db_path, "r") as f: + self.data= json.load(f) + def check_package(self, pkg_name: str): + if pkg_name in self.data.get("unsafe", []): + return "blocked" + elif pkg_name in self.data.get("safe",[]): + return "safe" + else: + return "not_verified" + def report_package(self,pkg_name: str, reason: str): + self.data.setdefault("reports", []).append({ + "package": pkg_name, + "reason": reason + }) + self.save() + def save(self): + with open(self.db_path, "w") as f: + json.dump(self.data,f, indent=2) + def update_security_db(self, remote_data= None): + """ + Update the local security DB. + remote_data: dict fetched from server + """ + if remote_data: + self.data.update(remote_data) + self.data["last_updated"]="2025-10-05" + self.save() + print("Security database updated from remote.") + else: + print("No remote data provided. Placeholder update.") \ No newline at end of file diff --git a/build/lib/archpkg/__main__.py b/build/lib/archpkg/__main__.py new file mode 100644 index 0000000..5aa814b --- /dev/null +++ b/build/lib/archpkg/__main__.py @@ -0,0 +1,37 @@ +import argparse +from archpkg.security import SecurityDB + +db = SecurityDB() + +parser= argparse.ArgumentParser() +subparsers= parser.add_subparsers(dest="command") + +install_parser= subparsers.add_parser("install") +install_parser.add_argument("package") + +report_parser= subparsers.add_parser("report") +report_parser.add_argument("package", help="Package to report as suspicious") +report_parser.add_argument("--reason", default="Suspicious package") + +update_parser= subparsers.add_parser("update-security-db") + +args=parser.parse_args() + +if args.command == "install": + status= db.check_package(args.package) + if status=="blocked": + print(f"{args.package} is blocked (unsafe)") + elif status=="safe": + print(f"{args.package} is verified and safe") + else: + print(f"{args.package} is not verified") + confirm= input("Do you want to contribute? [y/N]") + if confirm.lower() == "y": + pass + else: + print("Installation aborted.") +elif args.command =="report": + db.report_package(args.package, args.reason) + print(f"Reported {args.package} as suspicious") +elif args.command=="update-security-db": + db.update_security_db() diff --git a/build/lib/archpkg/config.py b/build/lib/archpkg/config.py new file mode 100644 index 0000000..5e40556 --- /dev/null +++ b/build/lib/archpkg/config.py @@ -0,0 +1,100 @@ +# config.py +"""Configuration constants and settings for the Universal Package Helper CLI.""" + +# Timeout values for different package managers (in seconds) +TIMEOUTS = { + 'aur': 15, + 'pacman': 30, + 'apt': 30, + 'dnf': 45, # DNF can be slower + 'flatpak': 30, + 'snap': 30, + 'command_check': 5 +} + +# Keywords used for filtering/scoring +JUNK_KEYWORDS = ["icon", "dummy", "meta", "symlink", "wrap", "material", "launcher", "unionfs"] +LOW_PRIORITY_KEYWORDS = ["extension", "plugin", "helper", "daemon", "patch", "theme"] +BOOST_KEYWORDS = ["editor", "browser", "ide", "official", "gui", "android", "studio", "stable", "canary", "beta"] + +# Supported platforms +SUPPORTED_PLATFORMS = ["arch", "debian", "ubuntu", "linuxmint", "fedora", "manjaro"] + +# Distribution mapping +DISTRO_MAP = { + "arch": "arch", + "manjaro": "arch", + "endeavouros": "arch", + "arco": "arch", + "garuda": "arch", + "ubuntu": "debian", + "debian": "debian", + "linuxmint": "debian", + "pop": "debian", + "elementary": "debian", + "fedora": "fedora", + "rhel": "fedora", + "centos": "fedora", + "rocky": "fedora", + "alma": "fedora" +} + +# AUR helpers in order of preference +AUR_HELPERS = ['yay', 'paru', 'trizen', 'yaourt'] + +# Logging configuration +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'detailed': { + 'format': '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S' + }, + 'simple': { + 'format': '%(levelname)s - %(name)s - %(message)s' + } + }, + 'handlers': { + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': '~/.local/share/archpkg-helper/archpkg-helper.log', + 'maxBytes': 5242880, # 5MB + 'backupCount': 3, + 'formatter': 'detailed', + 'level': 'DEBUG', + 'encoding': 'utf-8' + }, + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + 'level': 'WARNING', + 'stream': 'ext://sys.stderr' + } + }, + 'loggers': { + '': { # root logger + 'handlers': ['file', 'console'], + 'level': 'DEBUG', + 'propagate': False + } + } +} + +# Log levels for different components +LOG_LEVELS = { + 'cli': 'INFO', + 'search': 'INFO', + 'command_gen': 'INFO', + 'validation': 'DEBUG', + 'network': 'INFO', + 'subprocess': 'DEBUG' +} + +# Performance monitoring settings +PERFORMANCE_CONFIG = { + 'enable_timing_logs': True, + 'log_slow_operations_threshold': 5.0, # seconds + 'enable_memory_logging': False, + 'enable_detailed_tracing': False +} \ No newline at end of file diff --git a/build/lib/archpkg/exceptions.py b/build/lib/archpkg/exceptions.py new file mode 100644 index 0000000..609d289 --- /dev/null +++ b/build/lib/archpkg/exceptions.py @@ -0,0 +1,202 @@ +# exceptions.py +"""Common exception classes for the Universal Package Helper CLI.""" + +from typing import Optional + + +class PackageSearchException(Exception): + """Base exception class for package search operations. + + This is the base class for all package search related exceptions. + It provides consistent error handling and logging support. + """ + + def __init__(self, message: str, original_error: Optional[Exception] = None): + """Initialize the exception. + + Args: + message: Human-readable error message + original_error: The original exception that caused this error, if any + """ + super().__init__(message) + self.message = message + self.original_error = original_error + + def __str__(self) -> str: + """Return string representation of the exception.""" + if self.original_error: + return f"{self.message} (caused by: {type(self.original_error).__name__}: {str(self.original_error)})" + return self.message + + +class PackageManagerNotFound(PackageSearchException): + """Exception raised when a package manager is not installed or available. + + This exception is raised when the system doesn't have the required + package manager installed or it's not available in the PATH. + """ + + def __init__(self, message: str, package_manager: Optional[str] = None, original_error: Optional[Exception] = None): + """Initialize the exception. + + Args: + message: Human-readable error message + package_manager: Name of the missing package manager + original_error: The original exception that caused this error, if any + """ + super().__init__(message, original_error) + self.package_manager = package_manager + + +class PackageManagerError(PackageSearchException): + """Exception raised when a package manager encounters an error. + + This exception is raised when a package manager is available but + encounters an error during operation (e.g., corrupted database, + configuration issues). + """ + + def __init__(self, message: str, package_manager: Optional[str] = None, exit_code: Optional[int] = None, original_error: Optional[Exception] = None): + """Initialize the exception. + + Args: + message: Human-readable error message + package_manager: Name of the package manager that failed + exit_code: Exit code returned by the package manager + original_error: The original exception that caused this error, if any + """ + super().__init__(message, original_error) + self.package_manager = package_manager + self.exit_code = exit_code + + +class NetworkError(PackageSearchException): + """Exception raised for network-related errors. + + This exception is raised when network operations fail, such as + when unable to connect to package repositories or when network + timeouts occur. + """ + + def __init__(self, message: str, url: Optional[str] = None, original_error: Optional[Exception] = None): + """Initialize the exception. + + Args: + message: Human-readable error message + url: The URL that failed to be accessed, if applicable + original_error: The original exception that caused this error, if any + """ + super().__init__(message, original_error) + self.url = url + + +class TimeoutError(PackageSearchException): + """Exception raised when operations timeout. + + This exception is raised when package manager operations or + network requests exceed their configured timeout values. + """ + + def __init__(self, message: str, timeout_duration: Optional[float] = None, operation: Optional[str] = None, original_error: Optional[Exception] = None): + """Initialize the exception. + + Args: + message: Human-readable error message + timeout_duration: The timeout duration in seconds, if known + operation: Description of the operation that timed out + original_error: The original exception that caused this error, if any + """ + super().__init__(message, original_error) + self.timeout_duration = timeout_duration + self.operation = operation + + +class ValidationError(PackageSearchException): + """Exception raised for input validation errors. + + This exception is raised when user input or internal data + fails validation checks (e.g., empty queries, invalid package names). + """ + + def __init__(self, message: str, invalid_value: Optional[str] = None, validation_rule: Optional[str] = None, original_error: Optional[Exception] = None): + """Initialize the exception. + + Args: + message: Human-readable error message + invalid_value: The value that failed validation, if applicable + validation_rule: Description of the validation rule that was violated + original_error: The original exception that caused this error, if any + """ + super().__init__(message, original_error) + self.invalid_value = invalid_value + self.validation_rule = validation_rule + + +class CommandGenerationError(Exception): + """Exception raised when command generation fails. + + This exception is raised when the system cannot generate + installation commands for packages, typically due to missing + package managers or unsupported package sources. + """ + + def __init__(self, message: str, package_name: Optional[str] = None, source: Optional[str] = None, original_error: Optional[Exception] = None): + """Initialize the exception. + + Args: + message: Human-readable error message + package_name: Name of the package for which command generation failed + source: Package source (e.g., 'apt', 'pacman') that failed + original_error: The original exception that caused this error, if any + """ + super().__init__(message) + self.message = message + self.package_name = package_name + self.source = source + self.original_error = original_error + + def __str__(self) -> str: + """Return string representation of the exception.""" + parts = [self.message] + if self.package_name: + parts.append(f"package: {self.package_name}") + if self.source: + parts.append(f"source: {self.source}") + if self.original_error: + parts.append(f"caused by: {type(self.original_error).__name__}: {str(self.original_error)}") + return " | ".join(parts) + + +# Custom exception for logging purposes +class LoggedError: + """Mixin class to add logging capabilities to exceptions. + + This class provides helper methods for logging exceptions + before they are raised or handled. + """ + + @staticmethod + def log_and_raise(logger, exception_class, message: str, **kwargs): + """Log an error and then raise the specified exception. + + Args: + logger: Logger instance to use + exception_class: Exception class to instantiate and raise + message: Error message + **kwargs: Additional arguments to pass to the exception constructor + """ + logger.error(f"Raising {exception_class.__name__}: {message}") + raise exception_class(message, **kwargs) + + @staticmethod + def log_and_reraise(logger, exception: Exception, additional_context: str = ""): + """Log an exception and then re-raise it. + + Args: + logger: Logger instance to use + exception: Exception to log and re-raise + additional_context: Additional context information to log + """ + context_part = f" | Context: {additional_context}" if additional_context else "" + logger.error(f"Re-raising {type(exception).__name__}: {str(exception)}{context_part}") + raise exception \ No newline at end of file diff --git a/build/lib/archpkg/logging_config.py b/build/lib/archpkg/logging_config.py new file mode 100644 index 0000000..bc0178c --- /dev/null +++ b/build/lib/archpkg/logging_config.py @@ -0,0 +1,243 @@ +# logging_config.py +"""Centralized logging configuration for the Universal Package Helper CLI.""" + +import logging +import logging.handlers +import sys +import os +from pathlib import Path +from typing import Optional + + +class PackageHelperLogger: + """Centralized logger configuration for consistent logging across modules.""" + + _instance: Optional['PackageHelperLogger'] = None + _initialized: bool = False + + def __new__(cls) -> 'PackageHelperLogger': + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + self._setup_logging() + PackageHelperLogger._initialized = True + + def _get_log_directory(self) -> Path: + """Get the appropriate log directory for the current platform.""" + if sys.platform == 'win32': + # Windows: Use AppData/Local + base_dir = Path(os.environ.get('LOCALAPPDATA', Path.home() / 'AppData' / 'Local')) + return base_dir / 'archpkg-helper' + else: + # Linux/macOS: Use XDG standard + xdg_data_home = os.environ.get('XDG_DATA_HOME') + if xdg_data_home: + return Path(xdg_data_home) / 'archpkg-helper' + else: + return Path.home() / '.local' / 'share' / 'archpkg-helper' + + def _setup_console_only_logging(self) -> None: + """Fallback to console-only logging if file logging fails.""" + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + + # Clear any existing handlers + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # Console handler only + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(logging.WARNING) + + console_formatter = logging.Formatter( + '%(levelname)s - %(name)s - %(message)s' + ) + console_handler.setFormatter(console_formatter) + root_logger.addHandler(console_handler) + + print("Warning: File logging unavailable, using console logging only") + + def _setup_logging(self) -> None: + """Setup centralized logging configuration with robust error handling.""" + try: + # Get cross-platform log directory + log_dir = self._get_log_directory() + + # Attempt to create log directory + try: + log_dir.mkdir(parents=True, exist_ok=True) + except (OSError, PermissionError) as e: + print(f"Warning: Cannot create log directory {log_dir}: {e}") + self._setup_console_only_logging() + return + + log_file = log_dir / 'archpkg-helper.log' + + # Test file creation/write access + try: + # Test write access + test_file = log_dir / '.test_write' + with open(test_file, 'w') as f: + f.write('test') + test_file.unlink() # Remove test file + except (OSError, PermissionError) as e: + print(f"Warning: Cannot write to log directory {log_dir}: {e}") + self._setup_console_only_logging() + return + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + # Clear any existing handlers + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # File handler with rotation + try: + file_handler = logging.handlers.RotatingFileHandler( + log_file, + maxBytes=5 * 1024 * 1024, # 5MB + backupCount=3, + encoding='utf-8' + ) + file_handler.setLevel(logging.DEBUG) + + # formatter for file + file_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + file_handler.setFormatter(file_formatter) + root_logger.addHandler(file_handler) + + except (OSError, PermissionError) as e: + print(f"Warning: Cannot create log file {log_file}: {e}") + self._setup_console_only_logging() + return + + # Console handler (only for warnings and errors by default) + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(logging.WARNING) + + # formatter for console + console_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + console_handler.setFormatter(console_formatter) + root_logger.addHandler(console_handler) + + # Log the successful initialization + logger = logging.getLogger(__name__) + logger.info("Logging system initialized successfully") + logger.info(f"Log file location: {log_file}") + logger.debug(f"Log directory: {log_dir}") + logger.debug(f"Platform: {sys.platform}") + + except Exception as e: + # Final fallback - something went very wrong + print(f"Error: Logging setup completely failed: {e}") + import traceback + traceback.print_exc() + self._setup_console_only_logging() + + @staticmethod + def get_logger(name: str) -> logging.Logger: + """Get a logger with the specified name. + + Args: + name: Logger name (typically __name__) + + Returns: + logging.Logger: Configured logger instance + """ + # Ensure logging is initialized + PackageHelperLogger() + return logging.getLogger(name) + + @staticmethod + def set_debug_mode(enabled: bool = True) -> None: + """Enable or disable debug mode logging. + + Args: + enabled: Whether to enable debug mode + """ + console_handler = None + for handler in logging.getLogger().handlers: + if isinstance(handler, logging.StreamHandler) and handler.stream == sys.stderr: + console_handler = handler + break + + if console_handler: + if enabled: + console_handler.setLevel(logging.DEBUG) + logger = logging.getLogger(__name__) + logger.info("Debug mode enabled - console will show all log levels") + print("Debug mode enabled - verbose logging to console") + else: + console_handler.setLevel(logging.WARNING) + logger = logging.getLogger(__name__) + logger.info("Debug mode disabled - console shows warnings/errors only") + else: + print("Warning: Could not find console handler to modify debug level") + + @staticmethod + def log_exception(logger: logging.Logger, message: str, exception: Exception) -> None: + """Log an exception with full traceback. + + Args: + logger: Logger instance to use + message: Descriptive message about the error + exception: Exception instance to log + """ + try: + logger.error(f"{message}: {type(exception).__name__}: {str(exception)}", exc_info=True) + except Exception: + # If logging the exception fails, at least print it + print(f"Logging failed - {message}: {type(exception).__name__}: {str(exception)}") + + @staticmethod + def get_log_file_path() -> Optional[Path]: + """Get the current log file path if file logging is enabled. + + Returns: + Optional[Path]: Path to log file, or None if only console logging + """ + try: + for handler in logging.getLogger().handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + return Path(handler.baseFilename) + return None + except Exception: + return None + + +def get_logger(name: str) -> logging.Logger: + """Convenience function to get a configured logger. + + Args: + name: Logger name (typically __name__) + + Returns: + logging.Logger: Configured logger instance + """ + return PackageHelperLogger.get_logger(name) + + +def get_log_info() -> dict: + """Get information about current logging configuration. + + Returns: + dict: Logging configuration information + """ + log_file = PackageHelperLogger.get_log_file_path() + return { + 'log_file': str(log_file) if log_file else None, + 'file_logging_enabled': log_file is not None, + 'log_level': logging.getLogger().level, + 'handler_count': len(logging.getLogger().handlers) + } \ No newline at end of file diff --git a/build/lib/archpkg/paths.py b/build/lib/archpkg/paths.py new file mode 100644 index 0000000..7e22565 --- /dev/null +++ b/build/lib/archpkg/paths.py @@ -0,0 +1,31 @@ +import os +import json +from pathlib import Path + +def get_config_dir() -> Path: + """Return the XDG-compliant config directory for archpkg.""" + config_home= Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) + return config_home / "archpkg" +def get_security_db_path()-> Path: + """Return the full path to the security database JSON file.""" + return get_config_dir() / "security-db.json" +def init_security_db(): + """Ensure the security DB directory and file exist.""" + db_path= get_security_db_path() + db_path.parent.mkdir(parents=True, exist_ok=True) + + if not db_path.exists(): + default_data={ + "last_updated": None, + "safe": ["yay","paru"], + "unsafe": ["malicoius-pkg"], + "reports": [] + } + with open(db_path, "w") as f: + json.dump(default_data, f, indent=2) + print(f"Created new security database at {db_path}") + else: + print(f"Security database already exists at {db_path}") + return db_path +if __name__== "__main__": + init_security_db() \ No newline at end of file diff --git a/build/lib/archpkg/security.py b/build/lib/archpkg/security.py new file mode 100644 index 0000000..96bd269 --- /dev/null +++ b/build/lib/archpkg/security.py @@ -0,0 +1,37 @@ +import json +from pathlib import Path +from .paths import get_security_db_path + +class SecurityDB: + def __init__(self): + self.db_path= get_security_db_path() + with open(self.db_path, "r") as f: + self.data= json.load(f) + def check_package(self, pkg_name: str): + if pkg_name in self.data.get("unsafe", []): + return "blocked" + elif pkg_name in self.data.get("safe",[]): + return "safe" + else: + return "not_verified" + def report_package(self,pkg_name: str, reason: str): + self.data.setdefault("reports", []).append({ + "package": pkg_name, + "reason": reason + }) + self.save() + def save(self): + with open(self.db_path, "w") as f: + json.dump(self.data,f, indent=2) + def update_security_db(self, remote_data= None): + """ + Update the local security DB. + remote_data: dict fetched from server + """ + if remote_data: + self.data.update(remote_data) + self.data["last_updated"]="2025-10-05" + self.save() + print("Security database updated from remote.") + else: + print("No remote data provided. Placeholder update.") \ No newline at end of file