Compare commits
3 Commits
v2025.0720
...
v2025.0720
Author | SHA1 | Date | |
---|---|---|---|
187f1a250d | |||
52d8e5b95e | |||
bfeaf4d0db |
325
.kiro/specs/multi-server-support/design.md
Normal file
325
.kiro/specs/multi-server-support/design.md
Normal file
@ -0,0 +1,325 @@
|
||||
# Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
This design extends getpkg to support multiple package servers while maintaining full backward compatibility. The solution introduces a server configuration system, updates the client architecture to handle multiple servers, and reorganizes package metadata storage. The design prioritizes minimal disruption to existing functionality while providing powerful multi-server capabilities.
|
||||
|
||||
## Architecture
|
||||
|
||||
### High-Level Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
CLI[CLI Commands] --> SM[ServerManager]
|
||||
CLI --> PM[PackageManager]
|
||||
PM --> SM
|
||||
PM --> GC[GetbinClient]
|
||||
SM --> CF[servers.json]
|
||||
PM --> PF[packages/*.json]
|
||||
GC --> S1[Server 1]
|
||||
GC --> S2[Server 2]
|
||||
GC --> SN[Server N]
|
||||
```
|
||||
|
||||
### Server Management Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CLI
|
||||
participant ServerManager
|
||||
participant Config
|
||||
|
||||
User->>CLI: getpkg server add example.com
|
||||
CLI->>ServerManager: addServer("example.com")
|
||||
ServerManager->>Config: load servers.json
|
||||
ServerManager->>ServerManager: validate URL
|
||||
ServerManager->>Config: save updated servers.json
|
||||
ServerManager->>CLI: success confirmation
|
||||
CLI->>User: Server added successfully
|
||||
```
|
||||
|
||||
### Package Installation Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CLI
|
||||
participant PackageManager
|
||||
participant GetbinClient
|
||||
participant Server1
|
||||
participant Server2
|
||||
|
||||
User->>CLI: getpkg install tool
|
||||
CLI->>PackageManager: install("tool")
|
||||
PackageManager->>GetbinClient: download("tool", servers[0])
|
||||
GetbinClient->>Server1: GET /object/tool:arch
|
||||
alt Package found
|
||||
Server1-->>GetbinClient: 200 + package data
|
||||
GetbinClient-->>PackageManager: success
|
||||
else Package not found
|
||||
Server1-->>GetbinClient: 404
|
||||
GetbinClient->>Server2: GET /object/tool:arch
|
||||
Server2-->>GetbinClient: 200 + package data
|
||||
GetbinClient-->>PackageManager: success
|
||||
end
|
||||
PackageManager->>PackageManager: install package
|
||||
PackageManager->>CLI: installation complete
|
||||
```
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### ServerManager Class
|
||||
|
||||
**Purpose**: Manages server configuration, write tokens, and provides server list to other components.
|
||||
|
||||
**Interface**:
|
||||
```cpp
|
||||
class ServerManager {
|
||||
public:
|
||||
ServerManager();
|
||||
|
||||
// Server management
|
||||
bool addServer(const std::string& serverUrl, const std::string& writeToken = "");
|
||||
bool removeServer(const std::string& serverUrl);
|
||||
std::vector<std::string> getServers() const;
|
||||
std::string getDefaultServer() const;
|
||||
std::string getDefaultPublishServer() const; // First server with write token
|
||||
|
||||
// Token management
|
||||
bool setWriteToken(const std::string& serverUrl, const std::string& token);
|
||||
std::string getWriteToken(const std::string& serverUrl) const;
|
||||
bool hasWriteToken(const std::string& serverUrl) const;
|
||||
std::vector<std::string> getServersWithTokens() const;
|
||||
|
||||
// Configuration
|
||||
bool loadConfiguration();
|
||||
bool saveConfiguration();
|
||||
void ensureDefaultConfiguration();
|
||||
|
||||
// Migration
|
||||
bool migrateFromLegacy();
|
||||
|
||||
private:
|
||||
std::vector<ServerConfig> servers_;
|
||||
std::filesystem::path configPath_;
|
||||
|
||||
bool validateServerUrl(const std::string& url) const;
|
||||
bool isServerReachable(const std::string& url) const;
|
||||
ServerConfig* findServer(const std::string& url);
|
||||
};
|
||||
```
|
||||
|
||||
### Enhanced GetbinClient Class
|
||||
|
||||
**Purpose**: Extended to support multiple servers with fallback logic.
|
||||
|
||||
**Interface Changes**:
|
||||
```cpp
|
||||
class GetbinClient {
|
||||
public:
|
||||
GetbinClient(const std::vector<std::string>& servers);
|
||||
|
||||
// Existing methods with server selection
|
||||
bool download(const std::string& toolName, const std::string& arch,
|
||||
const std::string& outPath, ProgressCallback progressCallback = nullptr);
|
||||
bool downloadFromServer(const std::string& serverUrl, const std::string& toolName,
|
||||
const std::string& arch, const std::string& outPath,
|
||||
ProgressCallback progressCallback = nullptr);
|
||||
|
||||
// Server-specific operations
|
||||
bool upload(const std::string& serverUrl, const std::string& archivePath,
|
||||
std::string& outUrl, std::string& outHash, const std::string& token,
|
||||
ProgressCallback progressCallback = nullptr);
|
||||
bool getHash(const std::string& serverUrl, const std::string& toolName,
|
||||
const std::string& arch, std::string& outHash);
|
||||
|
||||
// Multi-server operations
|
||||
bool findPackageServer(const std::string& toolName, const std::string& arch,
|
||||
std::string& foundServer) const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> servers_;
|
||||
std::string buildUrl(const std::string& serverUrl, const std::string& endpoint) const;
|
||||
};
|
||||
```
|
||||
|
||||
### PackageMetadata Structure
|
||||
|
||||
**Purpose**: Enhanced metadata structure to track server source.
|
||||
|
||||
**Structure**:
|
||||
```cpp
|
||||
struct PackageMetadata {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string hash;
|
||||
std::string arch;
|
||||
std::string sourceServer; // New field
|
||||
std::string installDate; // New field for better tracking
|
||||
|
||||
// Serialization
|
||||
nlohmann::json toJson() const;
|
||||
static PackageMetadata fromJson(const nlohmann::json& j);
|
||||
|
||||
// Migration support
|
||||
static PackageMetadata fromLegacyJson(const nlohmann::json& j, const std::string& defaultServer);
|
||||
};
|
||||
```
|
||||
|
||||
### Migration Manager
|
||||
|
||||
**Purpose**: Handles migration from single-server to multi-server configuration.
|
||||
|
||||
**Interface**:
|
||||
```cpp
|
||||
class MigrationManager {
|
||||
public:
|
||||
MigrationManager();
|
||||
|
||||
bool needsMigration() const;
|
||||
bool performMigration();
|
||||
|
||||
private:
|
||||
bool migrateServerConfiguration();
|
||||
bool migratePackageMetadata();
|
||||
bool movePackageFiles();
|
||||
bool updatePackageMetadata();
|
||||
|
||||
std::filesystem::path oldConfigDir_;
|
||||
std::filesystem::path newConfigDir_;
|
||||
std::filesystem::path packagesDir_;
|
||||
};
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### Server Configuration Format
|
||||
|
||||
**File**: `~/.config/getpkg/servers.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"servers": [
|
||||
{
|
||||
"url": "getpkg.xyz",
|
||||
"name": "Official getpkg Registry",
|
||||
"default": true,
|
||||
"writeToken": "",
|
||||
"added": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"url": "packages.example.com",
|
||||
"name": "Example Corporate Registry",
|
||||
"default": false,
|
||||
"writeToken": "abc123token456",
|
||||
"added": "2024-01-16T14:20:00Z"
|
||||
}
|
||||
],
|
||||
"lastUpdated": "2024-01-16T14:20:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Enhanced Package Metadata Format
|
||||
|
||||
**File**: `~/.config/getpkg/packages/<tool_name>.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "example-tool",
|
||||
"version": "2024.0115.1430",
|
||||
"hash": "1234567890123456",
|
||||
"arch": "x86_64",
|
||||
"sourceServer": "getpkg.xyz",
|
||||
"installDate": "2024-01-15T14:30:00Z",
|
||||
"lastUpdated": "2024-01-15T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Directory Structure Changes
|
||||
|
||||
```
|
||||
~/.config/getpkg/
|
||||
├── servers.json # New: Server configuration with embedded tokens
|
||||
├── packages/ # New: Package metadata directory
|
||||
│ ├── tool1.json
|
||||
│ ├── tool2.json
|
||||
│ └── ...
|
||||
└── getpkg.xyz/ # Legacy: Will be migrated to servers.json
|
||||
└── write_token.txt # Legacy: Will be migrated
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Server Connectivity Issues
|
||||
|
||||
1. **Network Failures**: Graceful fallback to next server in list
|
||||
2. **Invalid Responses**: Clear error messages with server identification
|
||||
3. **Authentication Failures**: Server-specific error handling with token guidance
|
||||
|
||||
### Configuration Corruption
|
||||
|
||||
1. **Invalid JSON**: Automatic backup and reset to default configuration
|
||||
2. **Missing Files**: Automatic creation with default settings
|
||||
3. **Permission Issues**: Clear error messages with resolution steps
|
||||
|
||||
### Migration Failures
|
||||
|
||||
1. **Partial Migration**: Rollback capability with clear status reporting
|
||||
2. **File Conflicts**: Safe handling with backup creation
|
||||
3. **Metadata Corruption**: Individual file recovery without breaking entire system
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **ServerManager**: Configuration loading, validation, server management
|
||||
2. **GetbinClient**: Multi-server communication, fallback logic
|
||||
3. **PackageMetadata**: Serialization, migration, validation
|
||||
4. **MigrationManager**: Legacy data handling, file operations
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **End-to-End Installation**: Multi-server package discovery and installation
|
||||
2. **Server Management**: Add/remove servers with real configuration
|
||||
3. **Migration Testing**: Legacy to new format conversion
|
||||
4. **Publish/Unpublish**: Server-specific operations
|
||||
|
||||
### Compatibility Tests
|
||||
|
||||
1. **Backward Compatibility**: Existing installations continue working
|
||||
2. **Legacy Format**: Old package files are properly migrated
|
||||
3. **Default Behavior**: No configuration changes for existing users
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
- Implement ServerManager class
|
||||
- Create server configuration format
|
||||
- Add basic server validation
|
||||
|
||||
### Phase 2: Client Enhancement
|
||||
- Extend GetbinClient for multi-server support
|
||||
- Implement fallback logic
|
||||
- Add server-specific operations
|
||||
|
||||
### Phase 3: Package Management
|
||||
- Update package metadata format
|
||||
- Implement packages directory structure
|
||||
- Add server tracking to installations
|
||||
|
||||
### Phase 4: Migration System
|
||||
- Create MigrationManager
|
||||
- Implement automatic migration
|
||||
- Add backward compatibility layer
|
||||
|
||||
### Phase 5: CLI Integration
|
||||
- Add server management commands
|
||||
- Update existing commands for multi-server
|
||||
- Implement server selection options
|
||||
|
||||
### Phase 6: Testing and Polish
|
||||
- Comprehensive testing suite
|
||||
- Error handling refinement
|
||||
- Documentation updates
|
79
.kiro/specs/multi-server-support/requirements.md
Normal file
79
.kiro/specs/multi-server-support/requirements.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
This feature extends getpkg to support multiple package servers instead of being limited to only getpkg.xyz. Users will be able to add and remove package servers, with getpkg searching across all configured servers to find packages. The system will maintain backward compatibility while providing flexible server management capabilities.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1
|
||||
|
||||
**User Story:** As a developer, I want to configure multiple package servers, so that I can access packages from different repositories and have redundancy in case one server is unavailable.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN I run `getpkg server add <server_url>` THEN the system SHALL add the server to the configuration and confirm the addition
|
||||
2. WHEN I run `getpkg server remove <server_url>` THEN the system SHALL remove the server from the configuration and confirm the removal
|
||||
3. WHEN I run `getpkg server list` THEN the system SHALL display all configured servers in the order they were added
|
||||
4. WHEN no servers are configured THEN the system SHALL default to using getpkg.xyz as the primary server
|
||||
5. WHEN I add the first custom server THEN getpkg.xyz SHALL remain as the default first server unless explicitly removed
|
||||
|
||||
### Requirement 2
|
||||
|
||||
**User Story:** As a user, I want getpkg to search across all configured servers when installing packages, so that I can access packages from any of my configured repositories.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN I run `getpkg install <tool_name>` THEN the system SHALL search servers in the order they were configured
|
||||
2. WHEN a package is found on the first server THEN the system SHALL install from that server and not check remaining servers
|
||||
3. WHEN a package is not found on the first server THEN the system SHALL try the next server in order
|
||||
4. WHEN a package is not found on any server THEN the system SHALL report that the package was not found
|
||||
5. WHEN checking for updates THEN the system SHALL use the same server where the package was originally installed
|
||||
|
||||
### Requirement 3
|
||||
|
||||
**User Story:** As a package publisher, I want to specify which server to publish to and manage write tokens per server, so that I can control where my packages are distributed and authenticate appropriately.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN I run `getpkg publish <tool_name> <folder>` without specifying a server THEN the system SHALL publish to the first configured server that has a write token
|
||||
2. WHEN I run `getpkg publish --server <server_url> <tool_name> <folder>` THEN the system SHALL publish to the specified server using its stored write token
|
||||
3. WHEN I run `getpkg unpublish <tool_name>` without specifying a server THEN the system SHALL unpublish from the first configured server that has a write token
|
||||
4. WHEN I run `getpkg unpublish --server <server_url> <tool_name>` THEN the system SHALL unpublish from the specified server using its stored write token
|
||||
5. WHEN no servers have write tokens THEN the system SHALL report an error and suggest adding a write token to a server
|
||||
|
||||
### Requirement 4
|
||||
|
||||
**User Story:** As a user, I want my package metadata to be organized by server, so that I can track which packages came from which servers and manage them appropriately.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a package is installed THEN the system SHALL store the package metadata in `~/.config/getpkg/packages/<tool_name>.json`
|
||||
2. WHEN package metadata is stored THEN it SHALL include the source server URL in addition to existing fields
|
||||
3. WHEN the packages directory doesn't exist THEN the system SHALL create it automatically
|
||||
4. WHEN migrating from the old format THEN existing package JSON files SHALL be moved to the packages subdirectory
|
||||
5. WHEN migrating from the old format THEN existing package metadata SHALL be updated to include getpkg.xyz as the source server
|
||||
|
||||
### Requirement 5
|
||||
|
||||
**User Story:** As a user, I want server configuration to be persistent and secure, so that my settings are maintained across sessions and my authentication tokens are protected.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN server configuration is modified THEN it SHALL be stored in `~/.config/getpkg/servers.json`
|
||||
2. WHEN the configuration file doesn't exist THEN the system SHALL create it with getpkg.xyz as the default server
|
||||
3. WHEN reading server configuration THEN the system SHALL validate the JSON format and handle corruption gracefully
|
||||
4. WHEN a server URL is invalid THEN the system SHALL reject the addition and provide a helpful error message
|
||||
5. WHEN authentication tokens are needed THEN they SHALL continue to be stored per-server in the existing location pattern
|
||||
|
||||
### Requirement 6
|
||||
|
||||
**User Story:** As a user, I want the multi-server functionality to be backward compatible, so that existing installations continue to work without modification.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN getpkg starts with no server configuration THEN it SHALL automatically configure getpkg.xyz as the default server
|
||||
2. WHEN existing package JSON files are found in `~/.config/getpkg/` THEN they SHALL be automatically migrated to the packages subdirectory
|
||||
3. WHEN migrated package files are processed THEN they SHALL be updated to include server source information
|
||||
4. WHEN all existing functionality is used THEN it SHALL work exactly as before for users who don't configure additional servers
|
||||
5. WHEN the migration process fails THEN the system SHALL provide clear error messages and not break existing functionality
|
94
.kiro/specs/multi-server-support/tasks.md
Normal file
94
.kiro/specs/multi-server-support/tasks.md
Normal file
@ -0,0 +1,94 @@
|
||||
# Implementation Plan
|
||||
|
||||
Based on analysis of the current codebase, the multi-server support feature needs to be built from scratch. The current implementation has a hardcoded `SERVER_HOST = "getpkg.xyz"` in `GetbinClient` and no server management infrastructure.
|
||||
|
||||
## Core Infrastructure Tasks
|
||||
|
||||
- [-] 1. Create ServerManager class and server configuration system
|
||||
|
||||
|
||||
|
||||
- Implement ServerManager class with server add/remove/list functionality
|
||||
- Create server configuration JSON format and file handling
|
||||
- Add server URL validation and reachability checks
|
||||
- Implement write token management per server
|
||||
- _Requirements: 1.1, 1.2, 1.3, 5.1, 5.2, 5.4_
|
||||
|
||||
- [ ] 2. Enhance GetbinClient for multi-server support
|
||||
- Modify GetbinClient constructor to accept server list instead of hardcoded host
|
||||
- Implement multi-server fallback logic for downloads
|
||||
- Add server-specific upload and hash operations
|
||||
- Create findPackageServer method for package discovery
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
||||
|
||||
- [ ] 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
|
||||
- Create package metadata validation and error handling
|
||||
- _Requirements: 4.1, 4.2, 4.3_
|
||||
|
||||
## Migration and Compatibility Tasks
|
||||
|
||||
- [ ] 4. Implement migration system for existing installations
|
||||
- Create MigrationManager class for legacy data handling
|
||||
- Implement automatic migration from single-server to multi-server config
|
||||
- Migrate existing package JSON files to packages subdirectory
|
||||
- Update existing package metadata to include server source information
|
||||
- Add migration error handling and rollback capabilities
|
||||
- _Requirements: 4.4, 4.5, 6.1, 6.2, 6.3, 6.5_
|
||||
|
||||
- [ ] 5. Ensure backward compatibility
|
||||
- Implement default server configuration (getpkg.xyz) when no config exists
|
||||
- Maintain existing CLI behavior for users without custom server configuration
|
||||
- Preserve existing token storage location compatibility
|
||||
- Add graceful handling of missing or corrupted configuration files
|
||||
- _Requirements: 6.1, 6.4, 5.3_
|
||||
|
||||
## CLI Integration Tasks
|
||||
|
||||
- [ ] 6. Add server management commands to main.cpp
|
||||
- Implement `getpkg server add <url>` command
|
||||
- Implement `getpkg server remove <url>` command
|
||||
- Implement `getpkg server list` command
|
||||
- Add server URL validation and user feedback
|
||||
- _Requirements: 1.1, 1.2, 1.3_
|
||||
|
||||
- [ ] 7. Update existing commands for multi-server support
|
||||
- Modify install command to use ServerManager and multi-server GetbinClient
|
||||
- Update publish command to support --server option and default server selection
|
||||
- Update unpublish command to support --server option and default server selection
|
||||
- Ensure update command works with multi-server package tracking
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4, 3.5_
|
||||
|
||||
## Integration and Testing Tasks
|
||||
|
||||
- [ ] 8. Integrate all components in main application flow
|
||||
- Initialize ServerManager in main.cpp startup
|
||||
- Trigger migration process on first run with new version
|
||||
- Update package installation flow to use enhanced metadata
|
||||
- Ensure proper error handling and user messaging throughout
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
|
||||
- [ ] 9. Add comprehensive error handling and validation
|
||||
- Implement network error handling with server fallback
|
||||
- Add configuration file corruption recovery
|
||||
- Create user-friendly error messages for server connectivity issues
|
||||
- Add validation for server URLs and authentication tokens
|
||||
- _Requirements: 5.3, 5.4, 5.5_
|
||||
|
||||
- [ ] 10. Create unit tests for new components
|
||||
- Write unit tests for ServerManager class functionality
|
||||
- Test GetbinClient multi-server operations and fallback logic
|
||||
- Test PackageMetadata serialization and migration
|
||||
- Test MigrationManager with various legacy data scenarios
|
||||
- Create integration tests for complete multi-server workflows
|
||||
- _Requirements: All requirements validation_
|
||||
|
||||
## Notes
|
||||
|
||||
- Current codebase has `SERVER_HOST = "getpkg.xyz"` hardcoded in GetbinClient.cpp
|
||||
- No existing server management or configuration infrastructure
|
||||
- Package metadata is currently stored as individual JSON files in ~/.config/getpkg/
|
||||
- Token storage is in ~/.config/getpkg.xyz/write_token.txt (legacy format)
|
||||
- All functionality needs to be built from scratch while maintaining backward compatibility
|
@ -69,4 +69,4 @@
|
||||
## Configuration Files
|
||||
- **.gitignore**: Standard ignore patterns for build artifacts
|
||||
- **.vscode/**: VS Code workspace settings
|
||||
- **CMakeLists.txt**: Follows standard template with PROJECT_NAME parameter
|
||||
- **CMakeLists.txt**: Follows standard template with PROJECT_NAME parameter for the name of the project
|
||||
|
353
getpkg/src/ServerManager.cpp
Normal file
353
getpkg/src/ServerManager.cpp
Normal file
@ -0,0 +1,353 @@
|
||||
#include "ServerManager.hpp"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include <cpr/cpr.h>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// ServerConfig implementation
|
||||
json ServerConfig::toJson() const {
|
||||
return json{
|
||||
{"url", url},
|
||||
{"name", name},
|
||||
{"default", isDefault},
|
||||
{"writeToken", writeToken},
|
||||
{"added", addedDate}
|
||||
};
|
||||
}
|
||||
|
||||
ServerConfig ServerConfig::fromJson(const json& j) {
|
||||
ServerConfig config;
|
||||
config.url = j.value("url", "");
|
||||
config.name = j.value("name", "");
|
||||
config.isDefault = j.value("default", false);
|
||||
config.writeToken = j.value("writeToken", "");
|
||||
config.addedDate = j.value("added", "");
|
||||
return config;
|
||||
}
|
||||
|
||||
// ServerManager implementation
|
||||
ServerManager::ServerManager() {
|
||||
const char* home = getenv("HOME");
|
||||
if (home) {
|
||||
configPath_ = std::filesystem::path(home) / ".config" / "getpkg" / "servers.json";
|
||||
}
|
||||
}
|
||||
|
||||
bool ServerManager::addServer(const std::string& serverUrl, const std::string& writeToken) {
|
||||
if (!validateServerUrl(serverUrl)) {
|
||||
std::cerr << "Invalid server URL: " << serverUrl << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if server already exists
|
||||
if (findServer(serverUrl) != nullptr) {
|
||||
std::cerr << "Server already exists: " << serverUrl << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if server is reachable
|
||||
if (!isServerReachable(serverUrl)) {
|
||||
std::cerr << "Warning: Server may not be reachable: " << serverUrl << std::endl;
|
||||
// Continue anyway - server might be temporarily down
|
||||
}
|
||||
|
||||
ServerConfig config;
|
||||
config.url = serverUrl;
|
||||
config.name = serverUrl; // Use URL as default name
|
||||
config.isDefault = servers_.empty(); // First server becomes default
|
||||
config.writeToken = writeToken;
|
||||
config.addedDate = getCurrentTimestamp();
|
||||
|
||||
servers_.push_back(config);
|
||||
|
||||
return saveConfiguration();
|
||||
}
|
||||
|
||||
bool ServerManager::removeServer(const std::string& serverUrl) {
|
||||
auto it = std::find_if(servers_.begin(), servers_.end(),
|
||||
[&serverUrl](const ServerConfig& config) {
|
||||
return config.url == serverUrl;
|
||||
});
|
||||
|
||||
if (it == servers_.end()) {
|
||||
std::cerr << "Server not found: " << serverUrl << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't allow removing the last server
|
||||
if (servers_.size() == 1) {
|
||||
std::cerr << "Cannot remove the last server. Add another server first." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wasDefault = it->isDefault;
|
||||
servers_.erase(it);
|
||||
|
||||
// If we removed the default server, make the first remaining server default
|
||||
if (wasDefault && !servers_.empty()) {
|
||||
servers_[0].isDefault = true;
|
||||
}
|
||||
|
||||
return saveConfiguration();
|
||||
}
|
||||
|
||||
std::vector<std::string> ServerManager::getServers() const {
|
||||
std::vector<std::string> urls;
|
||||
for (const auto& server : servers_) {
|
||||
urls.push_back(server.url);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
std::string ServerManager::getDefaultServer() const {
|
||||
for (const auto& server : servers_) {
|
||||
if (server.isDefault) {
|
||||
return server.url;
|
||||
}
|
||||
}
|
||||
|
||||
// If no default is set, return the first server
|
||||
if (!servers_.empty()) {
|
||||
return servers_[0].url;
|
||||
}
|
||||
|
||||
return "getpkg.xyz"; // Fallback to original default
|
||||
}
|
||||
|
||||
std::string ServerManager::getDefaultPublishServer() const {
|
||||
// Return first server with a write token
|
||||
for (const auto& server : servers_) {
|
||||
if (!server.writeToken.empty()) {
|
||||
return server.url;
|
||||
}
|
||||
}
|
||||
|
||||
// If no server has a token, return the default server
|
||||
return getDefaultServer();
|
||||
}
|
||||
|
||||
bool ServerManager::setWriteToken(const std::string& serverUrl, const std::string& token) {
|
||||
ServerConfig* server = findServer(serverUrl);
|
||||
if (server == nullptr) {
|
||||
std::cerr << "Server not found: " << serverUrl << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
server->writeToken = token;
|
||||
return saveConfiguration();
|
||||
}
|
||||
|
||||
std::string ServerManager::getWriteToken(const std::string& serverUrl) const {
|
||||
const ServerConfig* server = findServer(serverUrl);
|
||||
if (server != nullptr) {
|
||||
return server->writeToken;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool ServerManager::hasWriteToken(const std::string& serverUrl) const {
|
||||
const ServerConfig* server = findServer(serverUrl);
|
||||
return server != nullptr && !server->writeToken.empty();
|
||||
}
|
||||
|
||||
std::vector<std::string> ServerManager::getServersWithTokens() const {
|
||||
std::vector<std::string> serversWithTokens;
|
||||
for (const auto& server : servers_) {
|
||||
if (!server.writeToken.empty()) {
|
||||
serversWithTokens.push_back(server.url);
|
||||
}
|
||||
}
|
||||
return serversWithTokens;
|
||||
}
|
||||
|
||||
bool ServerManager::loadConfiguration() {
|
||||
if (!std::filesystem::exists(configPath_)) {
|
||||
ensureDefaultConfiguration();
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
std::ifstream file(configPath_);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Failed to open server configuration file: " << configPath_ << std::endl;
|
||||
ensureDefaultConfiguration();
|
||||
return true;
|
||||
}
|
||||
|
||||
json config;
|
||||
file >> config;
|
||||
|
||||
if (!config.contains("servers") || !config["servers"].is_array()) {
|
||||
std::cerr << "Invalid server configuration format" << std::endl;
|
||||
ensureDefaultConfiguration();
|
||||
return true;
|
||||
}
|
||||
|
||||
servers_.clear();
|
||||
for (const auto& serverJson : config["servers"]) {
|
||||
try {
|
||||
servers_.push_back(ServerConfig::fromJson(serverJson));
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Warning: Skipping invalid server config: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have at least one server
|
||||
if (servers_.empty()) {
|
||||
ensureDefaultConfiguration();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading server configuration: " << e.what() << std::endl;
|
||||
ensureDefaultConfiguration();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ServerManager::saveConfiguration() {
|
||||
try {
|
||||
// Ensure directory exists
|
||||
std::filesystem::create_directories(configPath_.parent_path());
|
||||
|
||||
json config;
|
||||
config["version"] = "1.0";
|
||||
config["lastUpdated"] = getCurrentTimestamp();
|
||||
|
||||
json serversArray = json::array();
|
||||
for (const auto& server : servers_) {
|
||||
serversArray.push_back(server.toJson());
|
||||
}
|
||||
config["servers"] = serversArray;
|
||||
|
||||
std::ofstream file(configPath_);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Failed to open server configuration file for writing: " << configPath_ << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
file << config.dump(2);
|
||||
return file.good();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error saving server configuration: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ServerManager::ensureDefaultConfiguration() {
|
||||
servers_.clear();
|
||||
|
||||
ServerConfig defaultServer;
|
||||
defaultServer.url = "getpkg.xyz";
|
||||
defaultServer.name = "Official getpkg Registry";
|
||||
defaultServer.isDefault = true;
|
||||
defaultServer.writeToken = "";
|
||||
defaultServer.addedDate = getCurrentTimestamp();
|
||||
|
||||
servers_.push_back(defaultServer);
|
||||
|
||||
saveConfiguration();
|
||||
}
|
||||
|
||||
bool ServerManager::migrateFromLegacy() {
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path legacyTokenPath = std::filesystem::path(home) / ".config" / "getpkg.xyz" / "write_token.txt";
|
||||
|
||||
if (std::filesystem::exists(legacyTokenPath)) {
|
||||
try {
|
||||
std::ifstream tokenFile(legacyTokenPath);
|
||||
std::string token;
|
||||
std::getline(tokenFile, token);
|
||||
|
||||
if (!token.empty()) {
|
||||
// Set the token for getpkg.xyz server
|
||||
setWriteToken("getpkg.xyz", token);
|
||||
|
||||
// Optionally remove the legacy token file
|
||||
// std::filesystem::remove(legacyTokenPath);
|
||||
|
||||
std::cout << "Migrated legacy write token for getpkg.xyz" << std::endl;
|
||||
return true;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Warning: Failed to migrate legacy token: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServerManager::validateServerUrl(const std::string& url) const {
|
||||
if (url.empty() || url.length() > 253) { // DNS name length limit
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic URL validation - should be a valid hostname or IP
|
||||
// Allow formats like: example.com, sub.example.com, 192.168.1.1, localhost
|
||||
std::regex urlPattern(R"(^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$)");
|
||||
|
||||
if (!std::regex_match(url, urlPattern)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional checks
|
||||
if (url.find("..") != std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.front() == '.' || url.back() == '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerManager::isServerReachable(const std::string& url) const {
|
||||
try {
|
||||
std::string testUrl = "https://" + url + "/";
|
||||
|
||||
auto response = cpr::Head(cpr::Url{testUrl},
|
||||
cpr::Timeout{5000}, // 5 seconds
|
||||
cpr::VerifySsl{true});
|
||||
|
||||
// Accept any response that indicates the server is reachable
|
||||
// (200, 404, 403, etc. - as long as we get a response)
|
||||
return response.status_code > 0;
|
||||
} catch (const std::exception& e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ServerConfig* ServerManager::findServer(const std::string& url) {
|
||||
auto it = std::find_if(servers_.begin(), servers_.end(),
|
||||
[&url](const ServerConfig& config) {
|
||||
return config.url == url;
|
||||
});
|
||||
return (it != servers_.end()) ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
const ServerConfig* ServerManager::findServer(const std::string& url) const {
|
||||
auto it = std::find_if(servers_.begin(), servers_.end(),
|
||||
[&url](const ServerConfig& config) {
|
||||
return config.url == url;
|
||||
});
|
||||
return (it != servers_.end()) ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
std::string ServerManager::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();
|
||||
}
|
53
getpkg/src/ServerManager.hpp
Normal file
53
getpkg/src/ServerManager.hpp
Normal file
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
struct ServerConfig {
|
||||
std::string url;
|
||||
std::string name;
|
||||
bool isDefault = false;
|
||||
std::string writeToken;
|
||||
std::string addedDate;
|
||||
|
||||
// JSON serialization
|
||||
nlohmann::json toJson() const;
|
||||
static ServerConfig fromJson(const nlohmann::json& j);
|
||||
};
|
||||
|
||||
class ServerManager {
|
||||
public:
|
||||
ServerManager();
|
||||
|
||||
// Server management
|
||||
bool addServer(const std::string& serverUrl, const std::string& writeToken = "");
|
||||
bool removeServer(const std::string& serverUrl);
|
||||
std::vector<std::string> getServers() const;
|
||||
std::string getDefaultServer() const;
|
||||
std::string getDefaultPublishServer() const; // First server with write token
|
||||
|
||||
// Token management
|
||||
bool setWriteToken(const std::string& serverUrl, const std::string& token);
|
||||
std::string getWriteToken(const std::string& serverUrl) const;
|
||||
bool hasWriteToken(const std::string& serverUrl) const;
|
||||
std::vector<std::string> getServersWithTokens() const;
|
||||
|
||||
// Configuration
|
||||
bool loadConfiguration();
|
||||
bool saveConfiguration();
|
||||
void ensureDefaultConfiguration();
|
||||
|
||||
// Migration
|
||||
bool migrateFromLegacy();
|
||||
|
||||
private:
|
||||
std::vector<ServerConfig> servers_;
|
||||
std::filesystem::path configPath_;
|
||||
|
||||
bool validateServerUrl(const std::string& url) const;
|
||||
bool isServerReachable(const std::string& url) const;
|
||||
ServerConfig* findServer(const std::string& url);
|
||||
const ServerConfig* findServer(const std::string& url) const;
|
||||
std::string getCurrentTimestamp() const;
|
||||
};
|
128
gp/gp
128
gp/gp
@ -49,27 +49,43 @@ EOF
|
||||
|
||||
# Function to generate commit message based on changes
|
||||
generate_commit_message() {
|
||||
local files_changed
|
||||
files_changed=$(git diff --cached --name-only)
|
||||
local files_count
|
||||
files_count=$(echo "$files_changed" | wc -l)
|
||||
|
||||
if [ -z "$files_changed" ]; then
|
||||
files_changed=$(git diff --name-only)
|
||||
files_count=$(echo "$files_changed" | wc -l)
|
||||
# First check if we have staged changes
|
||||
local has_staged_changes=false
|
||||
if ! git diff --cached --quiet; then
|
||||
has_staged_changes=true
|
||||
fi
|
||||
|
||||
# If add-all is enabled, also include untracked files
|
||||
if [ "$ADD_ALL" = true ] && [ -z "$files_changed" ]; then
|
||||
files_changed=$(git ls-files --others --exclude-standard)
|
||||
files_count=$(echo "$files_changed" | wc -l)
|
||||
# Determine which changes to analyze based on staging status and ADD_ALL setting
|
||||
local status_command=""
|
||||
if [ "$has_staged_changes" = true ]; then
|
||||
status_command="git diff --cached --name-status"
|
||||
else
|
||||
status_command="git diff --name-status"
|
||||
fi
|
||||
|
||||
if [ -z "$files_changed" ]; then
|
||||
# Get all changes (staged or unstaged depending on context)
|
||||
local all_changes
|
||||
all_changes=$($status_command)
|
||||
|
||||
# If no changes from diff, check for untracked files when add-all is enabled
|
||||
if [ -z "$all_changes" ] && [ "$ADD_ALL" = true ]; then
|
||||
local untracked_files
|
||||
untracked_files=$(git ls-files --others --exclude-standard)
|
||||
if [ -n "$untracked_files" ]; then
|
||||
# Convert untracked files to "A" (added) status format
|
||||
all_changes=$(echo "$untracked_files" | sed 's/^/A\t/')
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$all_changes" ]; then
|
||||
echo "No changes to commit"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Count total files
|
||||
local files_count
|
||||
files_count=$(echo "$all_changes" | wc -l)
|
||||
|
||||
# Generate smart commit message based on file types and changes
|
||||
local has_source_files=false
|
||||
local has_config_files=false
|
||||
@ -77,7 +93,8 @@ generate_commit_message() {
|
||||
local has_tests=false
|
||||
local message=""
|
||||
|
||||
while IFS= read -r file; do
|
||||
# Extract just the filenames for type detection
|
||||
while IFS=$'\t' read -r status file; do
|
||||
[ -z "$file" ] && continue
|
||||
|
||||
case "$file" in
|
||||
@ -94,15 +111,18 @@ generate_commit_message() {
|
||||
has_tests=true
|
||||
;;
|
||||
esac
|
||||
done <<< "$files_changed"
|
||||
done <<< "$all_changes"
|
||||
|
||||
# Create descriptive commit message
|
||||
if [ "$files_count" -eq 1 ]; then
|
||||
local change_line
|
||||
change_line=$(echo "$all_changes" | head -1)
|
||||
local status
|
||||
local single_file
|
||||
single_file=$(echo "$files_changed" | head -1)
|
||||
local change_type
|
||||
change_type=$(git diff --cached --name-status -- "$single_file" 2>/dev/null || git diff --name-status -- "$single_file")
|
||||
case "${change_type:0:1}" in
|
||||
status=$(echo "$change_line" | cut -f1)
|
||||
single_file=$(echo "$change_line" | cut -f2)
|
||||
|
||||
case "${status:0:1}" in
|
||||
A) message="Add $single_file" ;;
|
||||
M) message="Update $single_file" ;;
|
||||
D) message="Remove $single_file" ;;
|
||||
@ -110,6 +130,58 @@ generate_commit_message() {
|
||||
*) message="Modify $single_file" ;;
|
||||
esac
|
||||
else
|
||||
# For multiple files, analyze the types of changes
|
||||
local added_count=0
|
||||
local modified_count=0
|
||||
local deleted_count=0
|
||||
local renamed_count=0
|
||||
|
||||
# Use the all_changes variable we already have
|
||||
|
||||
# Count different types of changes
|
||||
while IFS=$'\t' read -r status file; do
|
||||
[ -z "$status" ] && continue
|
||||
case "${status:0:1}" in
|
||||
A) ((added_count++)) ;;
|
||||
M) ((modified_count++)) ;;
|
||||
D) ((deleted_count++)) ;;
|
||||
R) ((renamed_count++)) ;;
|
||||
esac
|
||||
done <<< "$all_changes"
|
||||
|
||||
# Also count untracked files if add-all is enabled
|
||||
if [ "$ADD_ALL" = true ]; then
|
||||
local untracked_files
|
||||
untracked_files=$(git ls-files --others --exclude-standard)
|
||||
if [ -n "$untracked_files" ]; then
|
||||
local untracked_count
|
||||
untracked_count=$(echo "$untracked_files" | wc -l)
|
||||
((added_count += untracked_count))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate message based on change types
|
||||
local change_parts=()
|
||||
[ $added_count -gt 0 ] && change_parts+=("add $added_count")
|
||||
[ $modified_count -gt 0 ] && change_parts+=("update $modified_count")
|
||||
[ $deleted_count -gt 0 ] && change_parts+=("remove $deleted_count")
|
||||
[ $renamed_count -gt 0 ] && change_parts+=("rename $renamed_count")
|
||||
|
||||
local change_desc=""
|
||||
if [ ${#change_parts[@]} -eq 1 ]; then
|
||||
change_desc="${change_parts[0]}"
|
||||
elif [ ${#change_parts[@]} -eq 2 ]; then
|
||||
change_desc="${change_parts[0]} and ${change_parts[1]}"
|
||||
else
|
||||
# Join all but last with commas, last with "and"
|
||||
local last_idx=$((${#change_parts[@]} - 1))
|
||||
for i in $(seq 0 $((last_idx - 1))); do
|
||||
[ $i -gt 0 ] && change_desc+=", "
|
||||
change_desc+="${change_parts[i]}"
|
||||
done
|
||||
change_desc+=" and ${change_parts[last_idx]}"
|
||||
fi
|
||||
|
||||
local prefix=""
|
||||
if $has_tests; then
|
||||
prefix="test: "
|
||||
@ -121,18 +193,32 @@ generate_commit_message() {
|
||||
prefix="feat: "
|
||||
fi
|
||||
|
||||
message="${prefix}Update $files_count files"
|
||||
# Capitalize first letter of change description
|
||||
change_desc="$(echo "${change_desc:0:1}" | tr '[:lower:]' '[:upper:]')${change_desc:1}"
|
||||
|
||||
message="${prefix}${change_desc} files"
|
||||
fi
|
||||
|
||||
echo "$message"
|
||||
}
|
||||
|
||||
# Function to check if we're in a git repository
|
||||
# Function to check if we're in a git repository and change to repo root
|
||||
check_git_repo() {
|
||||
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
||||
print_error "Not in a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to the git repository root to ensure we operate on the entire repo
|
||||
local git_root
|
||||
git_root=$(git rev-parse --show-toplevel)
|
||||
if [ "$PWD" != "$git_root" ]; then
|
||||
print_info "Changing to git repository root: $git_root"
|
||||
cd "$git_root" || {
|
||||
print_error "Failed to change to git repository root"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check for uncommitted changes and unpushed commits
|
||||
|
Reference in New Issue
Block a user