mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-12-06 02:48:28 +01:00
Compare commits
No commits in common. "edf61049fc8b20d482e3e51f78e0b457783c9602" and "42387eb1faa9c0311458695f5241e2546b027572" have entirely different histories.
edf61049fc
...
42387eb1fa
23
README.md
23
README.md
@ -243,29 +243,6 @@ sshm --version
|
||||
sshm --help
|
||||
```
|
||||
|
||||
### Backup Configuration
|
||||
|
||||
SSHM automatically creates backups of your SSH configuration files before making any changes to ensure your configurations are safe.
|
||||
|
||||
**Backup Location:**
|
||||
- **Unix/Linux/macOS**: `~/.config/sshm/backups/` (or `$XDG_CONFIG_HOME/sshm/backups/` if set)
|
||||
- **Windows**: `%APPDATA%\sshm\backups\` (fallback: `%USERPROFILE%\.config\sshm\backups\`)
|
||||
|
||||
**Key Features:**
|
||||
- Automatic backup before any modification
|
||||
- One backup per file (overwrites previous backup)
|
||||
- Stored separately to avoid SSH Include conflicts
|
||||
- Easy manual recovery if needed
|
||||
|
||||
**Quick Recovery:**
|
||||
```bash
|
||||
# Unix/Linux/macOS
|
||||
cp ~/.config/sshm/backups/config.backup ~/.ssh/config
|
||||
|
||||
# Windows
|
||||
copy "%APPDATA%\sshm\backups\config.backup" "%USERPROFILE%\.ssh\config"
|
||||
```
|
||||
|
||||
### Configuration File Options
|
||||
|
||||
By default, SSHM uses the standard SSH configuration file at `~/.ssh/config`. You can specify a different configuration file using the `-c` flag:
|
||||
|
||||
@ -2,7 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Gu1llaum-3/sshm/internal/ui"
|
||||
"sshm/internal/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -2,7 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Gu1llaum-3/sshm/internal/ui"
|
||||
"sshm/internal/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -6,8 +6,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/ui"
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"sshm/internal/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
4
go.mod
4
go.mod
@ -1,4 +1,4 @@
|
||||
module github.com/Gu1llaum-3/sshm
|
||||
module sshm
|
||||
|
||||
go 1.23.1
|
||||
|
||||
@ -7,7 +7,6 @@ require (
|
||||
github.com/charmbracelet/bubbletea v1.3.6
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
golang.org/x/crypto v0.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -29,6 +28,7 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@ -53,14 +53,18 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@ -40,36 +40,6 @@ func GetDefaultSSHConfigPath() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetSSHMBackupDir returns the SSHM backup directory
|
||||
func GetSSHMBackupDir() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var configDir string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Use %APPDATA%/sshm on Windows
|
||||
appData := os.Getenv("APPDATA")
|
||||
if appData != "" {
|
||||
configDir = filepath.Join(appData, "sshm")
|
||||
} else {
|
||||
configDir = filepath.Join(homeDir, ".config", "sshm")
|
||||
}
|
||||
default:
|
||||
// Use XDG Base Directory specification
|
||||
xdgConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgConfigDir != "" {
|
||||
configDir = filepath.Join(xdgConfigDir, "sshm")
|
||||
} else {
|
||||
configDir = filepath.Join(homeDir, ".config", "sshm")
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, "backups"), nil
|
||||
}
|
||||
|
||||
// GetSSHDirectory returns the .ssh directory path
|
||||
func GetSSHDirectory() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
@ -97,23 +67,9 @@ func ensureSSHDirectory() error {
|
||||
// configMutex protects SSH config file operations from race conditions
|
||||
var configMutex sync.Mutex
|
||||
|
||||
// backupConfig creates a backup of the SSH config file in ~/.config/sshm/backups/
|
||||
// backupConfig creates a backup of the SSH config file
|
||||
func backupConfig(configPath string) error {
|
||||
// Get backup directory and ensure it exists
|
||||
backupDir, err := GetSSHMBackupDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get backup directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create backup directory: %w", err)
|
||||
}
|
||||
|
||||
// Create simple backup filename (overwrites previous backup)
|
||||
filename := filepath.Base(configPath)
|
||||
backupPath := filepath.Join(backupDir, filename+".backup")
|
||||
|
||||
// Copy file
|
||||
backupPath := configPath + ".backup"
|
||||
src, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -126,12 +82,8 @@ func backupConfig(configPath string) error {
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err = io.Copy(dst, src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set appropriate permissions
|
||||
return os.Chmod(backupPath, 0600)
|
||||
_, err = io.Copy(dst, src)
|
||||
return err
|
||||
}
|
||||
|
||||
// ParseSSHConfig parses the SSH config file and returns the list of hosts
|
||||
|
||||
@ -452,122 +452,6 @@ Include *.conf
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupConfigToSSHMDirectory(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Override the home directory for this test
|
||||
originalHome := os.Getenv("HOME")
|
||||
if originalHome == "" {
|
||||
originalHome = os.Getenv("USERPROFILE") // Windows
|
||||
}
|
||||
|
||||
// Set test home directory
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
// Create a test SSH config file
|
||||
sshDir := filepath.Join(tempDir, ".ssh")
|
||||
err := os.MkdirAll(sshDir, 0700)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create .ssh directory: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(sshDir, "config")
|
||||
configContent := `Host test-host
|
||||
HostName test.example.com
|
||||
User testuser
|
||||
`
|
||||
|
||||
err = os.WriteFile(configPath, []byte(configContent), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create config file: %v", err)
|
||||
}
|
||||
|
||||
// Test backup creation
|
||||
err = backupConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("backupConfig() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify backup directory was created
|
||||
backupDir, err := GetSSHMBackupDir()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSSHMBackupDir() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
||||
t.Errorf("Backup directory was not created: %s", backupDir)
|
||||
}
|
||||
|
||||
// Verify backup file was created
|
||||
files, err := os.ReadDir(backupDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read backup directory: %v", err)
|
||||
}
|
||||
|
||||
if len(files) != 1 {
|
||||
t.Errorf("Expected 1 backup file, got %d", len(files))
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
backupFile := files[0]
|
||||
expectedName := "config.backup"
|
||||
if backupFile.Name() != expectedName {
|
||||
t.Errorf("Backup file has unexpected name: got %s, want %s", backupFile.Name(), expectedName)
|
||||
}
|
||||
|
||||
// Verify backup content
|
||||
backupContent, err := os.ReadFile(filepath.Join(backupDir, backupFile.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read backup file: %v", err)
|
||||
}
|
||||
|
||||
if string(backupContent) != configContent {
|
||||
t.Errorf("Backup content doesn't match original")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that subsequent backups overwrite the previous one
|
||||
newConfigContent := `Host test-host-updated
|
||||
HostName updated.example.com
|
||||
User updateduser
|
||||
`
|
||||
|
||||
err = os.WriteFile(configPath, []byte(newConfigContent), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update config file: %v", err)
|
||||
}
|
||||
|
||||
// Create second backup
|
||||
err = backupConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Second backupConfig() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify still only one backup file exists
|
||||
files, err = os.ReadDir(backupDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read backup directory after second backup: %v", err)
|
||||
}
|
||||
|
||||
if len(files) != 1 {
|
||||
t.Errorf("Expected still 1 backup file after overwrite, got %d", len(files))
|
||||
}
|
||||
|
||||
// Verify backup content was updated
|
||||
if len(files) > 0 {
|
||||
backupContent, err := os.ReadFile(filepath.Join(backupDir, files[0].Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated backup file: %v", err)
|
||||
}
|
||||
|
||||
if string(backupContent) != newConfigContent {
|
||||
t.Errorf("Updated backup content doesn't match new config content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindHostInAllConfigs(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tempDir := t.TempDir()
|
||||
|
||||
@ -3,7 +3,7 @@ package connectivity
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"sshm/internal/config"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"sshm/internal/config"
|
||||
)
|
||||
|
||||
// ConnectionHistory represents the history of SSH connections
|
||||
|
||||
@ -4,8 +4,8 @@ import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/validation"
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/validation"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/validation"
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/validation"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
|
||||
@ -3,7 +3,7 @@ package ui
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"sshm/internal/config"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
@ -2,7 +2,7 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"sshm/internal/config"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||
"github.com/Gu1llaum-3/sshm/internal/history"
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/connectivity"
|
||||
"sshm/internal/history"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"sshm/internal/config"
|
||||
)
|
||||
|
||||
// sortHosts sorts hosts according to the current sort mode
|
||||
|
||||
@ -3,8 +3,8 @@ package ui
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/history"
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/history"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
)
|
||||
|
||||
@ -5,9 +5,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||
"github.com/Gu1llaum-3/sshm/internal/history"
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/connectivity"
|
||||
"sshm/internal/history"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
@ -52,7 +52,7 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Search hosts or tags..."
|
||||
ti.CharLimit = 50
|
||||
ti.Width = 25
|
||||
ti.Width = 50
|
||||
|
||||
// Use dynamic column width calculation (will fallback to static if width not available)
|
||||
nameWidth, hostnameWidth, tagsWidth, lastLoginWidth := m.calculateDynamicColumnWidths(sortedHosts)
|
||||
|
||||
@ -6,8 +6,8 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/connectivity"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
@ -2,7 +2,7 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||
"sshm/internal/connectivity"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user