refactor: move V1 code into v1/ subdirectory

- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/
- Create v1/README.md with V1 documentation
- Update root README for V1 + V2 structure
- V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass
- Root is now clean for V2 development
This commit is contained in:
swanadiva
2026-07-07 11:56:27 +07:00
parent 8ebdebedc8
commit 847989df75
68 changed files with 58 additions and 116 deletions
+105
View File
@@ -0,0 +1,105 @@
package errors
import (
"fmt"
"strings"
)
// ConnectionError represents SSH connection errors with helpful hints
type ConnectionError struct {
Type string // "auth", "network", "timeout", "config", "unknown"
Message string
Details string
Hints []string
}
// Error implements the error interface
func (e *ConnectionError) Error() string {
return fmt.Sprintf("Connection Error (%s): %s\nDetails: %s", e.Type, e.Message, e.Details)
}
// NewConnectionError creates a new ConnectionError
func NewConnectionError(errType, message, details string, hints []string) *ConnectionError {
return &ConnectionError{
Type: errType,
Message: message,
Details: details,
Hints: hints,
}
}
// HandleSSHError processes SSH errors and returns user-friendly errors
func HandleSSHError(err error) *ConnectionError {
if err == nil {
return nil
}
errStr := err.Error()
switch {
case strings.Contains(errStr, "connection refused"):
return &ConnectionError{
Type: "network",
Message: "Cannot connect to server",
Details: errStr,
Hints: []string{"Check if server is running", "Verify firewall rules", "Confirm hostname and port"},
}
case strings.Contains(errStr, "authentication failed"), strings.Contains(errStr, "unable to authenticate"):
return &ConnectionError{
Type: "auth",
Message: "Authentication failed",
Details: errStr,
Hints: []string{"Verify username and password", "Check SSH key is loaded", "Test with native SSH client"},
}
case strings.Contains(errStr, "timeout"), strings.Contains(errStr, "timed out"):
return &ConnectionError{
Type: "timeout",
Message: "Connection timeout",
Details: errStr,
Hints: []string{"Check network connectivity", "Try increasing timeout", "Verify server is reachable"},
}
case strings.Contains(errStr, "no such host"), strings.Contains(errStr, "hostname"):
return &ConnectionError{
Type: "config",
Message: "Invalid hostname",
Details: errStr,
Hints: []string{"Verify hostname spelling", "Check DNS resolution", "Try IP address instead"},
}
case strings.Contains(errStr, "permission denied"):
return &ConnectionError{
Type: "auth",
Message: "Permission denied",
Details: errStr,
Hints: []string{"Check user permissions on server", "Verify account is not locked", "Check authentication method"},
}
default:
return &ConnectionError{
Type: "unknown",
Message: "Connection failed",
Details: errStr,
Hints: []string{"Check host configuration", "Verify network settings", "Test with standard SSH client"},
}
}
}
// FormatConnectionError formats connection error for user-friendly display
func FormatConnectionError(err *ConnectionError) string {
var output strings.Builder
output.WriteString(fmt.Sprintf("❌ Connection Error: %s\n\n", err.Message))
output.WriteString(fmt.Sprintf("Details: %s\n\n", err.Details))
if len(err.Hints) > 0 {
output.WriteString("Possible solutions:\n")
for i, hint := range err.Hints {
output.WriteString(fmt.Sprintf(" %d. %s\n", i+1, hint))
}
}
return output.String()
}
+48
View File
@@ -0,0 +1,48 @@
package errors
import (
"fmt"
)
// Error codes
const (
ErrHostNotFound = "HOST_NOT_FOUND"
ErrAuthFailed = "AUTH_FAILED"
ErrConnectionTimeout = "CONNECTION_TIMEOUT"
ErrInvalidConfig = "INVALID_CONFIG"
ErrKeyNotFound = "KEY_NOT_FOUND"
ErrPermissionDenied = "PERMISSION_DENIED"
ErrFileCorrupted = "FILE_CORRUPTED"
ErrInvalidCredentials = "INVALID_CREDENTIALS"
)
// AppError represents an application error with code, message, cause, and hints
type AppError struct {
Code string
Message string
Cause error
Hints []string
}
// Error implements the error interface
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Unwrap returns the underlying cause for use with errors.Is/errors.As
func (e *AppError) Unwrap() error {
return e.Cause
}
// NewAppError creates a new application error
func NewAppError(code, message string, cause error, hints []string) *AppError {
return &AppError{
Code: code,
Message: message,
Cause: cause,
Hints: hints,
}
}
+140
View File
@@ -0,0 +1,140 @@
package models
import (
"time"
)
// Host represents an SSH host connection configuration
type Host struct {
ID string `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
Port int `json:"port"`
Username string `json:"username"`
Auth AuthConfig `json:"auth"`
Group string `json:"group,omitempty"`
Tags []string `json:"tags,omitempty"`
Notes string `json:"notes,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
}
// AuthConfig represents authentication configuration
type AuthConfig struct {
Type string `json:"type"` // "password", "key", "both"
Password string `json:"password,omitempty"`
KeyID string `json:"key_id,omitempty"` // Reference to KeyPair ID
}
// KeyPair represents an SSH key pair
type KeyPair struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // "rsa", "ed25519", "ecdsa"
PrivateKey string `json:"private_key"`
PublicKey string `json:"public_key,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Snippet represents a command snippet
type Snippet struct {
ID string `json:"id"`
Name string `json:"name"`
Command string `json:"command"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Profile represents a named configuration profile
type Profile struct {
Name string `json:"name"`
Theme string `json:"theme"`
DefaultGroup string `json:"default_group,omitempty"`
DefaultAuth string `json:"default_auth,omitempty"` // "password", "key", "both"
Editor string `json:"editor,omitempty"`
Notes string `json:"notes,omitempty"`
}
// AppConfig represents the application configuration
type AppConfig struct {
Version string `json:"version"`
DefaultPort int `json:"default_port"`
ConnectionTimeout int `json:"connection_timeout"` // in seconds
Theme string `json:"theme"`
Editor string `json:"editor"`
AutoSync bool `json:"auto_sync"`
SyncProvider string `json:"sync_provider,omitempty"`
// Profiles
Profiles []Profile `json:"profiles,omitempty"`
ActiveProfile string `json:"active_profile,omitempty"`
// Security
EncryptionEnabled bool `json:"encryption_enabled"`
PasswordHash string `json:"password_hash,omitempty"` // SHA-256 hash for verification
KnownHostsFile string `json:"known_hosts_file,omitempty"`
}
// KnownHost represents a verified host key
type KnownHost struct {
Hostname string `json:"hostname"`
Port int `json:"port"`
KeyType string `json:"key_type"` // "ssh-rsa", "ssh-ed25519", etc.
KeyHash string `json:"key_hash"` // Base64-encoded host key
AddedAt time.Time `json:"added_at"`
}
// DefaultConfig returns the default application configuration
func DefaultConfig() *AppConfig {
return &AppConfig{
Version: "1.0.0",
DefaultPort: 22,
ConnectionTimeout: 30,
Theme: "dark",
Editor: "vim",
AutoSync: false,
EncryptionEnabled: false,
Profiles: []Profile{
{
Name: "default",
Theme: "dark",
},
},
ActiveProfile: "default",
}
}
// GetProfile returns a profile by name
func (c *AppConfig) GetProfile(name string) *Profile {
for i := range c.Profiles {
if c.Profiles[i].Name == name {
return &c.Profiles[i]
}
}
return nil
}
// GetActiveProfile returns the active profile
func (c *AppConfig) GetActiveProfile() *Profile {
return c.GetProfile(c.ActiveProfile)
}
// AddProfile adds a new profile
func (c *AppConfig) AddProfile(p Profile) {
c.Profiles = append(c.Profiles, p)
}
// RemoveProfile removes a profile by name
func (c *AppConfig) RemoveProfile(name string) {
for i := range c.Profiles {
if c.Profiles[i].Name == name {
c.Profiles = append(c.Profiles[:i], c.Profiles[i+1:]...)
return
}
}
}