diff --git a/getpkg/src/GetbinClient.cpp b/getpkg/src/GetbinClient.cpp index dd60d49..3632005 100644 --- a/getpkg/src/GetbinClient.cpp +++ b/getpkg/src/GetbinClient.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include using json = nlohmann::json; @@ -376,4 +378,124 @@ bool GetbinClient::deleteObject(const std::string& hash, const std::string& toke worker.join(); return success; -} \ No newline at end of file +} +bool GetbinClient::listPackages(std::vector& outPackages) { + outPackages.clear(); + + // Set up SSL configuration + std::string ca_path = find_ca_certificates(); + if (!ca_path.empty()) { + std::cout << "[GetbinClient] Found CA certificates at: " << ca_path << std::endl; + } + + bool success = false; + bool done = false; + std::mutex mtx; + std::condition_variable cv; + + std::thread worker([&]() { + trantor::EventLoop loop; + + auto client = drogon::HttpClient::newHttpClient("https://" + std::string(SERVER_HOST), &loop, true, false); + if (!ca_path.empty()) { + std::vector> sslConfigs; + sslConfigs.push_back({"VerifyCAFile", ca_path}); + client->addSSLConfigs(sslConfigs); + } + + auto req = drogon::HttpRequest::newHttpRequest(); + req->setMethod(drogon::Get); + req->setPath("/dir"); + + client->sendRequest(req, [&](drogon::ReqResult result, const drogon::HttpResponsePtr& response) { + if (result == drogon::ReqResult::Ok) { + int status_code = response->getStatusCode(); + std::string response_body = std::string(response->getBody()); + + if (status_code == 200) { + try { + json json_response = json::parse(response_body); + + if (json_response.contains("entries") && json_response["entries"].is_array()) { + for (const auto& entry : json_response["entries"]) { + if (entry.contains("labeltags") && entry["labeltags"].is_array()) { + for (const auto& labeltag : entry["labeltags"]) { + if (labeltag.is_string()) { + std::string name = labeltag.get(); + // Extract tool name (remove architecture suffix if present) + size_t colon_pos = name.find(":"); + if (colon_pos != std::string::npos) { + name = name.substr(0, colon_pos); + } + + // Skip empty names + if (name.empty()) continue; + + // Add to list if not already present + if (std::find(outPackages.begin(), outPackages.end(), name) == outPackages.end()) { + outPackages.push_back(name); + } + } + } + } + } + success = true; + } + } catch (const std::exception& e) { + std::cerr << "[GetbinClient::listPackages] JSON parse error: " << e.what() << std::endl; + } + } else { + std::cerr << "[GetbinClient::listPackages] HTTP error: status code " << status_code << std::endl; + } + } else { + std::cerr << "[GetbinClient::listPackages] HTTP request failed." << std::endl; + } + done = true; + cv.notify_one(); + loop.quit(); + }, 10.0); + + loop.loop(); + }); + + // Wait for completion + { + std::unique_lock lock(mtx); + cv.wait(lock, [&] { return done; }); + } + + worker.join(); + + // Filter out duplicates where we have both toolname and toolname-noarch + // Keep the base name and remove the -noarch variant + std::vector filteredPackages; + std::set baseNames; + + // First pass: collect all base names (without -noarch) + for (const auto& pkg : outPackages) { + const std::string suffix = "-noarch"; + if (pkg.length() < suffix.length() || pkg.substr(pkg.length() - suffix.length()) != suffix) { + baseNames.insert(pkg); + } + } + + // Second pass: add packages, skipping -noarch variants if base exists + for (const auto& pkg : outPackages) { + const std::string suffix = "-noarch"; + if (pkg.length() >= suffix.length() && pkg.substr(pkg.length() - suffix.length()) == suffix) { + std::string baseName = pkg.substr(0, pkg.length() - suffix.length()); + if (baseNames.find(baseName) == baseNames.end()) { + filteredPackages.push_back(pkg); // Keep -noarch only if no base version + } + } else { + filteredPackages.push_back(pkg); // Always keep base versions + } + } + + outPackages = std::move(filteredPackages); + + // Sort the packages for better display + std::sort(outPackages.begin(), outPackages.end()); + + return success; +} diff --git a/getpkg/src/GetbinClient.hpp b/getpkg/src/GetbinClient.hpp index 0bd6845..9bd2a56 100644 --- a/getpkg/src/GetbinClient.hpp +++ b/getpkg/src/GetbinClient.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include class GetbinClient { public: @@ -8,4 +9,5 @@ public: bool upload(const std::string& archivePath, std::string& outUrl, std::string& outHash, const std::string& token); bool getHash(const std::string& toolName, const std::string& arch, std::string& outHash); bool deleteObject(const std::string& hash, const std::string& token); + bool listPackages(std::vector& outPackages); }; \ No newline at end of file diff --git a/getpkg/src/main.cpp b/getpkg/src/main.cpp index ccc28e7..df3af0b 100644 --- a/getpkg/src/main.cpp +++ b/getpkg/src/main.cpp @@ -63,9 +63,11 @@ #include #include #include +#include #include #include #include +#include #include namespace { @@ -486,6 +488,109 @@ int unpublish_tool(int argc, char* argv[]) { } } +int list_packages(int argc, char* argv[]) { + GetbinClient getbin; + std::vector availablePackages; + + std::cout << "Fetching package list from getpkg.xyz..." << std::endl; + if (!getbin.listPackages(availablePackages)) { + std::cerr << "Failed to fetch package list from server." << std::endl; + return 1; + } + + if (availablePackages.empty()) { + std::cout << "No packages found on server." << std::endl; + return 0; + } + + // Get local installation status + std::string home = get_home(); + std::filesystem::path configDir = std::filesystem::path(home) / ".config/getpkg"; + std::filesystem::path getpkgDir = std::filesystem::path(home) / ".getpkg"; + + // Build list of installed packages with their info + std::map installedPackages; + if (std::filesystem::exists(configDir)) { + for (const auto& entry : std::filesystem::directory_iterator(configDir)) { + if (entry.path().extension() == ".json") { + std::string toolName = entry.path().stem().string(); + try { + std::ifstream configFile(entry.path()); + json toolConfig; + configFile >> toolConfig; + installedPackages[toolName] = toolConfig; + } catch (...) { + // Skip malformed config files + } + } + } + } + + // Print header + std::cout << std::endl; + std::cout << "+" << std::string(25, '-') << "+" << std::string(12, '-') << "+" << std::string(18, '-') << "+" << std::string(10, '-') << "+" << std::endl; + std::cout << "|" << std::setw(25) << std::left << " Package" + << "|" << std::setw(12) << std::left << " Status" + << "|" << std::setw(18) << std::left << " Local Version" + << "|" << std::setw(10) << std::left << " Latest" + << "|" << std::endl; + std::cout << "+" << std::string(25, '-') << "+" << std::string(12, '-') << "+" << std::string(18, '-') << "+" << std::string(10, '-') << "+" << std::endl; + + // Process each package + for (const auto& packageName : availablePackages) { + std::string status = "Available"; + std::string localVersion = "-"; + std::string remoteStatus = "✓"; + + auto it = installedPackages.find(packageName); + if (it != installedPackages.end()) { + // Package is installed + json& config = it->second; + localVersion = config.value("version", "unknown"); + std::string localHash = config.value("hash", ""); + std::string arch = config.value("arch", get_arch()); + + // Check if it's up to date by comparing hashes + std::string remoteHash; + if (getbin.getHash(packageName, arch, remoteHash) && !remoteHash.empty()) { + if (localHash == remoteHash) { + status = "✓ Latest"; + remoteStatus = "✓"; + } else { + status = "⚡ Update"; + remoteStatus = "⚡"; + } + } else { + status = "✓ Local"; + remoteStatus = "?"; + } + } + + // Print row + std::cout << "|" << std::setw(25) << std::left << (" " + packageName).substr(0, 24) + << "|" << std::setw(12) << std::left << (" " + status).substr(0, 11) + << "|" << std::setw(18) << std::left << (" " + localVersion).substr(0, 17) + << "|" << std::setw(10) << std::left << (" " + remoteStatus).substr(0, 9) + << "|" << std::endl; + } + + // Print footer + std::cout << "+" << std::string(25, '-') << "+" << std::string(12, '-') << "+" << std::string(18, '-') << "+" << std::string(10, '-') << "+" << std::endl; + std::cout << std::endl; + + // Print legend + std::cout << "Legend:" << std::endl; + std::cout << " Available - Package available for installation" << std::endl; + std::cout << " ✓ Latest - Installed and up to date" << std::endl; + std::cout << " ⚡ Update - Installed but update available" << std::endl; + std::cout << " ✓ Local - Installed (remote status unknown)" << std::endl; + std::cout << std::endl; + std::cout << "Total packages: " << availablePackages.size() + << ", Installed: " << installedPackages.size() << std::endl; + + return 0; +} + int clean_tool(int argc, char* argv[]) { std::string home = get_home(); std::filesystem::path getpkgDir = std::filesystem::path(home) / ".getpkg"; @@ -699,6 +804,9 @@ void show_help() { std::cout << " hash Calculate hash of file or directory" << std::endl; std::cout << " Outputs raw hash value to stdout" << std::endl; std::cout << std::endl; + std::cout << " list List all available packages" << std::endl; + std::cout << " Shows installation status and version information" << std::endl; + std::cout << std::endl; std::cout << " clean Clean up orphaned configs and symlinks" << std::endl; std::cout << " Removes unused config files and dangling symlinks" << std::endl; std::cout << std::endl; @@ -752,6 +860,7 @@ update version create hash +list clean help )"; @@ -761,6 +870,8 @@ help return create_tool(argc, argv); } else if (command == "hash") { return hash_command(argc, argv); + } else if (command == "list") { + return list_packages(argc, argv); } else if (command == "clean") { return clean_tool(argc, argv); } else if (command == "help") {