mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-07 13:20:40 +02:00
feat: add Windows platform support
This commit is contained in:
parent
e8c6e602a2
commit
20bc506e36
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
@ -34,6 +34,14 @@ jobs:
|
|||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
suffix: darwin-arm64
|
suffix: darwin-arm64
|
||||||
|
# Windows AMD64
|
||||||
|
- goos: windows
|
||||||
|
goarch: amd64
|
||||||
|
suffix: windows-amd64
|
||||||
|
# Windows ARM64
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
|
suffix: windows-arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -60,19 +68,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
VERSION=${GITHUB_REF#refs/tags/}
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
go build -ldflags="-s -w -X sshm/cmd.version=${VERSION}" -o dist/sshm-${{ matrix.suffix }} .
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
|
go build -ldflags="-s -w -X sshm/cmd.version=${VERSION}" -o dist/sshm-${{ matrix.suffix }}.exe .
|
||||||
|
else
|
||||||
|
go build -ldflags="-s -w -X sshm/cmd.version=${VERSION}" -o dist/sshm-${{ matrix.suffix }} .
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Create tarball
|
- name: Create archive
|
||||||
run: |
|
run: |
|
||||||
cd dist
|
cd dist
|
||||||
tar -czf sshm-${{ matrix.suffix }}.tar.gz sshm-${{ matrix.suffix }}
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
rm sshm-${{ matrix.suffix }}
|
zip sshm-${{ matrix.suffix }}.zip sshm-${{ matrix.suffix }}.exe
|
||||||
|
rm sshm-${{ matrix.suffix }}.exe
|
||||||
|
else
|
||||||
|
tar -czf sshm-${{ matrix.suffix }}.tar.gz sshm-${{ matrix.suffix }}
|
||||||
|
rm sshm-${{ matrix.suffix }}
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: sshm-${{ matrix.suffix }}
|
name: sshm-${{ matrix.suffix }}
|
||||||
path: dist/sshm-${{ matrix.suffix }}.tar.gz
|
path: |
|
||||||
|
dist/sshm-${{ matrix.suffix }}.tar.gz
|
||||||
|
dist/sshm-${{ matrix.suffix }}.zip
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
@ -91,6 +110,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p release
|
mkdir -p release
|
||||||
find ./artifacts -name "*.tar.gz" -exec cp {} ./release/ \;
|
find ./artifacts -name "*.tar.gz" -exec cp {} ./release/ \;
|
||||||
|
find ./artifacts -name "*.zip" -exec cp {} ./release/ \;
|
||||||
ls -la ./release/
|
ls -la ./release/
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
|
36
README.md
36
README.md
@ -9,7 +9,7 @@
|
|||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://github.com/Gu1llaum-3/sshm/releases)
|
[](https://github.com/Gu1llaum-3/sshm/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/Gu1llaum-3/sshm/releases)
|
[](https://github.com/Gu1llaum-3/sshm/releases)
|
||||||
|
|
||||||
> **A modern, interactive SSH Manager for your terminal** 🔥
|
> **A modern, interactive SSH Manager for your terminal** 🔥
|
||||||
|
|
||||||
@ -49,19 +49,26 @@ SSHM is a beautiful command-line tool that transforms how you manage and connect
|
|||||||
### 🎮 **User Experience**
|
### 🎮 **User Experience**
|
||||||
- **Zero configuration** - Works out of the box with your existing SSH setup
|
- **Zero configuration** - Works out of the box with your existing SSH setup
|
||||||
- **Keyboard shortcuts** for power users
|
- **Keyboard shortcuts** for power users
|
||||||
- **Cross-platform** - Supports Linux and macOS (Intel & Apple Silicon)
|
- **Cross-platform** - Supports Linux, macOS (Intel & Apple Silicon), and Windows
|
||||||
- **Lightweight** - Single binary with no dependencies
|
- **Lightweight** - Single binary with no dependencies
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
**One-line install (Recommended):**
|
**Unix/Linux/macOS (One-line install):**
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/unix.sh | bash
|
curl -sSL https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/unix.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
irm https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/windows.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
**Alternative methods:**
|
**Alternative methods:**
|
||||||
|
|
||||||
|
*Linux/macOS:*
|
||||||
```bash
|
```bash
|
||||||
# Download specific release
|
# Download specific release
|
||||||
wget https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-linux-amd64.tar.gz
|
wget https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-linux-amd64.tar.gz
|
||||||
@ -71,6 +78,14 @@ tar -xzf sshm-linux-amd64.tar.gz
|
|||||||
sudo mv sshm-linux-amd64 /usr/local/bin/sshm
|
sudo mv sshm-linux-amd64 /usr/local/bin/sshm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
*Windows:*
|
||||||
|
```powershell
|
||||||
|
# Download and extract
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-windows-amd64.zip" -OutFile "sshm-windows-amd64.zip"
|
||||||
|
Expand-Archive sshm-windows-amd64.zip -DestinationPath C:\tools\
|
||||||
|
# Add C:\tools to your PATH environment variable
|
||||||
|
```
|
||||||
|
|
||||||
## 📖 Usage
|
## 📖 Usage
|
||||||
|
|
||||||
### Interactive Mode
|
### Interactive Mode
|
||||||
@ -153,6 +168,19 @@ sshm add hostname -c /path/to/custom/ssh_config
|
|||||||
sshm edit hostname -c /path/to/custom/ssh_config
|
sshm edit hostname -c /path/to/custom/ssh_config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Notes
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
- SSHM works with the built-in OpenSSH client (Windows 10/11)
|
||||||
|
- Configuration file location: `%USERPROFILE%\.ssh\config`
|
||||||
|
- Compatible with WSL SSH configurations
|
||||||
|
- Supports the same SSH options as Unix systems
|
||||||
|
|
||||||
|
**Unix/Linux/macOS:**
|
||||||
|
- Standard SSH configuration file: `~/.ssh/config`
|
||||||
|
- Full compatibility with OpenSSH features
|
||||||
|
- Preserves file permissions automatically
|
||||||
|
|
||||||
## 🏗️ Configuration
|
## 🏗️ Configuration
|
||||||
|
|
||||||
SSHM works directly with your standard SSH configuration file (`~/.ssh/config`). It adds special comment tags for enhanced functionality while maintaining full compatibility with standard SSH tools.
|
SSHM works directly with your standard SSH configuration file (`~/.ssh/config`). It adds special comment tags for enhanced functionality while maintaining full compatibility with standard SSH tools.
|
||||||
@ -314,6 +342,8 @@ Automated releases are built for multiple platforms:
|
|||||||
| Linux | ARM64 | [sshm-linux-arm64.tar.gz](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-linux-arm64.tar.gz) |
|
| Linux | ARM64 | [sshm-linux-arm64.tar.gz](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-linux-arm64.tar.gz) |
|
||||||
| macOS | Intel | [sshm-darwin-amd64.tar.gz](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-darwin-amd64.tar.gz) |
|
| macOS | Intel | [sshm-darwin-amd64.tar.gz](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-darwin-amd64.tar.gz) |
|
||||||
| macOS | Apple Silicon | [sshm-darwin-arm64.tar.gz](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-darwin-arm64.tar.gz) |
|
| macOS | Apple Silicon | [sshm-darwin-arm64.tar.gz](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-darwin-arm64.tar.gz) |
|
||||||
|
| Windows | AMD64 | [sshm-windows-amd64.zip](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-windows-amd64.zip) |
|
||||||
|
| Windows | ARM64 | [sshm-windows-arm64.zip](https://github.com/Gu1llaum-3/sshm/releases/latest/download/sshm-windows-arm64.zip) |
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
@ -12,8 +12,28 @@ curl -sSL https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/unix.sh
|
|||||||
|
|
||||||
**Note:** When using the pipe method, the installer will automatically proceed with installation if SSHM is already installed.
|
**Note:** When using the pipe method, the installer will automatically proceed with installation if SSHM is already installed.
|
||||||
|
|
||||||
|
## Windows Installation
|
||||||
|
|
||||||
|
### Quick Install (Recommended)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/windows.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
### Install Options
|
### Install Options
|
||||||
|
|
||||||
|
**Force install without prompts:**
|
||||||
|
```powershell
|
||||||
|
iex "& { $(irm https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/windows.ps1) } -Force"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom installation directory:**
|
||||||
|
```powershell
|
||||||
|
iex "& { $(irm https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/windows.ps1) } -InstallDir 'C:\tools'"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unix/Linux/macOS Advanced Options
|
||||||
|
|
||||||
**Force install without prompts:**
|
**Force install without prompts:**
|
||||||
```bash
|
```bash
|
||||||
FORCE_INSTALL=true bash -c "$(curl -sSL https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/unix.sh)"
|
FORCE_INSTALL=true bash -c "$(curl -sSL https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/unix.sh)"
|
||||||
|
107
install/windows.ps1
Normal file
107
install/windows.ps1
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# SSHM Windows Installation Script
|
||||||
|
# Usage: irm https://raw.githubusercontent.com/Gu1llaum-3/sshm/main/install/windows.ps1 | iex
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$InstallDir = "$env:USERPROFILE\bin",
|
||||||
|
[switch]$Force = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
function Write-ColorOutput($ForegroundColor) {
|
||||||
|
$fc = $host.UI.RawUI.ForegroundColor
|
||||||
|
$host.UI.RawUI.ForegroundColor = $ForegroundColor
|
||||||
|
if ($args) {
|
||||||
|
Write-Output $args
|
||||||
|
}
|
||||||
|
$host.UI.RawUI.ForegroundColor = $fc
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Info { Write-ColorOutput Green $args }
|
||||||
|
function Write-Warning { Write-ColorOutput Yellow $args }
|
||||||
|
function Write-Error { Write-ColorOutput Red $args }
|
||||||
|
|
||||||
|
Write-Info "🚀 Installing SSHM - SSH Manager"
|
||||||
|
Write-Info ""
|
||||||
|
|
||||||
|
# Check if SSHM is already installed
|
||||||
|
$existingSSHM = Get-Command sshm -ErrorAction SilentlyContinue
|
||||||
|
if ($existingSSHM -and -not $Force) {
|
||||||
|
$currentVersion = & sshm --version 2>$null | Select-String "version" | ForEach-Object { $_.ToString().Split()[-1] }
|
||||||
|
Write-Warning "SSHM is already installed (version: $currentVersion)"
|
||||||
|
$response = Read-Host "Do you want to continue with the installation? (y/N)"
|
||||||
|
if ($response -ne "y" -and $response -ne "Y") {
|
||||||
|
Write-Info "Installation cancelled."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
$arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" }
|
||||||
|
Write-Info "Detected platform: Windows ($arch)"
|
||||||
|
|
||||||
|
# Get latest version
|
||||||
|
Write-Info "Fetching latest version..."
|
||||||
|
try {
|
||||||
|
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/Gu1llaum-3/sshm/releases/latest"
|
||||||
|
$latestVersion = $latestRelease.tag_name
|
||||||
|
Write-Info "Latest version: $latestVersion"
|
||||||
|
} catch {
|
||||||
|
Write-Error "Failed to fetch latest version"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download binary
|
||||||
|
$fileName = "sshm-windows-$arch.zip"
|
||||||
|
$downloadUrl = "https://github.com/Gu1llaum-3/sshm/releases/download/$latestVersion/$fileName"
|
||||||
|
$tempFile = "$env:TEMP\$fileName"
|
||||||
|
|
||||||
|
Write-Info "Downloading $fileName..."
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile
|
||||||
|
} catch {
|
||||||
|
Write-Error "Download failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create installation directory
|
||||||
|
if (-not (Test-Path $InstallDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract archive
|
||||||
|
Write-Info "Extracting..."
|
||||||
|
try {
|
||||||
|
Expand-Archive -Path $tempFile -DestinationPath $env:TEMP -Force
|
||||||
|
$extractedBinary = "$env:TEMP\sshm-windows-$arch.exe"
|
||||||
|
$targetPath = "$InstallDir\sshm.exe"
|
||||||
|
|
||||||
|
Move-Item -Path $extractedBinary -Destination $targetPath -Force
|
||||||
|
} catch {
|
||||||
|
Write-Error "Extraction failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
Remove-Item $tempFile -Force
|
||||||
|
|
||||||
|
# Check PATH
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
|
if ($userPath -notlike "*$InstallDir*") {
|
||||||
|
Write-Warning "The directory $InstallDir is not in your PATH."
|
||||||
|
Write-Info "Adding to user PATH..."
|
||||||
|
[Environment]::SetEnvironmentVariable("Path", "$userPath;$InstallDir", "User")
|
||||||
|
Write-Info "Please restart your terminal to use the 'sshm' command."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info ""
|
||||||
|
Write-Info "✅ SSHM successfully installed to: $targetPath"
|
||||||
|
Write-Info "You can now use the 'sshm' command!"
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
if (Test-Path $targetPath) {
|
||||||
|
Write-Info ""
|
||||||
|
Write-Info "Verifying installation..."
|
||||||
|
& $targetPath --version
|
||||||
|
}
|
11
internal/config/permissions_unix.go
Normal file
11
internal/config/permissions_unix.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// SetSecureFilePermissions configures secure permissions on Unix systems
|
||||||
|
func SetSecureFilePermissions(filepath string) error {
|
||||||
|
// Set file permissions to 0600 (owner read/write only)
|
||||||
|
return os.Chmod(filepath, 0600)
|
||||||
|
}
|
24
internal/config/permissions_windows.go
Normal file
24
internal/config/permissions_windows.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetSecureFilePermissions configures secure permissions on Windows
|
||||||
|
func SetSecureFilePermissions(filepath string) error {
|
||||||
|
// On Windows, file permissions work differently
|
||||||
|
// We ensure the file is not read-only and has basic permissions
|
||||||
|
info, err := os.Stat(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the file is not read-only
|
||||||
|
if info.Mode()&os.ModeType == 0 {
|
||||||
|
return os.Chmod(filepath, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@ -22,6 +23,46 @@ type SSHHost struct {
|
|||||||
Tags []string
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDefaultSSHConfigPath returns the default SSH config path for the current platform
|
||||||
|
func GetDefaultSSHConfigPath() (string, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return filepath.Join(homeDir, ".ssh", "config"), nil
|
||||||
|
default:
|
||||||
|
// Linux, macOS, etc.
|
||||||
|
return filepath.Join(homeDir, ".ssh", "config"), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSSHDirectory returns the .ssh directory path
|
||||||
|
func GetSSHDirectory() (string, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(homeDir, ".ssh"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureSSHDirectory creates the .ssh directory with appropriate permissions
|
||||||
|
func ensureSSHDirectory() error {
|
||||||
|
sshDir, err := GetSSHDirectory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(sshDir); os.IsNotExist(err) {
|
||||||
|
// 0700 provides owner-only access across platforms
|
||||||
|
return os.MkdirAll(sshDir, 0700)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// configMutex protects SSH config file operations from race conditions
|
// configMutex protects SSH config file operations from race conditions
|
||||||
var configMutex sync.Mutex
|
var configMutex sync.Mutex
|
||||||
|
|
||||||
@ -46,12 +87,10 @@ func backupConfig(configPath string) error {
|
|||||||
|
|
||||||
// ParseSSHConfig parses the SSH config file and returns the list of hosts
|
// ParseSSHConfig parses the SSH config file and returns the list of hosts
|
||||||
func ParseSSHConfig() ([]SSHHost, error) {
|
func ParseSSHConfig() ([]SSHHost, error) {
|
||||||
homeDir, err := os.UserHomeDir()
|
configPath, err := GetDefaultSSHConfigPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath := filepath.Join(homeDir, ".ssh", "config")
|
|
||||||
return ParseSSHConfigFile(configPath)
|
return ParseSSHConfigFile(configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,18 +98,22 @@ func ParseSSHConfig() ([]SSHHost, error) {
|
|||||||
func ParseSSHConfigFile(configPath string) ([]SSHHost, error) {
|
func ParseSSHConfigFile(configPath string) ([]SSHHost, error) {
|
||||||
// Check if the file exists, otherwise create it (and the parent directory if needed)
|
// Check if the file exists, otherwise create it (and the parent directory if needed)
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
dir := filepath.Dir(configPath)
|
// Ensure .ssh directory exists with proper permissions
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if err := ensureSSHDirectory(); err != nil {
|
||||||
err = os.MkdirAll(dir, 0700)
|
return nil, fmt.Errorf("failed to create .ssh directory: %w", err)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create .ssh directory: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.OpenFile(configPath, os.O_CREATE|os.O_WRONLY, 0600)
|
file, err := os.OpenFile(configPath, os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create SSH config file: %w", err)
|
return nil, fmt.Errorf("failed to create SSH config file: %w", err)
|
||||||
}
|
}
|
||||||
file.Close()
|
file.Close()
|
||||||
|
|
||||||
|
// Set secure permissions on the config file
|
||||||
|
if err := SetSecureFilePermissions(configPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set secure permissions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// File created, return empty host list
|
// File created, return empty host list
|
||||||
return []SSHHost{}, nil
|
return []SSHHost{}, nil
|
||||||
}
|
}
|
||||||
@ -181,11 +224,10 @@ func ParseSSHConfigFile(configPath string) ([]SSHHost, error) {
|
|||||||
|
|
||||||
// AddSSHHost adds a new SSH host to the config file
|
// AddSSHHost adds a new SSH host to the config file
|
||||||
func AddSSHHost(host SSHHost) error {
|
func AddSSHHost(host SSHHost) error {
|
||||||
homeDir, err := os.UserHomeDir()
|
configPath, err := GetDefaultSSHConfigPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := filepath.Join(homeDir, ".ssh", "config")
|
|
||||||
return AddSSHHostToFile(host, configPath)
|
return AddSSHHostToFile(host, configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,11 +446,10 @@ func GetSSHHostFromFile(hostName string, configPath string) (*SSHHost, error) {
|
|||||||
|
|
||||||
// UpdateSSHHost updates an existing SSH host configuration
|
// UpdateSSHHost updates an existing SSH host configuration
|
||||||
func UpdateSSHHost(oldName string, newHost SSHHost) error {
|
func UpdateSSHHost(oldName string, newHost SSHHost) error {
|
||||||
homeDir, err := os.UserHomeDir()
|
configPath, err := GetDefaultSSHConfigPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := filepath.Join(homeDir, ".ssh", "config")
|
|
||||||
return UpdateSSHHostInFile(oldName, newHost, configPath)
|
return UpdateSSHHostInFile(oldName, newHost, configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,11 +605,10 @@ func UpdateSSHHostInFile(oldName string, newHost SSHHost, configPath string) err
|
|||||||
|
|
||||||
// DeleteSSHHost removes an SSH host configuration from the config file
|
// DeleteSSHHost removes an SSH host configuration from the config file
|
||||||
func DeleteSSHHost(hostName string) error {
|
func DeleteSSHHost(hostName string) error {
|
||||||
homeDir, err := os.UserHomeDir()
|
configPath, err := GetDefaultSSHConfigPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := filepath.Join(homeDir, ".ssh", "config")
|
|
||||||
return DeleteSSHHostFromFile(hostName, configPath)
|
return DeleteSSHHostFromFile(hostName, configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
73
internal/config/ssh_test.go
Normal file
73
internal/config/ssh_test.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetDefaultSSHConfigPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
goos string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"Linux", "linux", ".ssh/config"},
|
||||||
|
{"macOS", "darwin", ".ssh/config"},
|
||||||
|
{"Windows", "windows", ".ssh/config"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Save original GOOS
|
||||||
|
originalGOOS := runtime.GOOS
|
||||||
|
defer func() {
|
||||||
|
// Note: We can't actually change runtime.GOOS at runtime
|
||||||
|
// This test verifies the function logic with the current OS
|
||||||
|
_ = originalGOOS
|
||||||
|
}()
|
||||||
|
|
||||||
|
configPath, err := GetDefaultSSHConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDefaultSSHConfigPath() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(configPath, tt.expected) {
|
||||||
|
t.Errorf("Expected path to end with %q, got %q", tt.expected, configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the path uses the correct separator for current OS
|
||||||
|
expectedSeparator := string(filepath.Separator)
|
||||||
|
if !strings.Contains(configPath, expectedSeparator) && len(configPath) > len(tt.expected) {
|
||||||
|
t.Errorf("Path should use OS-specific separator %q, got %q", expectedSeparator, configPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSSHDirectory(t *testing.T) {
|
||||||
|
sshDir, err := GetSSHDirectory()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSSHDirectory() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(sshDir, ".ssh") {
|
||||||
|
t.Errorf("Expected directory to end with .ssh, got %q", sshDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the path uses the correct separator for current OS
|
||||||
|
expectedSeparator := string(filepath.Separator)
|
||||||
|
if !strings.Contains(sshDir, expectedSeparator) && len(sshDir) > 4 {
|
||||||
|
t.Errorf("Path should use OS-specific separator %q, got %q", expectedSeparator, sshDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureSSHDirectory(t *testing.T) {
|
||||||
|
// This test just ensures the function doesn't panic
|
||||||
|
// and returns without error when .ssh directory already exists
|
||||||
|
err := ensureSSHDirectory()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ensureSSHDirectory() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user