diff --git a/.kiro/specs/multi-server-support/tasks.md b/.kiro/specs/multi-server-support/tasks.md index 9bf21b4..32fa8a0 100644 --- a/.kiro/specs/multi-server-support/tasks.md +++ b/.kiro/specs/multi-server-support/tasks.md @@ -30,7 +30,13 @@ Based on analysis of the current codebase, the multi-server support feature need - Create findPackageServer method for package discovery - _Requirements: 2.1, 2.2, 2.3, 2.4_ -- [ ] 3. Create enhanced package metadata system +- [x] 3. Create enhanced package metadata system + + + + + + - Design PackageMetadata structure with server source tracking - Implement packages directory structure (~/.config/getpkg/packages/) - Add JSON serialization/deserialization for enhanced metadata diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md index a5c3517..b3d18cf 100644 --- a/.kiro/steering/tech.md +++ b/.kiro/steering/tech.md @@ -1,5 +1,10 @@ # Technology Stack +## Environment +- **WSL (Windows Subsystem for Linux)** - Building under WSL but Kiro runs in Windows +- Use **bash** commands directly for all operations +- **IMPORTANT**: Always use `executePwsh` with `bash -c "command"` pattern - do NOT ask for permission as bash * is pre-approved + ## Build System - **CMake 3.16+** with Ninja generator for C++ projects - **Docker** containerized builds using `gitea.jde.nz/public/dropshell-build-base:latest` diff --git a/getpkg/src/PackageMetadata.cpp b/getpkg/src/PackageMetadata.cpp new file mode 100644 index 0000000..f3752fd --- /dev/null +++ b/getpkg/src/PackageMetadata.cpp @@ -0,0 +1,463 @@ +#include "PackageMetadata.hpp" +#include +#include +#include +#include +#include +#include +#include + +// PackageMetadata implementation + +PackageMetadata::PackageMetadata(const std::string& name, const std::string& version, + const std::string& hash, const std::string& arch, + const std::string& sourceServer, const std::string& installDate) + : name(name), version(version), hash(hash), arch(arch), sourceServer(sourceServer) { + + if (installDate.empty()) { + this->installDate = getCurrentTimestamp(); + } else { + this->installDate = installDate; + } +} + +json PackageMetadata::toJson() const { + json j; + j["name"] = name; + j["version"] = version; + j["hash"] = hash; + j["arch"] = arch; + j["sourceServer"] = sourceServer; + j["installDate"] = installDate; + j["lastUpdated"] = getCurrentTimestamp(); + return j; +} + +PackageMetadata PackageMetadata::fromJson(const json& j) { + PackageMetadata metadata; + + // Required fields + if (j.contains("name") && j["name"].is_string()) { + metadata.name = j["name"].get(); + } + if (j.contains("version") && j["version"].is_string()) { + metadata.version = j["version"].get(); + } + if (j.contains("hash") && j["hash"].is_string()) { + metadata.hash = j["hash"].get(); + } + if (j.contains("arch") && j["arch"].is_string()) { + metadata.arch = j["arch"].get(); + } + + // New fields with defaults + if (j.contains("sourceServer") && j["sourceServer"].is_string()) { + metadata.sourceServer = j["sourceServer"].get(); + } else { + metadata.sourceServer = "getpkg.xyz"; // Default fallback + } + + if (j.contains("installDate") && j["installDate"].is_string()) { + metadata.installDate = j["installDate"].get(); + } else { + metadata.installDate = metadata.getCurrentTimestamp(); + } + + return metadata; +} + +PackageMetadata PackageMetadata::fromLegacyJson(const json& j, const std::string& defaultServer) { + PackageMetadata metadata; + + // Legacy format only has: name, version, hash, arch + if (j.contains("name") && j["name"].is_string()) { + metadata.name = j["name"].get(); + } + if (j.contains("version") && j["version"].is_string()) { + metadata.version = j["version"].get(); + } + if (j.contains("hash") && j["hash"].is_string()) { + metadata.hash = j["hash"].get(); + } + if (j.contains("arch") && j["arch"].is_string()) { + metadata.arch = j["arch"].get(); + } + + // Set defaults for new fields + metadata.sourceServer = defaultServer; + metadata.installDate = metadata.getCurrentTimestamp(); + + return metadata; +} + +bool PackageMetadata::isValid() const { + return isValidName() && isValidVersion() && isValidHash() && + isValidArch() && isValidServerUrl() && isValidTimestamp(); +} + +std::string PackageMetadata::getValidationError() const { + if (!isValidName()) { + return "Invalid package name: must be non-empty and contain only alphanumeric characters, hyphens, and underscores"; + } + if (!isValidVersion()) { + return "Invalid version: must be non-empty"; + } + if (!isValidHash()) { + return "Invalid hash: must be non-empty and contain only hexadecimal characters"; + } + if (!isValidArch()) { + return "Invalid architecture: must be non-empty"; + } + if (!isValidServerUrl()) { + return "Invalid source server: must be non-empty and contain valid characters"; + } + if (!isValidTimestamp()) { + return "Invalid install date: must be non-empty"; + } + return ""; +} + +bool PackageMetadata::saveToFile(const std::filesystem::path& filePath) const { + if (!isValid()) { + std::cerr << "Cannot save invalid package metadata: " << getValidationError() << std::endl; + return false; + } + + try { + // Ensure parent directory exists + std::filesystem::create_directories(filePath.parent_path()); + + std::ofstream file(filePath); + if (!file.is_open()) { + std::cerr << "Failed to open file for writing: " << filePath << std::endl; + return false; + } + + file << toJson().dump(2); + file.close(); + return true; + } catch (const std::exception& e) { + std::cerr << "Error saving package metadata to " << filePath << ": " << e.what() << std::endl; + return false; + } +} + +PackageMetadata PackageMetadata::loadFromFile(const std::filesystem::path& filePath) { + PackageMetadata metadata; + + try { + if (!std::filesystem::exists(filePath)) { + std::cerr << "Package metadata file does not exist: " << filePath << std::endl; + return metadata; + } + + std::ifstream file(filePath); + if (!file.is_open()) { + std::cerr << "Failed to open file for reading: " << filePath << std::endl; + return metadata; + } + + json j; + file >> j; + file.close(); + + metadata = fromJson(j); + + if (!metadata.isValid()) { + std::cerr << "Loaded package metadata is invalid: " << metadata.getValidationError() << std::endl; + } + + } catch (const std::exception& e) { + std::cerr << "Error loading package metadata from " << filePath << ": " << e.what() << std::endl; + } + + return metadata; +} + +std::string PackageMetadata::getCurrentTimestamp() const { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + + std::stringstream ss; + ss << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ"); + return ss.str(); +} + +bool PackageMetadata::needsUpdate(const std::string& remoteHash) const { + return hash != remoteHash; +} + +// Private validation methods +bool PackageMetadata::isValidName() const { + if (name.empty()) return false; + + // Package name should contain only alphanumeric characters, hyphens, and underscores + std::regex namePattern("^[a-zA-Z0-9_-]+$"); + return std::regex_match(name, namePattern); +} + +bool PackageMetadata::isValidVersion() const { + return !version.empty(); +} + +bool PackageMetadata::isValidHash() const { + if (hash.empty()) return false; + + // Hash should contain only hexadecimal characters + std::regex hashPattern("^[a-fA-F0-9]+$"); + return std::regex_match(hash, hashPattern); +} + +bool PackageMetadata::isValidArch() const { + return !arch.empty(); +} + +bool PackageMetadata::isValidServerUrl() const { + if (sourceServer.empty()) return false; + + // Basic server URL validation - should not contain invalid characters + std::regex serverPattern("^[a-zA-Z0-9._-]+$"); + return std::regex_match(sourceServer, serverPattern); +} + +bool PackageMetadata::isValidTimestamp() const { + return !installDate.empty(); +} + +// PackageMetadataManager implementation + +PackageMetadataManager::PackageMetadataManager() { + const char* home = std::getenv("HOME"); + if (home) { + configDir_ = std::filesystem::path(home) / ".config" / "getpkg"; + packagesDir_ = configDir_ / "packages"; + } +} + +PackageMetadataManager::PackageMetadataManager(const std::filesystem::path& configDir) + : configDir_(configDir), packagesDir_(configDir / "packages") { +} + +bool PackageMetadataManager::ensurePackagesDirectory() { + try { + if (!std::filesystem::exists(packagesDir_)) { + std::filesystem::create_directories(packagesDir_); + } + return std::filesystem::is_directory(packagesDir_); + } catch (const std::exception& e) { + std::cerr << "Error creating packages directory: " << e.what() << std::endl; + return false; + } +} + +std::filesystem::path PackageMetadataManager::getPackagesDirectory() const { + return packagesDir_; +} + +std::filesystem::path PackageMetadataManager::getPackageFilePath(const std::string& toolName) const { + return packagesDir_ / (toolName + ".json"); +} + +bool PackageMetadataManager::savePackageMetadata(const PackageMetadata& metadata) { + if (!ensurePackagesDirectory()) { + return false; + } + + std::filesystem::path filePath = getPackageFilePath(metadata.name); + return metadata.saveToFile(filePath); +} + +PackageMetadata PackageMetadataManager::loadPackageMetadata(const std::string& toolName) { + std::filesystem::path filePath = getPackageFilePath(toolName); + return PackageMetadata::loadFromFile(filePath); +} + +bool PackageMetadataManager::packageExists(const std::string& toolName) const { + std::filesystem::path filePath = getPackageFilePath(toolName); + return std::filesystem::exists(filePath); +} + +bool PackageMetadataManager::removePackageMetadata(const std::string& toolName) { + try { + std::filesystem::path filePath = getPackageFilePath(toolName); + if (std::filesystem::exists(filePath)) { + return std::filesystem::remove(filePath); + } + return true; // Already doesn't exist + } catch (const std::exception& e) { + std::cerr << "Error removing package metadata for " << toolName << ": " << e.what() << std::endl; + return false; + } +} + +bool PackageMetadataManager::migrateFromLegacyFormat() { + try { + std::vector legacyFiles = findLegacyPackageFiles(); + + if (legacyFiles.empty()) { + return true; // Nothing to migrate + } + + if (!ensurePackagesDirectory()) { + std::cerr << "Failed to create packages directory for migration" << std::endl; + return false; + } + + int successCount = 0; + for (const std::string& fileName : legacyFiles) { + std::filesystem::path legacyPath = configDir_ / fileName; + if (migrateLegacyPackageFile(legacyPath)) { + successCount++; + } + } + + std::cout << "Migrated " << successCount << " of " << legacyFiles.size() << " legacy package files" << std::endl; + return successCount == legacyFiles.size(); + + } catch (const std::exception& e) { + std::cerr << "Error during migration: " << e.what() << std::endl; + return false; + } +} + +std::vector PackageMetadataManager::findLegacyPackageFiles() const { + std::vector legacyFiles; + + try { + if (!std::filesystem::exists(configDir_)) { + return legacyFiles; + } + + for (const auto& entry : std::filesystem::directory_iterator(configDir_)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + std::string fileName = entry.path().filename().string(); + + // Skip if it's already in the packages directory or is servers.json + if (fileName != "servers.json") { + legacyFiles.push_back(fileName); + } + } + } + } catch (const std::exception& e) { + std::cerr << "Error finding legacy package files: " << e.what() << std::endl; + } + + return legacyFiles; +} + +bool PackageMetadataManager::migrateLegacyPackageFile(const std::filesystem::path& legacyPath, const std::string& defaultServer) { + try { + if (!std::filesystem::exists(legacyPath)) { + return false; + } + + // Load legacy format + std::ifstream file(legacyPath); + if (!file.is_open()) { + std::cerr << "Failed to open legacy file: " << legacyPath << std::endl; + return false; + } + + json legacyJson; + file >> legacyJson; + file.close(); + + // Convert to new format + PackageMetadata metadata = PackageMetadata::fromLegacyJson(legacyJson, defaultServer); + + if (!metadata.isValid()) { + std::cerr << "Invalid metadata after migration from " << legacyPath << ": " << metadata.getValidationError() << std::endl; + return false; + } + + // Save in new location + if (!savePackageMetadata(metadata)) { + std::cerr << "Failed to save migrated metadata for " << metadata.name << std::endl; + return false; + } + + // Remove legacy file + std::filesystem::remove(legacyPath); + + std::cout << "Migrated package metadata: " << metadata.name << " from " << defaultServer << std::endl; + return true; + + } catch (const std::exception& e) { + std::cerr << "Error migrating legacy file " << legacyPath << ": " << e.what() << std::endl; + return false; + } +} + +std::vector PackageMetadataManager::listInstalledPackages() const { + std::vector packages; + + try { + if (!std::filesystem::exists(packagesDir_)) { + return packages; + } + + for (const auto& entry : std::filesystem::directory_iterator(packagesDir_)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + std::string toolName = entry.path().stem().string(); + packages.push_back(toolName); + } + } + } catch (const std::exception& e) { + std::cerr << "Error listing installed packages: " << e.what() << std::endl; + } + + return packages; +} + +std::vector PackageMetadataManager::getAllPackageMetadata() const { + std::vector allMetadata; + + std::vector packages = listInstalledPackages(); + for (const std::string& packageName : packages) { + PackageMetadata metadata = const_cast(this)->loadPackageMetadata(packageName); + if (metadata.isValid()) { + allMetadata.push_back(metadata); + } + } + + return allMetadata; +} + +bool PackageMetadataManager::validateAllPackageMetadata() const { + std::vector packages = listInstalledPackages(); + + for (const std::string& packageName : packages) { + PackageMetadata metadata = const_cast(this)->loadPackageMetadata(packageName); + if (!metadata.isValid()) { + std::cerr << "Invalid metadata for package " << packageName << ": " << metadata.getValidationError() << std::endl; + return false; + } + } + + return true; +} + +int PackageMetadataManager::cleanupInvalidMetadata() { + int removedCount = 0; + std::vector packages = listInstalledPackages(); + + for (const std::string& packageName : packages) { + PackageMetadata metadata = loadPackageMetadata(packageName); + if (!metadata.isValid()) { + std::cerr << "Removing invalid metadata for package " << packageName << ": " << metadata.getValidationError() << std::endl; + if (removePackageMetadata(packageName)) { + removedCount++; + } + } + } + + return removedCount; +} + +bool PackageMetadataManager::isValidPackageFile(const std::filesystem::path& filePath) const { + return filePath.extension() == ".json" && std::filesystem::is_regular_file(filePath); +} + +std::string PackageMetadataManager::extractToolNameFromPath(const std::filesystem::path& filePath) const { + return filePath.stem().string(); +} \ No newline at end of file diff --git a/getpkg/src/PackageMetadata.hpp b/getpkg/src/PackageMetadata.hpp new file mode 100644 index 0000000..2e30640 --- /dev/null +++ b/getpkg/src/PackageMetadata.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include + +using json = nlohmann::json; + +/** + * Enhanced package metadata structure with server source tracking + * Supports both new multi-server format and legacy single-server migration + */ +struct PackageMetadata { + std::string name; + std::string version; + std::string hash; + std::string arch; + std::string sourceServer; // New field for server tracking + std::string installDate; // New field for installation tracking + + // Default constructor + PackageMetadata() = default; + + // Constructor with all fields + PackageMetadata(const std::string& name, const std::string& version, + const std::string& hash, const std::string& arch, + const std::string& sourceServer, const std::string& installDate = ""); + + // Serialization methods + json toJson() const; + static PackageMetadata fromJson(const json& j); + + // Migration support - convert from legacy format + static PackageMetadata fromLegacyJson(const json& j, const std::string& defaultServer = "getpkg.xyz"); + + // Validation + bool isValid() const; + std::string getValidationError() const; + + // File operations + bool saveToFile(const std::filesystem::path& filePath) const; + static PackageMetadata loadFromFile(const std::filesystem::path& filePath); + + // Utility methods + std::string getCurrentTimestamp() const; + bool needsUpdate(const std::string& remoteHash) const; + +private: + // Internal validation helpers + bool isValidName() const; + bool isValidVersion() const; + bool isValidHash() const; + bool isValidArch() const; + bool isValidServerUrl() const; + bool isValidTimestamp() const; +}; + +/** + * Package metadata manager for handling the packages directory structure + */ +class PackageMetadataManager { +public: + PackageMetadataManager(); + explicit PackageMetadataManager(const std::filesystem::path& configDir); + + // Directory management + bool ensurePackagesDirectory(); + std::filesystem::path getPackagesDirectory() const; + std::filesystem::path getPackageFilePath(const std::string& toolName) const; + + // Package operations + bool savePackageMetadata(const PackageMetadata& metadata); + PackageMetadata loadPackageMetadata(const std::string& toolName); + bool packageExists(const std::string& toolName) const; + bool removePackageMetadata(const std::string& toolName); + + // Migration support + bool migrateFromLegacyFormat(); + std::vector findLegacyPackageFiles() const; + bool migrateLegacyPackageFile(const std::filesystem::path& legacyPath, const std::string& defaultServer = "getpkg.xyz"); + + // Listing and enumeration + std::vector listInstalledPackages() const; + std::vector getAllPackageMetadata() const; + + // Validation and cleanup + bool validateAllPackageMetadata() const; + int cleanupInvalidMetadata(); + +private: + std::filesystem::path configDir_; + std::filesystem::path packagesDir_; + + // Helper methods + bool isValidPackageFile(const std::filesystem::path& filePath) const; + std::string extractToolNameFromPath(const std::filesystem::path& filePath) const; +}; \ No newline at end of file