commit 0add4e98be35b35293a347ed3530fe882432157f Author: spitkov Date: Sun Jan 5 14:22:47 2025 +0100 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8ec735 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Build directories +build/ +cmake-build-*/ + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Package manager specific +/var/cache/yns/ +/var/lib/yns/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8f392d9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.10) +project(yns VERSION 1.0) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find required packages +find_package(CURL REQUIRED) +find_package(nlohmann_json REQUIRED) + +# Add executable +add_executable(yns + src/main.cpp + src/package_manager.cpp +) + +# Include directories +target_include_directories(yns PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CURL_INCLUDE_DIRS} +) + +# Link libraries +target_link_libraries(yns PRIVATE + ${CURL_LIBRARIES} + nlohmann_json::nlohmann_json +) + +# Installation +install(TARGETS yns DESTINATION bin) + +# Create required directories during installation +install(CODE " + execute_process( + COMMAND mkdir -p /var/cache/yns + COMMAND mkdir -p /var/lib/yns + ) +") \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d62b08 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 YNS Package Manager + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6eee9b --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# YNS Package Manager + +A lightweight package manager for Linux that uses a single repository source. + +## Dependencies + +- CMake (>= 3.10) +- C++17 compiler +- libcurl +- nlohmann-json + +On Ubuntu/Debian, you can install the dependencies with: +```bash +sudo apt-get install build-essential cmake libcurl4-openssl-dev nlohmann-json3-dev +``` + +## Building + +```bash +mkdir build +cd build +cmake .. +make +``` + +## Installation + +After building, you can install the package manager with: +```bash +sudo make install +``` + +This will: +- Install the `yns` binary to `/usr/local/bin` +- Create necessary directories: + - `/var/cache/yns/` - For package cache + - `/var/lib/yns/` - For installed package database + +## Usage + +### Update package cache +```bash +yns update +``` + +### Install a package +```bash +yns install +``` + +### Remove a package +```bash +yns remove +``` + +### Upgrade a package +```bash +yns upgrade +``` + +### List all packages +```bash +yns list +``` + +### Interactive Mode +```bash +yns interactive +``` + +Interactive mode provides a shell-like interface where you can execute multiple commands without the `yns` prefix. Available commands in interactive mode: + +- `help` - Show available commands +- `update` - Update package cache +- `install ` - Install a package +- `remove ` - Remove a package +- `upgrade ` - Upgrade a package +- `list` - List all packages +- `clear` - Clear the screen +- `exit` - Exit interactive mode + +Example interactive session: +``` +$ yns interactive +YNS Package Manager Interactive Mode +Type 'help' for available commands or 'exit' to quit +yns> update +[100%] Package cache updated successfully +yns> list +Available packages: +================== +calc [installed 1.1, update available 2.0] +notepad [available 2.0] +yns> install notepad +[50%] Installing notepad@2.0 +notepad@2.0 installed successfully +yns> exit +Goodbye! +``` + +## Color Coding + +The package manager uses color-coded output for better visibility: + +🔵 Blue: +- Progress indicators (e.g., "[50%] Installing package...") +- Available but not installed packages in `yns list` +- Interactive mode prompt + +🟢 Green: +- Success messages +- Installed packages that are up to date in `yns list` +- Interactive mode welcome message + +🟡 Yellow: +- Installed packages with updates available in `yns list` + +🔴 Red: +- Error messages + +Example `yns list` output: +``` +Available packages: +================== +calc [installed 1.1, update available 2.0] # Yellow +notepad [available 2.0] # Blue +vim [installed 3.0] # Green +``` + +## Repository Structure + +The package manager uses a single repository source at: +`https://raw.githubusercontent.com/spitkov/ynsrepo/refs/heads/main/repo.json` + +The repository contains package information in JSON format, including: +- Package name +- Version +- Installation script URL +- Update script URL +- Remove script URL + +## Features + +- Colored terminal output +- Progress indicators +- Automatic package cache management +- Version tracking for installed packages +- Secure script execution +- Error handling and recovery +- Interactive shell mode \ No newline at end of file diff --git a/include/package_manager.hpp b/include/package_manager.hpp new file mode 100644 index 0000000..adf4056 --- /dev/null +++ b/include/package_manager.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +using json = nlohmann::json; + +class PackageManager { +public: + PackageManager(); + + bool update(); + bool install(const std::string& package_name); + bool remove(const std::string& package_name); + bool upgrade(const std::string& package_name); + bool list(); + bool interactive_mode(); + +private: + static constexpr const char* REPO_URL = "https://raw.githubusercontent.com/spitkov/ynsrepo/refs/heads/main/repo.json"; + static constexpr const char* CACHE_DIR = "/var/cache/yns/"; + static constexpr const char* CACHE_FILE = "/var/cache/yns/repo.json"; + static constexpr const char* INSTALLED_DB = "/var/lib/yns/installed.json"; + + bool download_file(const std::string& url, const std::string& output_path); + bool execute_script(const std::string& script_path); + bool cache_repo(); + json read_cache(); + json read_installed_db(); + void save_installed_db(const json& db); + void print_progress(const std::string& message, int percentage); + void print_error(const std::string& message); + void print_success(const std::string& message); + void print_interactive_help(); + + json repo_cache; + json installed_packages; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..84c7f55 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,96 @@ +#include "package_manager.hpp" +#include +#include +#include +#include + +void print_usage() { + std::cout << "Usage: yns [package_name]\n\n" + << "Commands:\n" + << " update Update package cache\n" + << " install Install a package\n" + << " remove Remove a package\n" + << " upgrade Upgrade a package\n" + << " list List all packages\n" + << " interactive Start interactive mode\n\n" + << "Interactive Mode:\n" + << " Run 'yns interactive' to enter interactive mode where you can\n" + << " execute multiple commands without prefix. Type 'help' in\n" + << " interactive mode for available commands.\n"; +} + +bool needs_sudo(const std::string& command) { + return command == "update" || command == "install" || + command == "remove" || command == "upgrade" || + command == "interactive" || command == "list"; +} + +std::string get_self_path() { + char path[1024]; + ssize_t count = readlink("/proc/self/exe", path, sizeof(path)); + return std::string(path, (count > 0) ? count : 0); +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + print_usage(); + return 1; + } + + std::string command = argv[1]; + + if (needs_sudo(command) && geteuid() != 0) { + std::string self = get_self_path(); + std::string sudo_cmd = "sudo " + self; + + for (int i = 1; i < argc; i++) { + sudo_cmd += " "; + sudo_cmd += argv[i]; + } + + std::cout << "This operation requires root privileges.\n"; + return system(sudo_cmd.c_str()); + } + + PackageManager pm; + + try { + if (command == "update") { + return pm.update() ? 0 : 1; + } + else if (command == "list") { + return pm.list() ? 0 : 1; + } + else if (command == "interactive") { + return pm.interactive_mode() ? 0 : 1; + } + else if (command == "install" || command == "remove" || command == "upgrade") { + if (argc < 3) { + std::cerr << "Error: Package name required for " << command << " command\n"; + print_usage(); + return 1; + } + + std::string package_name = argv[2]; + + if (command == "install") { + return pm.install(package_name) ? 0 : 1; + } + else if (command == "remove") { + return pm.remove(package_name) ? 0 : 1; + } + else { + return pm.upgrade(package_name) ? 0 : 1; + } + } + else { + std::cerr << "Error: Unknown command '" << command << "'\n"; + print_usage(); + return 1; + } + } + catch (const std::exception& e) { + std::cerr << "\033[31mError: " << e.what() << "\033[0m\n"; + return 1; + } +} \ No newline at end of file diff --git a/src/package_manager.cpp b/src/package_manager.cpp new file mode 100644 index 0000000..41ee5bf --- /dev/null +++ b/src/package_manager.cpp @@ -0,0 +1,380 @@ +#include "package_manager.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +const std::string RED = "\033[31m"; +const std::string GREEN = "\033[32m"; +const std::string BLUE = "\033[34m"; +const std::string YELLOW = "\033[33m"; +const std::string RESET = "\033[0m"; + +static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +PackageManager::PackageManager() { + fs::create_directories(CACHE_DIR); + fs::create_directories("/var/lib/yns"); + curl_global_init(CURL_GLOBAL_DEFAULT); + installed_packages = read_installed_db(); +} + +bool PackageManager::download_file(const std::string& url, const std::string& output_path) { + CURL* curl = curl_easy_init(); + if (!curl) return false; + + std::string response_data; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data); + + CURLcode res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) return false; + + std::ofstream output_file(output_path); + if (!output_file) return false; + + output_file << response_data; + return true; +} + +bool PackageManager::execute_script(const std::string& script_path) { + std::string cmd = "chmod +x " + script_path + " && " + script_path; + int exit_code = system(cmd.c_str()); + if (WIFEXITED(exit_code)) { + int status = WEXITSTATUS(exit_code); + if (status != 0) { + print_error("Script failed with exit code: " + std::to_string(status)); + return false; + } + return true; + } + print_error("Script terminated abnormally"); + return false; +} + +bool PackageManager::cache_repo() { + print_progress("Updating package cache", 0); + if (!download_file(REPO_URL, CACHE_FILE)) { + print_error("Failed to download repository data"); + return false; + } + print_progress("Updating package cache", 100); + print_success("Package cache updated successfully"); + return true; +} + +json PackageManager::read_cache() { + try { + std::ifstream cache_file(CACHE_FILE); + if (!cache_file) return json::object(); + return json::parse(cache_file); + } catch (...) { + return json::object(); + } +} + +json PackageManager::read_installed_db() { + try { + std::ifstream db_file(INSTALLED_DB); + if (!db_file) return json::object(); + return json::parse(db_file); + } catch (...) { + return json::object(); + } +} + +void PackageManager::save_installed_db(const json& db) { + std::ofstream db_file(INSTALLED_DB); + db_file << db.dump(4); +} + +void PackageManager::print_progress(const std::string& message, int percentage) { + std::cout << BLUE << "[" << percentage << "%] " << message << RESET << std::endl; +} + +void PackageManager::print_error(const std::string& message) { + std::cerr << RED << "Error: " << message << RESET << std::endl; +} + +void PackageManager::print_success(const std::string& message) { + std::cout << GREEN << message << RESET << std::endl; +} + +bool PackageManager::update() { + return cache_repo(); +} + +bool PackageManager::install(const std::string& package_name) { + if (!update()) return false; + + repo_cache = read_cache(); + + if (!repo_cache["packages"].contains(package_name)) { + print_error("Package '" + package_name + "' not found"); + return false; + } + + auto package = repo_cache["packages"][package_name]; + std::string repo_version = package["version"]; + + if (installed_packages.contains(package_name)) { + std::string installed_version = installed_packages[package_name]["version"]; + if (installed_version == repo_version) { + print_success(package_name + " is already installed (version " + installed_version + ")"); + return true; + } else { + print_progress("New version available: " + repo_version + " (currently installed: " + installed_version + ")", 0); + std::cout << "Would you like to upgrade? [y/N] "; + std::string response; + std::getline(std::cin, response); + if (response == "y" || response == "Y") { + return upgrade(package_name); + } else { + print_success("Keeping current version " + installed_version); + return true; + } + } + } + + std::string install_script = package["install"]; + std::string temp_script = "/tmp/yns_install_" + package_name + ".sh"; + print_progress("Downloading installation script", 0); + + if (!download_file(install_script, temp_script)) { + print_error("Failed to download installation script"); + return false; + } + + print_progress("Installing " + package_name + "@" + repo_version, 50); + + std::string cmd = "chmod +x " + temp_script + " && " + temp_script; + int exit_code = system(cmd.c_str()); + fs::remove(temp_script); + + if (!WIFEXITED(exit_code)) { + print_error("Installation script terminated abnormally"); + return false; + } + + int status = WEXITSTATUS(exit_code); + if (status != 0) { + print_error("Installation failed with exit code: " + std::to_string(status)); + return false; + } + + installed_packages[package_name] = { + {"version", repo_version} + }; + save_installed_db(installed_packages); + + print_success(package_name + "@" + repo_version + " installed successfully"); + return true; +} + +bool PackageManager::remove(const std::string& package_name) { + if (!installed_packages.contains(package_name)) { + print_error("Package '" + package_name + "' is not installed"); + return false; + } + + repo_cache = read_cache(); + if (!repo_cache["packages"].contains(package_name)) { + print_error("Package information not found in repository"); + return false; + } + + std::string remove_script = repo_cache["packages"][package_name]["remove"]; + std::string temp_script = "/tmp/yns_remove_" + package_name + ".sh"; + + print_progress("Downloading removal script", 0); + if (!download_file(remove_script, temp_script)) { + print_error("Failed to download removal script"); + return false; + } + + print_progress("Removing " + package_name, 50); + + std::string cmd = "chmod +x " + temp_script + " && " + temp_script; + int exit_code = system(cmd.c_str()); + fs::remove(temp_script); + + if (!WIFEXITED(exit_code)) { + print_error("Removal script terminated abnormally"); + return false; + } + + int status = WEXITSTATUS(exit_code); + if (status != 0) { + print_error("Removal failed with exit code: " + std::to_string(status)); + return false; + } + + installed_packages.erase(package_name); + save_installed_db(installed_packages); + + print_success(package_name + " removed successfully"); + return true; +} + +bool PackageManager::upgrade(const std::string& package_name) { + if (!installed_packages.contains(package_name)) { + print_error("Package '" + package_name + "' is not installed"); + return false; + } + + if (!update()) return false; + + repo_cache = read_cache(); + if (!repo_cache["packages"].contains(package_name)) { + print_error("Package information not found in repository"); + return false; + } + + std::string installed_version = installed_packages[package_name]["version"]; + std::string repo_version = repo_cache["packages"][package_name]["version"]; + + if (installed_version == repo_version) { + print_success(package_name + " is already up to date (" + installed_version + ")"); + return true; + } + + std::string update_script = repo_cache["packages"][package_name]["update"]; + std::string temp_script = "/tmp/yns_update_" + package_name + ".sh"; + + print_progress("Downloading update script", 0); + if (!download_file(update_script, temp_script)) { + print_error("Failed to download update script"); + return false; + } + + print_progress("Updating " + package_name + " from " + installed_version + " to " + repo_version, 50); + + std::string cmd = "chmod +x " + temp_script + " && " + temp_script; + int exit_code = system(cmd.c_str()); + fs::remove(temp_script); + + if (!WIFEXITED(exit_code)) { + print_error("Update script terminated abnormally"); + return false; + } + + int status = WEXITSTATUS(exit_code); + if (status != 0) { + print_error("Update failed with exit code: " + std::to_string(status)); + return false; + } + + installed_packages[package_name]["version"] = repo_version; + save_installed_db(installed_packages); + + print_success(package_name + " updated to version " + repo_version); + return true; +} + +bool PackageManager::list() { + if (!update()) return false; + + repo_cache = read_cache(); + + std::cout << "\nAvailable packages:\n"; + std::cout << "==================\n"; + + for (const auto& [name, package] : repo_cache["packages"].items()) { + std::string repo_version = package["version"]; + std::string status; + + if (installed_packages.contains(name)) { + std::string installed_version = installed_packages[name]["version"]; + if (installed_version == repo_version) { + status = GREEN + "[installed " + installed_version + "]" + RESET; + } else { + status = YELLOW + "[installed " + installed_version + ", update available " + repo_version + "]" + RESET; + } + } else { + status = BLUE + "[available " + repo_version + "]" + RESET; + } + + std::cout << name << " " << status << "\n"; + } + + return true; +} + +void PackageManager::print_interactive_help() { + std::cout << "\nAvailable commands:\n" + << " help Show this help message\n" + << " update Update package cache\n" + << " install Install a package\n" + << " remove Remove a package\n" + << " upgrade Upgrade a package\n" + << " list List all packages\n" + << " clear Clear the screen\n" + << " exit Exit interactive mode\n\n"; +} + +bool PackageManager::interactive_mode() { + std::cout << GREEN << "YNS Package Manager Interactive Mode\n" << RESET; + std::cout << "Type 'help' for available commands or 'exit' to quit\n"; + + std::string line; + while (true) { + std::cout << BLUE << "yns> " << RESET; + if (!std::getline(std::cin, line) || line == "exit") { + std::cout << "Goodbye!\n"; + break; + } + + if (line.empty()) { + continue; + } + + std::istringstream iss(line); + std::string command; + iss >> command; + + if (command == "help") { + print_interactive_help(); + } + else if (command == "update") { + update(); + } + else if (command == "list") { + list(); + } + else if (command == "clear") { + system("clear"); + } + else if (command == "install" || command == "remove" || command == "upgrade") { + std::string package_name; + if (!(iss >> package_name)) { + print_error("Package name required for " + command + " command"); + continue; + } + + if (command == "install") { + install(package_name); + } + else if (command == "remove") { + remove(package_name); + } + else { + upgrade(package_name); + } + } + else { + print_error("Unknown command '" + command + "'. Type 'help' for available commands."); + } + } + return true; +} \ No newline at end of file