Skip to content

Commit c61605e

Browse files
committed
Add SSH key upload UI and connection retry view
Introduces a new SSH key upload form accessible from the host list (press 'k'), allowing users to select or paste a public key and upload it via ssh-copy-id, with optional SSH config update. Adds a connection error view with retry/back-to-list options, and refactors the update command to use the install script instead of manual binary replacement. Updates internal state and view handling to support these new features.
1 parent aec5463 commit c61605e

5 files changed

Lines changed: 733 additions & 197 deletions

File tree

cmd/update.go

Lines changed: 10 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
package cmd
22

33
import (
4-
"archive/tar"
5-
"compress/gzip"
64
"context"
75
"fmt"
8-
"io"
9-
"net/http"
106
"os"
117
"os/exec"
12-
"path/filepath"
13-
"runtime"
14-
"strings"
158
"time"
169

1710
"github.com/xvertile/sshc/internal/version"
@@ -30,8 +23,7 @@ var updateCmd = &cobra.Command{
3023
3124
This command will:
3225
1. Check GitHub for the latest release
33-
2. Download the appropriate binary for your system
34-
3. Replace the current binary with the new version
26+
2. Run the install script to update to the latest version
3527
3628
Examples:
3729
sshc update # Update to latest version if available
@@ -69,197 +61,20 @@ func runUpdate(cmd *cobra.Command, args []string) {
6961
fmt.Printf("Reinstalling current version (%s)\n", AppVersion)
7062
}
7163

72-
// Determine OS and architecture
73-
goos := runtime.GOOS
74-
goarch := runtime.GOARCH
64+
fmt.Println("Running install script...")
7565

76-
if goos != "darwin" && goos != "linux" {
77-
fmt.Fprintf(os.Stderr, "Unsupported operating system: %s\n", goos)
78-
os.Exit(1)
79-
}
80-
81-
if goarch != "amd64" && goarch != "arm64" {
82-
fmt.Fprintf(os.Stderr, "Unsupported architecture: %s\n", goarch)
83-
os.Exit(1)
84-
}
85-
86-
// Get the version to download
87-
downloadVersion := updateInfo.LatestVer
88-
if !updateInfo.Available && forceUpdate {
89-
downloadVersion = AppVersion
90-
}
91-
92-
// Strip 'v' prefix for filename
93-
versionNum := strings.TrimPrefix(downloadVersion, "v")
94-
95-
// Construct download URL
96-
filename := fmt.Sprintf("sshc_%s_%s_%s.tar.gz", versionNum, goos, goarch)
97-
downloadURL := fmt.Sprintf("https://github.com/xvertile/sshc/releases/download/%s/%s", downloadVersion, filename)
98-
99-
fmt.Printf("Downloading %s...\n", filename)
100-
101-
// Download the release
102-
resp, err := http.Get(downloadURL)
103-
if err != nil {
104-
fmt.Fprintf(os.Stderr, "Error downloading update: %v\n", err)
105-
os.Exit(1)
106-
}
107-
defer resp.Body.Close()
108-
109-
if resp.StatusCode != http.StatusOK {
110-
fmt.Fprintf(os.Stderr, "Error downloading update: HTTP %d\n", resp.StatusCode)
111-
os.Exit(1)
112-
}
113-
114-
// Create temp directory
115-
tmpDir, err := os.MkdirTemp("", "sshc-update-*")
116-
if err != nil {
117-
fmt.Fprintf(os.Stderr, "Error creating temp directory: %v\n", err)
118-
os.Exit(1)
119-
}
120-
defer os.RemoveAll(tmpDir)
66+
// Run the install script via curl and bash
67+
installCmd := exec.Command("bash", "-c", "curl -fsSL https://raw.githubusercontent.com/xvertile/sshc/main/install/install.sh | bash")
68+
installCmd.Stdin = os.Stdin
69+
installCmd.Stdout = os.Stdout
70+
installCmd.Stderr = os.Stderr
12171

122-
// Extract the tarball
123-
fmt.Println("Extracting...")
124-
if err := extractTarGz(resp.Body, tmpDir); err != nil {
125-
fmt.Fprintf(os.Stderr, "Error extracting update: %v\n", err)
72+
if err := installCmd.Run(); err != nil {
73+
fmt.Fprintf(os.Stderr, "Error running install script: %v\n", err)
12674
os.Exit(1)
12775
}
12876

129-
// Find the binary
130-
newBinary := filepath.Join(tmpDir, "sshc")
131-
if _, err := os.Stat(newBinary); os.IsNotExist(err) {
132-
fmt.Fprintf(os.Stderr, "Error: binary not found in archive\n")
133-
os.Exit(1)
134-
}
135-
136-
// Get current binary path
137-
currentBinary, err := os.Executable()
138-
if err != nil {
139-
fmt.Fprintf(os.Stderr, "Error finding current binary: %v\n", err)
140-
os.Exit(1)
141-
}
142-
143-
// Resolve symlinks
144-
currentBinary, err = filepath.EvalSymlinks(currentBinary)
145-
if err != nil {
146-
fmt.Fprintf(os.Stderr, "Error resolving binary path: %v\n", err)
147-
os.Exit(1)
148-
}
149-
150-
fmt.Printf("Installing to %s...\n", currentBinary)
151-
152-
// Check if we need sudo
153-
needSudo := false
154-
if err := os.Rename(newBinary, currentBinary); err != nil {
155-
if os.IsPermission(err) {
156-
needSudo = true
157-
} else {
158-
// Try copying instead (cross-device link)
159-
if err := copyFile(newBinary, currentBinary); err != nil {
160-
if os.IsPermission(err) {
161-
needSudo = true
162-
} else {
163-
fmt.Fprintf(os.Stderr, "Error installing update: %v\n", err)
164-
os.Exit(1)
165-
}
166-
}
167-
}
168-
}
169-
170-
if needSudo {
171-
fmt.Println("Need elevated privileges to install...")
172-
sudoCmd := exec.Command("sudo", "cp", newBinary, currentBinary)
173-
sudoCmd.Stdin = os.Stdin
174-
sudoCmd.Stdout = os.Stdout
175-
sudoCmd.Stderr = os.Stderr
176-
if err := sudoCmd.Run(); err != nil {
177-
fmt.Fprintf(os.Stderr, "Error installing with sudo: %v\n", err)
178-
os.Exit(1)
179-
}
180-
181-
// Set permissions
182-
chmodCmd := exec.Command("sudo", "chmod", "+x", currentBinary)
183-
if err := chmodCmd.Run(); err != nil {
184-
fmt.Fprintf(os.Stderr, "Error setting permissions: %v\n", err)
185-
os.Exit(1)
186-
}
187-
} else {
188-
// Set permissions
189-
if err := os.Chmod(currentBinary, 0755); err != nil {
190-
fmt.Fprintf(os.Stderr, "Error setting permissions: %v\n", err)
191-
os.Exit(1)
192-
}
193-
}
194-
195-
fmt.Printf("\nSuccessfully updated to %s\n", downloadVersion)
196-
197-
// Show new version
198-
versionCmd := exec.Command(currentBinary, "--version")
199-
versionCmd.Stdout = os.Stdout
200-
versionCmd.Stderr = os.Stderr
201-
versionCmd.Run()
202-
}
203-
204-
// extractTarGz extracts a tar.gz archive to the destination directory
205-
func extractTarGz(r io.Reader, dest string) error {
206-
gzr, err := gzip.NewReader(r)
207-
if err != nil {
208-
return err
209-
}
210-
defer gzr.Close()
211-
212-
tr := tar.NewReader(gzr)
213-
214-
for {
215-
header, err := tr.Next()
216-
if err == io.EOF {
217-
break
218-
}
219-
if err != nil {
220-
return err
221-
}
222-
223-
target := filepath.Join(dest, header.Name)
224-
225-
switch header.Typeflag {
226-
case tar.TypeDir:
227-
if err := os.MkdirAll(target, 0755); err != nil {
228-
return err
229-
}
230-
case tar.TypeReg:
231-
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
232-
if err != nil {
233-
return err
234-
}
235-
236-
if _, err := io.Copy(f, tr); err != nil {
237-
f.Close()
238-
return err
239-
}
240-
f.Close()
241-
}
242-
}
243-
244-
return nil
245-
}
246-
247-
// copyFile copies a file from src to dst
248-
func copyFile(src, dst string) error {
249-
source, err := os.Open(src)
250-
if err != nil {
251-
return err
252-
}
253-
defer source.Close()
254-
255-
destination, err := os.Create(dst)
256-
if err != nil {
257-
return err
258-
}
259-
defer destination.Close()
260-
261-
_, err = io.Copy(destination, source)
262-
return err
77+
fmt.Println("\nUpdate complete!")
26378
}
26479

26580
func init() {

internal/ui/model.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ const (
4848
ViewK8sAdd
4949
ViewK8sEdit
5050
ViewTheme
51+
ViewConnectionError
52+
ViewSSHKeyUpload
5153
)
5254

5355
// PortForwardType defines the type of port forwarding
@@ -127,6 +129,7 @@ type Model struct {
127129
k8sAddForm *k8sAddFormModel
128130
k8sEditForm *k8sEditFormModel
129131
themePicker *themePickerModel
132+
sshKeyUploadForm *sshKeyUploadModel
130133

131134
// Terminal size and styles
132135
width int
@@ -137,6 +140,11 @@ type Model struct {
137140
// Error handling
138141
errorMessage string
139142
showingError bool
143+
144+
// Connection retry state
145+
connectionHost string // Host being connected to
146+
connectionIsK8s bool // Whether it's a k8s host
147+
connectionError string // Last connection error
140148
}
141149

142150
// updateTableStyles updates the table header border color based on focus state

0 commit comments

Comments
 (0)