New Upstream Release - golang-github-charmbracelet-wish

Ready changes

Summary

Merged new upstream version: 1.1.1 (was: 0.6.0).

Diff

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 011f8ad..3081cf0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,7 +4,6 @@ updates:
     directory: "/"
     schedule:
       interval: "daily"
-      time: "08:00"
     labels:
       - "dependencies"
     commit-message:
@@ -14,7 +13,6 @@ updates:
     directory: "/"
     schedule:
       interval: "daily"
-      time: "08:00"
     labels:
       - "dependencies"
     commit-message:
@@ -24,7 +22,6 @@ updates:
     directory: "/"
     schedule:
       interval: "daily"
-      time: "08:00"
     labels:
       - "dependencies"
     commit-message:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3686566..bd84ac3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,24 +6,17 @@ on:
 
 jobs:
   build:
-    strategy:
-      matrix:
-        go-version: [~1.17, ^1]
+    uses: charmbracelet/meta/.github/workflows/build.yml@main
+
+  codecov:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
-
-      - name: Set up Go
-        uses: actions/setup-go@v3
+      - uses: actions/setup-go@v4
         with:
-          go-version: ${{ matrix.go-version }}
-
-      - name: Build
-        run: go build -v ./...
-
-      - name: Test
-        run: go test -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./... -timeout 5m
-
+          go-version: '^1'
+          cache: true
+      - run: go test -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./... -timeout 5m
       - uses: codecov/codecov-action@v3
         with:
           file: ./coverage.txt
diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml
index 1d75f05..afb97fa 100644
--- a/.github/workflows/examples.yml
+++ b/.github/workflows/examples.yml
@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
-      - uses: actions/setup-go@v3
+      - uses: actions/setup-go@v4
         with:
           go-version: '^1'
           cache: true
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
index 55b3662..9bd4548 100644
--- a/.github/workflows/goreleaser.yml
+++ b/.github/workflows/goreleaser.yml
@@ -17,3 +17,14 @@ jobs:
       docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
       gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
       goreleaser_key: ${{ secrets.GORELEASER_KEY }}
+      twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }}
+      twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }}
+      twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
+      twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
+      mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }}
+      mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }}
+      mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }}
+      discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
+      discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
+
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d0bb996..2f13cc3 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -9,8 +9,11 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
-      - name: golangci-lint
-        uses: golangci/golangci-lint-action@v3
+      - uses: actions/setup-go@v4
+        with:
+          go-version: ^1
+          cache: true
+      - uses: golangci/golangci-lint-action@v3
         with:
           # Optional: golangci-lint command line arguments.
           args: --issues-exit-code=0
diff --git a/.golangci.yml b/.golangci.yml
index 85cfb6d..7783944 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -32,3 +32,12 @@ linters:
     - unconvert
     - unparam
     - whitespace
+    - depguard
+
+
+linters-settings:
+  depguard:
+     list-type: 'denylist'
+     packages-with-error-message:
+       - 'github.com/gliderlabs/ssh': 'use github.com/charmbracelet/ssh instead'
+
diff --git a/.goreleaser.yml b/.goreleaser.yml
index eea5c74..9f9bd92 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,3 +1,8 @@
 includes:
   - from_url:
-      url: charmbracelet/meta/main/goreleaser-lib.yaml
\ No newline at end of file
+      url: charmbracelet/meta/main/goreleaser-lib.yaml
+  - from_url:
+      url: charmbracelet/meta/main/goreleaser-announce.yaml
+
+# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
+
diff --git a/README.md b/README.md
index aceb8a5..bcbee5e 100755
--- a/README.md
+++ b/README.md
@@ -111,6 +111,57 @@ Host localhost
     UserKnownHostsFile /dev/null
 ```
 
+## How it works?
+
+Wish uses [gliderlabs/ssh][gliderlabs/ssh] to implement its SSH server, and
+the OpenSSH is never used nor needed — you can even uninstall it if you want to.
+
+Incidentally, there's no risk of accidentally sharing a shell because there's no
+default behavior that does that on Wish.
+
+## Running with SystemD
+
+If you want to run a Wish app with `systemd`, you can create an unit like so:
+
+`/etc/systemd/system/myapp.service`:
+```service
+[Unit]
+Description=My App
+After=network.target
+
+[Service]
+Type=simple
+User=myapp
+Group=myapp
+WorkingDirectory=/home/myapp/
+ExecStart=/usr/bin/myapp
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
+```
+
+You can tune the values bellow, and once you're happy with them, you can run:
+
+```bash
+# need to run this every time you change the unit file
+sudo systemctl daemon-reload
+
+# start/restart/stop/etc:
+sudo systemctl start myapp
+```
+
+If you use a new user for each app (which is good), you'll need to create them
+first:
+
+```bash
+useradd --system --user-group --create-home myapp
+```
+
+That should do it.
+
+###
+
 ## Feedback
 
 We’d love to hear your thoughts on this project. Feel free to drop us a note!
diff --git a/accesscontrol/accesscontrol.go b/accesscontrol/accesscontrol.go
index a268577..5a8b310 100644
--- a/accesscontrol/accesscontrol.go
+++ b/accesscontrol/accesscontrol.go
@@ -4,8 +4,8 @@ package accesscontrol
 import (
 	"fmt"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 // Middleware will exit 1 connections trying to execute commands that are not allowed.
diff --git a/accesscontrol/accesscontrol_test.go b/accesscontrol/accesscontrol_test.go
index 42f6f35..580ec97 100644
--- a/accesscontrol/accesscontrol_test.go
+++ b/accesscontrol/accesscontrol_test.go
@@ -4,9 +4,9 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/accesscontrol"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
diff --git a/activeterm/activeterm.go b/activeterm/activeterm.go
index 38cd2e5..8ed590c 100644
--- a/activeterm/activeterm.go
+++ b/activeterm/activeterm.go
@@ -4,8 +4,8 @@ package activeterm
 import (
 	"fmt"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 // Middleware will exit 1 connections trying with no active terminals.
diff --git a/activeterm/activeterm_test.go b/activeterm/activeterm_test.go
index 2bf17fd..e0cebd3 100644
--- a/activeterm/activeterm_test.go
+++ b/activeterm/activeterm_test.go
@@ -3,9 +3,9 @@ package activeterm_test
 import (
 	"testing"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/activeterm"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
diff --git a/bubbletea/tea.go b/bubbletea/tea.go
index a4fe896..ff17737 100644
--- a/bubbletea/tea.go
+++ b/bubbletea/tea.go
@@ -2,12 +2,11 @@
 package bubbletea
 
 import (
-	"log"
-
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 	"github.com/muesli/termenv"
 )
 
@@ -67,7 +66,6 @@ func MiddlewareWithProgramHandler(bth ProgramHandler, cp termenv.Profile) wish.M
 	return func(sh ssh.Handler) ssh.Handler {
 		lipgloss.SetColorProfile(cp)
 		return func(s ssh.Session) {
-			errc := make(chan error, 1)
 			p := bth(s)
 			if p != nil {
 				_, windowChanges, _ := s.Pty()
@@ -77,19 +75,18 @@ func MiddlewareWithProgramHandler(bth ProgramHandler, cp termenv.Profile) wish.M
 						case <-s.Context().Done():
 							if p != nil {
 								p.Quit()
+								return
 							}
 						case w := <-windowChanges:
 							if p != nil {
 								p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
 							}
-						case err := <-errc:
-							if err != nil {
-								log.Print(err)
-							}
 						}
 					}
 				}()
-				errc <- p.Start()
+				if _, err := p.Run(); err != nil {
+					log.Error("app exit with error", "error", err)
+				}
 				// p.Kill() will force kill the program if it's still running,
 				// and restore the terminal to its original state in case of a
 				// tui crash
diff --git a/comment/comment.go b/comment/comment.go
index 3e883f4..8b45153 100644
--- a/comment/comment.go
+++ b/comment/comment.go
@@ -1,8 +1,8 @@
 package comment
 
 import (
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 // Middleware prints a comment at the end of the session.
diff --git a/comment/comment_test.go b/comment/comment_test.go
index 05b46e2..841a5e9 100644
--- a/comment/comment_test.go
+++ b/comment/comment_test.go
@@ -3,8 +3,8 @@ package comment
 import (
 	"testing"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
diff --git a/debian/changelog b/debian/changelog
index ccaa22a..2adac9e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,12 @@
-golang-github-charmbracelet-wish (0.6.0-1) UNRELEASED; urgency=medium
+golang-github-charmbracelet-wish (1.1.1-1) UNRELEASED; urgency=medium
 
+  [ Martin Dosch ]
   * New upstream release.
 
- -- Martin Dosch <martin@mdosch.de>  Sun, 30 Oct 2022 09:37:45 +0000
+  [ Debian Janitor ]
+  * New upstream release.
+
+ -- Martin Dosch <martin@mdosch.de>  Mon, 05 Jun 2023 11:25:40 -0000
 
 golang-github-charmbracelet-wish (0.1.1-2) unstable; urgency=medium
 
diff --git a/elapsed/elapsed.go b/elapsed/elapsed.go
index 24993f3..5b0885b 100644
--- a/elapsed/elapsed.go
+++ b/elapsed/elapsed.go
@@ -3,8 +3,8 @@ package timer
 import (
 	"time"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 // MiddlewareWithFormat returns a middleware that logs the elapsed time of the
diff --git a/elapsed/elapsed_test.go b/elapsed/elapsed_test.go
index 7802fcd..efb5305 100644
--- a/elapsed/elapsed_test.go
+++ b/elapsed/elapsed_test.go
@@ -4,14 +4,12 @@ import (
 	"testing"
 	"time"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
-var (
-	waitDuration = time.Second
-)
+var waitDuration = time.Second
 
 func TestMiddleware(t *testing.T) {
 	t.Run("recover session", func(t *testing.T) {
diff --git a/examples/bubbletea/main.go b/examples/bubbletea/main.go
index e14b0a0..989a9d7 100644
--- a/examples/bubbletea/main.go
+++ b/examples/bubbletea/main.go
@@ -5,18 +5,19 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	bm "github.com/charmbracelet/wish/bubbletea"
 	lm "github.com/charmbracelet/wish/logging"
-	"github.com/gliderlabs/ssh"
 )
 
 const (
@@ -34,24 +35,24 @@ func main() {
 		),
 	)
 	if err != nil {
-		log.Fatalln(err)
+		log.Error("could not start server", "error", err)
 	}
 
 	done := make(chan os.Signal, 1)
 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	log.Printf("Starting SSH server on %s:%d", host, port)
+	log.Info("Starting SSH server", "host", host, "port", port)
 	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			log.Fatalln(err)
+		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+			log.Error("could not start server", "error", err)
 		}
 	}()
 
 	<-done
-	log.Println("Stopping SSH server")
+	log.Info("Stopping SSH server")
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		log.Fatalln(err)
+	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+		log.Error("could not stop server", "error", err)
 	}
 }
 
diff --git a/examples/bubbleteaprogram/main.go b/examples/bubbleteaprogram/main.go
index 9fcc372..174851e 100644
--- a/examples/bubbleteaprogram/main.go
+++ b/examples/bubbleteaprogram/main.go
@@ -5,18 +5,19 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	bm "github.com/charmbracelet/wish/bubbletea"
 	lm "github.com/charmbracelet/wish/logging"
-	"github.com/gliderlabs/ssh"
 	"github.com/muesli/termenv"
 )
 
@@ -35,24 +36,24 @@ func main() {
 		),
 	)
 	if err != nil {
-		log.Fatalln(err)
+		log.Error("could not start server", "error", err)
 	}
 
 	done := make(chan os.Signal, 1)
 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	log.Printf("Starting SSH server on %s:%d", host, port)
+	log.Info("Starting SSH server", "host", host, "port", port)
 	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			log.Fatalln(err)
+		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+			log.Error("could not start server", "error", err)
 		}
 	}()
 
 	<-done
-	log.Println("Stopping SSH server")
+	log.Info("Stopping SSH server")
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		log.Fatalln(err)
+	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+		log.Error("could not stop server", "error", err)
 	}
 }
 
diff --git a/examples/cobra/main.go b/examples/cobra/main.go
index 0f74dd1..cb91eda 100644
--- a/examples/cobra/main.go
+++ b/examples/cobra/main.go
@@ -2,16 +2,17 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	"github.com/charmbracelet/wish/logging"
-	"github.com/gliderlabs/ssh"
 	"github.com/spf13/cobra"
 )
 
@@ -57,7 +58,7 @@ func main() {
 					rootCmd.SetErr(s.Stderr())
 					rootCmd.CompletionOptions.DisableDefaultCmd = true
 					if err := rootCmd.Execute(); err != nil {
-						s.Exit(1)
+						_ = s.Exit(1)
 						return
 					}
 
@@ -68,23 +69,23 @@ func main() {
 		),
 	)
 	if err != nil {
-		log.Fatalln(err)
+		log.Error("could not start server", "error", err)
 	}
 
 	done := make(chan os.Signal, 1)
 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	log.Printf("Starting SSH server on %s:%d", host, port)
+	log.Info("Starting SSH server", "host", host, "port", port)
 	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			log.Fatalln(err)
+		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+			log.Error("could not start server", "error", err)
 		}
 	}()
 
 	<-done
-	log.Println("Stopping SSH server")
+	log.Info("Stopping SSH server")
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		log.Fatalln(err)
+	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+		log.Error("could not stop server", "error", err)
 	}
 }
diff --git a/examples/git/main.go b/examples/git/main.go
index c54dab6..4125d87 100644
--- a/examples/git/main.go
+++ b/examples/git/main.go
@@ -5,18 +5,19 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io/fs"
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	gm "github.com/charmbracelet/wish/git"
 	lm "github.com/charmbracelet/wish/logging"
-	"github.com/gliderlabs/ssh"
 )
 
 const (
@@ -34,11 +35,11 @@ func (a app) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel {
 }
 
 func (a app) Push(repo string, pk ssh.PublicKey) {
-	log.Printf("pushed %s", repo)
+	log.Info("push", "repo", repo)
 }
 
 func (a app) Fetch(repo string, pk ssh.PublicKey) {
-	log.Printf("fetch %s", repo)
+	log.Info("fetch", "repo", repo)
 }
 
 func passHandler(ctx ssh.Context, password string) bool {
@@ -65,24 +66,24 @@ func main() {
 		),
 	)
 	if err != nil {
-		log.Fatalln(err)
+		log.Error("could not start server", "error", err)
 	}
 
 	done := make(chan os.Signal, 1)
 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	log.Printf("Starting SSH server on %s:%d", host, port)
+	log.Info("Starting SSH server", "host", host, "port", port)
 	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			log.Fatalln(err)
+		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+			log.Error("could not start server", "error", err)
 		}
 	}()
 
 	<-done
-	log.Println("Stopping SSH server")
+	log.Info("Stopping SSH server")
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		log.Fatalln(err)
+	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+		log.Error("could not stop server", "error", err)
 	}
 }
 
@@ -96,7 +97,7 @@ func gitListMiddleware(h ssh.Handler) ssh.Handler {
 		if len(s.Command()) == 0 {
 			des, err := os.ReadDir(repoDir)
 			if err != nil && err != fs.ErrNotExist {
-				log.Println(err)
+				log.Error("invalid repository", "error", err)
 			}
 			if len(des) > 0 {
 				fmt.Fprintf(s, "\n### Repo Menu ###\n\n")
diff --git a/examples/go.mod b/examples/go.mod
index a1f5ebd..2f31522 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -3,47 +3,53 @@ module examples
 go 1.18
 
 require (
-	github.com/charmbracelet/bubbletea v0.22.1
+	github.com/charmbracelet/bubbletea v0.23.2
+	github.com/charmbracelet/log v0.1.2
+	github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
 	github.com/charmbracelet/wish v0.5.0
-	github.com/gliderlabs/ssh v0.3.5
-	github.com/muesli/termenv v0.12.0
+	github.com/muesli/termenv v0.15.1
 	github.com/spf13/cobra v1.5.0
 )
 
 require (
-	github.com/Microsoft/go-winio v0.4.16 // indirect
-	github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
-	github.com/acomagu/bufpipe v1.0.3 // indirect
+	github.com/Microsoft/go-winio v0.5.2 // indirect
+	github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
+	github.com/acomagu/bufpipe v1.0.4 // indirect
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/caarlos0/sshmarshal v0.1.0 // indirect
-	github.com/charmbracelet/keygen v0.3.0 // indirect
-	github.com/charmbracelet/lipgloss v0.6.0 // indirect
+	github.com/charmbracelet/keygen v0.4.1 // indirect
+	github.com/charmbracelet/lipgloss v0.7.1 // indirect
+	github.com/cloudflare/circl v1.1.0 // indirect
 	github.com/containerd/console v1.0.3 // indirect
-	github.com/emirpasic/gods v1.12.0 // indirect
+	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/go-git/gcfg v1.5.0 // indirect
-	github.com/go-git/go-billy/v5 v5.3.1 // indirect
-	github.com/go-git/go-git/v5 v5.4.2 // indirect
-	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/go-git/go-billy/v5 v5.4.1 // indirect
+	github.com/go-git/go-git/v5 v5.6.1 // indirect
+	github.com/go-logfmt/logfmt v0.6.0 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
-	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
+	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
-	github.com/mattn/go-isatty v0.0.16 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
-	github.com/mattn/go-runewidth v0.0.13 // indirect
-	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/reflow v0.3.0 // indirect
+	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/sergi/go-diff v1.1.0 // indirect
+	github.com/skeema/knownhosts v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/xanzy/ssh-agent v0.3.0 // indirect
-	golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
-	golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
-	golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
-	golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
-	golang.org/x/text v0.3.7 // indirect
+	github.com/xanzy/ssh-agent v0.3.3 // indirect
+	golang.org/x/crypto v0.8.0 // indirect
+	golang.org/x/net v0.9.0 // indirect
+	golang.org/x/sync v0.1.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+	golang.org/x/term v0.7.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 )
 
diff --git a/examples/go.sum b/examples/go.sum
index c770cea..2ec754a 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -1,23 +1,32 @@
-github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
-github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
-github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
-github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
-github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
+github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
+github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
+github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
 github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
-github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24=
-github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0=
-github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
-github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
-github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
+github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
+github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
+github.com/charmbracelet/keygen v0.4.1 h1:ylwHCcCrb4UL2nHrUvVwME+/RFACcX1sjopOrIkc14g=
+github.com/charmbracelet/keygen v0.4.1/go.mod h1:4e4FT3HSdLU/u83RfJWvzJIaVb8aX4MxtDlfXwpDJaI=
 github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
+github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
+github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
+github.com/charmbracelet/log v0.1.2 h1:xmKMxo0T/lcftgggQOhUkS32exku2/ID55FGYbr4nKQ=
+github.com/charmbracelet/log v0.1.2/go.mod h1:86XdIdmrubqtL/6u0z+jGFol1bQejBGG/qPSTwGZuQQ=
+github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103 h1:wpHMERIN0pQZE635jWwT1dISgfjbpUcEma+fbPKSMCU=
+github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103/go.mod h1:0Vm2/8yBljiLDnGJHU8ehswfawrEybGk33j5ssqKQVM=
+github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
+github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -25,33 +34,32 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
-github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
-github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
-github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
 github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
 github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
 github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
-github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
 github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
-github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
-github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
-github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
+github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
+github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
+github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
+github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
+github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -62,18 +70,18 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
-github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -82,11 +90,12 @@ github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIW
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc=
-github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
+github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
+github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
+github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -97,56 +106,94 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0=
+github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag=
 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
 github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
-github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
-golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
 golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -156,8 +203,9 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/examples/identity/main.go b/examples/identity/main.go
index 13c9f17..36f0976 100644
--- a/examples/identity/main.go
+++ b/examples/identity/main.go
@@ -2,16 +2,17 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	"github.com/charmbracelet/wish/logging"
-	"github.com/gliderlabs/ssh"
 )
 
 const (
@@ -45,23 +46,23 @@ func main() {
 		),
 	)
 	if err != nil {
-		log.Fatalln(err)
+		log.Error("could not start server", "error", err)
 	}
 
 	done := make(chan os.Signal, 1)
 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	log.Printf("Starting SSH server on %s:%d", host, port)
+	log.Info("Starting SSH server", "host", host, "port", port)
 	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			log.Fatalln(err)
+		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+			log.Error("could not start server", "error", err)
 		}
 	}()
 
 	<-done
-	log.Println("Stopping SSH server")
+	log.Info("Stopping SSH server")
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		log.Fatalln(err)
+	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+		log.Error("could not stop server", "error", err)
 	}
 }
diff --git a/examples/scp/main.go b/examples/scp/main.go
index d0086d7..77d6232 100644
--- a/examples/scp/main.go
+++ b/examples/scp/main.go
@@ -4,19 +4,23 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	"github.com/charmbracelet/wish/scp"
 )
 
-const host = "localhost"
-const port = 23234
+const (
+	host = "localhost"
+	port = 23235
+)
 
 func main() {
 	handler := scp.NewFileSystemHandler("./examples/scp/testdata")
@@ -28,23 +32,23 @@ func main() {
 		),
 	)
 	if err != nil {
-		log.Fatalln(err)
+		log.Error("could not start server", "error", err)
 	}
 
 	done := make(chan os.Signal, 1)
 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	log.Printf("Starting SSH server on %s:%d", host, port)
+	log.Info("Starting SSH server", "host", host, "port", port)
 	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			log.Fatalln(err)
+		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+			log.Error("could not start server", "error", err)
 		}
 	}()
 
 	<-done
-	log.Println("Stopping SSH server")
+	log.Info("Stopping SSH server")
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		log.Fatalln(err)
+	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+		log.Error("could not stop server", "error", err)
 	}
 }
diff --git a/examples/simple/main.go b/examples/simple/main.go
index 2faa474..163d788 100644
--- a/examples/simple/main.go
+++ b/examples/simple/main.go
@@ -2,16 +2,17 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
 	"github.com/charmbracelet/wish/logging"
-	"github.com/gliderlabs/ssh"
 )
 
 const (
@@ -34,23 +35,23 @@ func main() {
 		),
 	)
 	if err != nil {
-		log.Fatalln(err)
+		log.Error("could not start server", "error", err)
 	}
 
 	done := make(chan os.Signal, 1)
 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	log.Printf("Starting SSH server on %s:%d", host, port)
+	log.Info("Starting SSH server", "host", host, "port", port)
 	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			log.Fatalln(err)
+		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+			log.Error("could not start server", "error", err)
 		}
 	}()
 
 	<-done
-	log.Println("Stopping SSH server")
+	log.Info("Stopping SSH server")
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		log.Fatalln(err)
+	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+		log.Error("could not stop server", "error", err)
 	}
 }
diff --git a/git/git.go b/git/git.go
index 811675f..dc5c012 100644
--- a/git/git.go
+++ b/git/git.go
@@ -3,14 +3,14 @@ package git
 import (
 	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"strings"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
 )
@@ -70,9 +70,13 @@ func Middleware(repoDir string, gh Hooks) wish.Middleware {
 			cmd := s.Command()
 			if len(cmd) == 2 {
 				gc := cmd[0]
-				repo := cmd[1] // cmd[1] should be `/REPO`
+				// repo should be in the form of "repo.git" or "user/repo.git"
+				repo := strings.TrimSuffix(strings.TrimPrefix(cmd[1], "/"), "/")
 				repo = filepath.Clean(repo)
-				repo = filepath.Base(repo)
+				if n := strings.Count(repo, "/"); n > 1 {
+					Fatal(s, ErrInvalidRepo)
+					return
+				}
 				pk := s.PublicKey()
 				access := gh.AuthRepo(repo, pk)
 				switch gc {
@@ -99,7 +103,7 @@ func Middleware(repoDir string, gh Hooks) wish.Middleware {
 						case nil:
 							gh.Fetch(repo, pk)
 						default:
-							log.Printf("unknown git error: %s", err)
+							log.Error("unknown git error", "error", err)
 							Fatal(s, ErrSystemMalfunction)
 						}
 					default:
@@ -139,6 +143,7 @@ func gitPack(s ssh.Session, gitCmd string, repoDir string, repo string) error {
 		if err != nil {
 			return err
 		}
+		// Needed for git dumb http server
 		return runGit(s, rp, "update-server-info")
 	default:
 		return fmt.Errorf("unknown git command: %s", gitCmd)
@@ -171,7 +176,7 @@ func ensureRepo(dir string, repo string) error {
 		return err
 	}
 	if !exists {
-		err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0700))
+		err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0o700))
 		if err != nil {
 			return err
 		}
diff --git a/git/git_test.go b/git/git_test.go
index 65aa9e8..aa59d93 100644
--- a/git/git_test.go
+++ b/git/git_test.go
@@ -3,19 +3,20 @@ package git
 import (
 	"fmt"
 	"net"
-	"os"
 	"os/exec"
 	"path/filepath"
+	"runtime"
 	"sync"
 	"testing"
 
 	"github.com/charmbracelet/keygen"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 func TestGitMiddleware(t *testing.T) {
 	pubkey, pkPath := createKeyPair(t)
+	hkPath := filepath.Join(t.TempDir(), "id_ed25519")
 
 	l, err := net.Listen("tcp", "127.0.0.1:0")
 	requireNoError(t, err)
@@ -33,9 +34,12 @@ func TestGitMiddleware(t *testing.T) {
 			{pubkey, "repo5", NoAccess},
 			{pubkey, "repo6", ReadOnlyAccess},
 			{pubkey, "repo7", AdminAccess},
+			{pubkey, "abc/repo1", AdminAccess},
+			{pubkey, "abc/def/repo1", AdminAccess},
 		},
 	}
 	srv, err := wish.NewServer(
+		wish.WithHostKeyPath(hkPath),
 		wish.WithMiddleware(Middleware(repoDir, hooks)),
 		wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
 			return true
@@ -63,6 +67,34 @@ func TestGitMiddleware(t *testing.T) {
 		requireHasAction(t, hooks.pushes, pubkey, "repo2")
 	})
 
+	t.Run("create repo in subdir", func(t *testing.T) {
+		if runtime.GOOS == "windows" {
+			t.Skip("permission issues")
+		}
+		cwd := t.TempDir()
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "main"))
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/abc/repo1"))
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit"))
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main"))
+		requireHasAction(t, hooks.pushes, pubkey, "abc/repo1")
+	})
+
+	t.Run("create wrong repo", func(t *testing.T) {
+		cwd := t.TempDir()
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "main"))
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"//../../repo1"))
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit"))
+		requireError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main"))
+	})
+
+	t.Run("create wrong repo in subdir", func(t *testing.T) {
+		cwd := t.TempDir()
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "main"))
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/abc/def/repo1"))
+		requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit"))
+		requireError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main"))
+	})
+
 	t.Run("create and clone repo", func(t *testing.T) {
 		cwd := t.TempDir()
 		requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "main"))
@@ -77,7 +109,7 @@ func TestGitMiddleware(t *testing.T) {
 		requireHasAction(t, hooks.fetches, pubkey, "repo3")
 	})
 
-	t.Run("clone repo that doesnt exist", func(t *testing.T) {
+	t.Run("clone repo that doesn't exist", func(t *testing.T) {
 		cwd := t.TempDir()
 		requireError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo4"))
 	})
@@ -125,7 +157,7 @@ func runGitHelper(t *testing.T, pk, cwd string, args ...string) error {
 
 	cmd := exec.Command("git", allArgs...)
 	cmd.Dir = cwd
-	cmd.Env = []string{fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i %s -F /dev/null`, pk)}
+	cmd.Env = []string{fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i "%s" -F /dev/null`, pk)}
 	out, err := cmd.CombinedOutput()
 	if err != nil {
 		t.Log("git out:", string(out))
@@ -163,15 +195,10 @@ func requireHasAction(t *testing.T, actions []action, key ssh.PublicKey, repo st
 func createKeyPair(t *testing.T) (ssh.PublicKey, string) {
 	t.Helper()
 
-	keyDir := t.TempDir()
-	_, err := keygen.NewWithWrite(filepath.Join(keyDir, "id"), nil, keygen.Ed25519)
-	requireNoError(t, err)
-	pk := filepath.Join(keyDir, "id_ed25519")
-	pubBytes, err := os.ReadFile(filepath.Join(keyDir, "id_ed25519.pub"))
-	requireNoError(t, err)
-	pubkey, _, _, _, err := ssh.ParseAuthorizedKey(pubBytes)
+	pk := filepath.Join(t.TempDir(), "id_ed25519")
+	kp, err := keygen.New(pk, keygen.WithKeyType(keygen.Ed25519), keygen.WithWrite())
 	requireNoError(t, err)
-	return pubkey, pk
+	return kp.PublicKey(), pk
 }
 
 type accessDetails struct {
diff --git a/go.mod b/go.mod
index 3a1f187..8e6ea67 100644
--- a/go.mod
+++ b/go.mod
@@ -1,49 +1,54 @@
 module github.com/charmbracelet/wish
 
-go 1.17
+go 1.18
 
 require (
-	github.com/charmbracelet/bubbletea v0.22.1
-	github.com/charmbracelet/keygen v0.3.0
-	github.com/charmbracelet/lipgloss v0.6.0
-	github.com/gliderlabs/ssh v0.3.5
-	github.com/go-git/go-git/v5 v5.4.2
-	github.com/hashicorp/golang-lru v0.5.4
-	github.com/matryer/is v1.4.0
-	github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
-	golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
-	golang.org/x/time v0.0.0-20220411224347-583f2d630306
+	github.com/charmbracelet/bubbletea v0.23.2
+	github.com/charmbracelet/keygen v0.4.2
+	github.com/charmbracelet/lipgloss v0.7.1
+	github.com/charmbracelet/log v0.2.1
+	github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
+	github.com/go-git/go-git/v5 v5.6.1
+	github.com/google/go-cmp v0.5.9
+	github.com/hashicorp/golang-lru/v2 v2.0.2
+	github.com/matryer/is v1.4.1
+	github.com/muesli/termenv v0.15.1
+	golang.org/x/crypto v0.8.0
+	golang.org/x/sync v0.1.0
+	golang.org/x/time v0.3.0
 )
 
 require (
-	github.com/Microsoft/go-winio v0.4.16 // indirect
-	github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
-	github.com/acomagu/bufpipe v1.0.3 // indirect
+	github.com/Microsoft/go-winio v0.5.2 // indirect
+	github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
+	github.com/acomagu/bufpipe v1.0.4 // indirect
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/caarlos0/sshmarshal v0.1.0 // indirect
+	github.com/cloudflare/circl v1.1.0 // indirect
 	github.com/containerd/console v1.0.3 // indirect
-	github.com/emirpasic/gods v1.12.0 // indirect
+	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/go-git/gcfg v1.5.0 // indirect
-	github.com/go-git/go-billy/v5 v5.3.1 // indirect
-	github.com/google/go-cmp v0.5.5 // indirect
-	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/go-git/go-billy/v5 v5.4.1 // indirect
+	github.com/go-logfmt/logfmt v0.6.0 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
-	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
+	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
-	github.com/mattn/go-isatty v0.0.16 // indirect
+	github.com/mattn/go-isatty v0.0.18 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
-	github.com/mattn/go-runewidth v0.0.13 // indirect
-	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/reflow v0.3.0 // indirect
+	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/sergi/go-diff v1.1.0 // indirect
-	github.com/xanzy/ssh-agent v0.3.0 // indirect
-	golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
-	golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
-	golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
-	golang.org/x/text v0.3.7 // indirect
+	github.com/skeema/knownhosts v1.1.0 // indirect
+	github.com/xanzy/ssh-agent v0.3.3 // indirect
+	golang.org/x/net v0.9.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+	golang.org/x/term v0.7.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 )
diff --git a/go.sum b/go.sum
index cd5b936..80f81a4 100644
--- a/go.sum
+++ b/go.sum
@@ -1,58 +1,65 @@
-github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
-github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
-github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
-github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
-github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
+github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
+github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
+github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
-github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
+github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
 github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
-github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24=
-github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0=
-github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
-github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
-github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
-github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
+github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
+github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
+github.com/charmbracelet/keygen v0.4.1 h1:ylwHCcCrb4UL2nHrUvVwME+/RFACcX1sjopOrIkc14g=
+github.com/charmbracelet/keygen v0.4.1/go.mod h1:4e4FT3HSdLU/u83RfJWvzJIaVb8aX4MxtDlfXwpDJaI=
+github.com/charmbracelet/keygen v0.4.2 h1:TNHua2MlXc6W1dQB2iW4msSZGKlb8RtxtmYDWUs4iRw=
+github.com/charmbracelet/keygen v0.4.2/go.mod h1:4e4FT3HSdLU/u83RfJWvzJIaVb8aX4MxtDlfXwpDJaI=
+github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
+github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
+github.com/charmbracelet/log v0.2.1 h1:1z7jpkk4yKyjwlmKmKMM5qnEDSpV32E7XtWhuv0mTZE=
+github.com/charmbracelet/log v0.2.1/go.mod h1:GwFfjewhcVDWLrpAbY5A0Hin9YOlEn40eWT4PNaxFT4=
+github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103 h1:wpHMERIN0pQZE635jWwT1dISgfjbpUcEma+fbPKSMCU=
+github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103/go.mod h1:0Vm2/8yBljiLDnGJHU8ehswfawrEybGk33j5ssqKQVM=
+github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
+github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
-github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
-github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
-github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
 github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
 github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
 github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
-github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
 github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
-github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
-github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
-github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
-github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
+github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
+github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
+github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
+github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
+github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU=
+github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -63,31 +70,30 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
-github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
-github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
-github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
+github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
+github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -97,58 +103,87 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0=
+github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
-github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
-golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
 golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
-golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -158,7 +193,7 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/logging/logging.go b/logging/logging.go
index e083ae0..8bd69b8 100644
--- a/logging/logging.go
+++ b/logging/logging.go
@@ -1,11 +1,11 @@
 package logging
 
 import (
-	"log"
 	"time"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 // Middleware provides basic connection logging. Connects are logged with the
@@ -15,7 +15,7 @@ import (
 //
 // The logger is set to the std default logger.
 func Middleware() wish.Middleware {
-	return MiddlewareWithLogger(log.Default())
+	return MiddlewareWithLogger(log.StandardLog())
 }
 
 // Logger is the interface that wraps the basic Log method.
@@ -27,15 +27,28 @@ type Logger interface {
 // remote address, invoked command, TERM setting, window dimensions and if the
 // auth was public key based. Disconnect will log the remote address and
 // connection duration.
-func MiddlewareWithLogger(l Logger) wish.Middleware {
+func MiddlewareWithLogger(logger Logger) wish.Middleware {
 	return func(sh ssh.Handler) ssh.Handler {
 		return func(s ssh.Session) {
 			ct := time.Now()
 			hpk := s.PublicKey() != nil
 			pty, _, _ := s.Pty()
-			l.Printf("%s connect %s %v %v %s %v %v\n", s.User(), s.RemoteAddr().String(), hpk, s.Command(), pty.Term, pty.Window.Width, pty.Window.Height)
+			logger.Printf(
+				"%s connect %s %v %v %s %v %v",
+				s.User(),
+				s.RemoteAddr().String(),
+				hpk,
+				s.Command(),
+				pty.Term,
+				pty.Window.Width,
+				pty.Window.Height,
+			)
 			sh(s)
-			l.Printf("%s disconnect %s\n", s.RemoteAddr().String(), time.Since(ct))
+			logger.Printf(
+				"%s disconnect %s\n",
+				s.RemoteAddr().String(),
+				time.Since(ct),
+			)
 		}
 	}
 }
diff --git a/logging/logging_test.go b/logging/logging_test.go
index 9b61e60..9dcb6e4 100644
--- a/logging/logging_test.go
+++ b/logging/logging_test.go
@@ -3,9 +3,9 @@ package logging_test
 import (
 	"testing"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/logging"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
diff --git a/options.go b/options.go
index 0d9254b..5272def 100644
--- a/options.go
+++ b/options.go
@@ -4,15 +4,14 @@ import (
 	"bufio"
 	"bytes"
 	"errors"
-	"fmt"
 	"io"
 	"os"
-	"path/filepath"
 	"strings"
 	"time"
 
 	"github.com/charmbracelet/keygen"
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
@@ -51,15 +50,12 @@ func WithMiddleware(mw ...Middleware) ssh.Option {
 // WithHostKeyFile returns an ssh.Option that sets the path to the private.
 func WithHostKeyPath(path string) ssh.Option {
 	if _, err := os.Stat(path); os.IsNotExist(err) {
-		dir, f := filepath.Split(path)
-		n := strings.TrimSuffix(f, "_ed25519")
-		_, err := keygen.NewWithWrite(filepath.Join(dir, n), nil, keygen.Ed25519)
+		_, err := keygen.New(path, keygen.WithKeyType(keygen.Ed25519), keygen.WithWrite())
 		if err != nil {
 			return func(*ssh.Server) error {
 				return err
 			}
 		}
-		path = filepath.Join(dir, n+"_ed25519")
 	}
 	return ssh.HostKeyFile(path)
 }
@@ -72,17 +68,13 @@ func WithHostKeyPEM(pem []byte) ssh.Option {
 // WithAuthorizedKeys allows to use a SSH authorized_keys file to allowlist users.
 func WithAuthorizedKeys(path string) ssh.Option {
 	return func(s *ssh.Server) error {
-		keys, err := parseAuthorizedKeys(path)
-		if err != nil {
+		if _, err := os.Stat(path); err != nil {
 			return err
 		}
 		return WithPublicKeyAuth(func(_ ssh.Context, key ssh.PublicKey) bool {
-			for _, upk := range keys {
-				if ssh.KeysEqual(upk, key) {
-					return true
-				}
-			}
-			return false
+			return isAuthorized(path, func(k ssh.PublicKey) bool {
+				return ssh.KeysEqual(key, k)
+			})
 		})(s)
 	}
 }
@@ -92,11 +84,9 @@ func WithAuthorizedKeys(path string) ssh.Option {
 // Analogous to the TrustedUserCAKeys OpenSSH option.
 func WithTrustedUserCAKeys(path string) ssh.Option {
 	return func(s *ssh.Server) error {
-		cas, err := parseAuthorizedKeys(path)
-		if err != nil {
+		if _, err := os.Stat(path); err != nil {
 			return err
 		}
-
 		return WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
 			cert, ok := key.(*gossh.Certificate)
 			if !ok {
@@ -104,38 +94,33 @@ func WithTrustedUserCAKeys(path string) ssh.Option {
 				return false
 			}
 
-			checker := &gossh.CertChecker{
-				IsUserAuthority: func(auth gossh.PublicKey) bool {
-					for _, ca := range cas {
-						if bytes.Equal(auth.Marshal(), ca.Marshal()) {
-							// its a cert signed by one of the CAs
-							return true
-						}
-					}
-					// it is a cert, but signed by another CA
-					return false
-				},
-			}
+			return isAuthorized(path, func(k ssh.PublicKey) bool {
+				checker := &gossh.CertChecker{
+					IsUserAuthority: func(auth gossh.PublicKey) bool {
+						// its a cert signed by one of the CAs
+						return bytes.Equal(auth.Marshal(), k.Marshal())
+					},
+				}
 
-			if !checker.IsUserAuthority(cert.SignatureKey) {
-				return false
-			}
+				if !checker.IsUserAuthority(cert.SignatureKey) {
+					return false
+				}
 
-			if err := checker.CheckCert(ctx.User(), cert); err != nil {
-				return false
-			}
+				if err := checker.CheckCert(ctx.User(), cert); err != nil {
+					return false
+				}
 
-			return true
+				return true
+			})
 		})(s)
 	}
 }
 
-func parseAuthorizedKeys(path string) ([]ssh.PublicKey, error) {
-	var keys []ssh.PublicKey
-
+func isAuthorized(path string, checker func(k ssh.PublicKey) bool) bool {
 	f, err := os.Open(path)
 	if err != nil {
-		return keys, fmt.Errorf("failed to parse %q: %w", path, err)
+		log.Warn("failed to parse", "path", path, "error", err)
+		return false
 	}
 	defer f.Close() // nolint: errcheck
 
@@ -146,15 +131,25 @@ func parseAuthorizedKeys(path string) ([]ssh.PublicKey, error) {
 			if errors.Is(err, io.EOF) {
 				break
 			}
-			return keys, fmt.Errorf("failed to parse %q: %w", path, err)
+			log.Warn("failed to parse", "path", path, "error", err)
+			return false
+		}
+		if strings.TrimSpace(string(line)) == "" {
+			continue
+		}
+		if bytes.HasPrefix(line, []byte{'#'}) {
+			continue
 		}
 		upk, _, _, _, err := ssh.ParseAuthorizedKey(line)
 		if err != nil {
-			return keys, fmt.Errorf("failed to parse %q: %w", path, err)
+			log.Warn("failed to parse", "path", path, "error", err)
+			return false
+		}
+		if checker(upk) {
+			return true
 		}
-		keys = append(keys, upk)
 	}
-	return keys, nil
+	return false
 }
 
 // WithPublicKeyAuth returns an ssh.Option that sets the public key auth handler.
diff --git a/options_test.go b/options_test.go
index 5108944..99e18ae 100755
--- a/options_test.go
+++ b/options_test.go
@@ -8,8 +8,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
@@ -25,23 +25,17 @@ func TestWithMaxTimeout(t *testing.T) {
 	requireEqual(t, time.Second, s.MaxTimeout)
 }
 
-func TestParseAuthorizedKeys(t *testing.T) {
+func TestIsAuthorized(t *testing.T) {
 	t.Run("valid", func(t *testing.T) {
-		keys, err := parseAuthorizedKeys("testdata/authorized_keys")
-		requireNoError(t, err)
-		requireEqual(t, 6, len(keys))
+		requireEqual(t, true, isAuthorized("testdata/authorized_keys", func(k ssh.PublicKey) bool { return true }))
 	})
 
 	t.Run("invalid", func(t *testing.T) {
-		keys, err := parseAuthorizedKeys("testdata/invalid_authorized_keys")
-		requireEqual(t, `failed to parse "testdata/invalid_authorized_keys": ssh: no key found`, err.Error())
-		requireEqual(t, 0, len(keys))
+		requireEqual(t, false, isAuthorized("testdata/invalid_authorized_keys", func(k ssh.PublicKey) bool { return true }))
 	})
 
 	t.Run("file not found", func(t *testing.T) {
-		keys, err := parseAuthorizedKeys("testdata/nope_authorized_keys")
-		requireEqual(t, `failed to parse "testdata/nope_authorized_keys": open testdata/nope_authorized_keys: no such file or directory`, err.Error())
-		requireEqual(t, 0, len(keys))
+		requireEqual(t, false, isAuthorized("testdata/nope_authorized_keys", func(k ssh.PublicKey) bool { return true }))
 	})
 }
 
@@ -65,12 +59,18 @@ func TestWithAuthorizedKeys(t *testing.T) {
 
 	t.Run("invalid", func(t *testing.T) {
 		s := ssh.Server{}
-		requireEqual(
+		requireNoError(
 			t,
-			`failed to parse "testdata/invalid_authorized_keys": ssh: no key found`,
-			WithAuthorizedKeys("testdata/invalid_authorized_keys")(&s).Error(),
+			WithAuthorizedKeys("testdata/invalid_authorized_keys")(&s),
 		)
 	})
+
+	t.Run("file not found", func(t *testing.T) {
+		s := ssh.Server{}
+		if err := WithAuthorizedKeys("testdata/nope_authorized_keys")(&s); err == nil {
+			t.Fatal("expected an error, got nil")
+		}
+	})
 }
 
 func TestWithTrustedUserCAKeys(t *testing.T) {
@@ -102,7 +102,7 @@ func TestWithTrustedUserCAKeys(t *testing.T) {
 	t.Run("invalid ca key", func(t *testing.T) {
 		s := &ssh.Server{}
 		if err := WithTrustedUserCAKeys("testdata/invalid-path")(s); err == nil {
-			t.Fatal("expedted an error, got nil")
+			t.Fatal("expected an error, got nil")
 		}
 	})
 
diff --git a/ratelimiter/ratelimiter.go b/ratelimiter/ratelimiter.go
index 64b966e..7b9d2b7 100644
--- a/ratelimiter/ratelimiter.go
+++ b/ratelimiter/ratelimiter.go
@@ -3,12 +3,12 @@ package ratelimiter
 
 import (
 	"errors"
-	"log"
 	"net"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
-	lru "github.com/hashicorp/golang-lru"
+	lru "github.com/hashicorp/golang-lru/v2"
 	"golang.org/x/time/rate"
 )
 
@@ -48,7 +48,7 @@ func NewRateLimiter(r rate.Limit, burst int, maxEntries int) RateLimiter {
 		maxEntries = 1
 	}
 	// only possible error is if maxEntries is <= 0, which is prevented above.
-	cache, _ := lru.New(maxEntries)
+	cache, _ := lru.New[string, *rate.Limiter](maxEntries)
 	return &limiters{
 		rate:  r,
 		burst: burst,
@@ -57,7 +57,7 @@ func NewRateLimiter(r rate.Limit, burst int, maxEntries int) RateLimiter {
 }
 
 type limiters struct {
-	cache *lru.Cache
+	cache *lru.Cache[string, *rate.Limiter]
 	rate  rate.Limit
 	burst int
 }
@@ -74,14 +74,14 @@ func (r *limiters) Allow(s ssh.Session) error {
 	var allowed bool
 	limiter, ok := r.cache.Get(key)
 	if ok {
-		allowed = limiter.(*rate.Limiter).Allow()
+		allowed = limiter.Allow()
 	} else {
 		limiter := rate.NewLimiter(r.rate, r.burst)
 		allowed = limiter.Allow()
 		r.cache.Add(key, limiter)
 	}
 
-	log.Printf("rate limiter key: %q, allowed? %v", key, allowed)
+	log.Debug("rate limiter key", "key", key, "allowed", allowed)
 	if allowed {
 		return nil
 	}
diff --git a/ratelimiter/ratelimiter_test.go b/ratelimiter/ratelimiter_test.go
index 5c426a1..018c38b 100644
--- a/ratelimiter/ratelimiter_test.go
+++ b/ratelimiter/ratelimiter_test.go
@@ -4,8 +4,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/time/rate"
 )
diff --git a/recover/recover.go b/recover/recover.go
index 6dff73d..769309c 100644
--- a/recover/recover.go
+++ b/recover/recover.go
@@ -1,11 +1,11 @@
 package recover
 
 import (
-	"log"
 	"runtime/debug"
 
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 // Middleware is a wish middleware that recovers from panics and log to stderr.
@@ -13,11 +13,16 @@ func Middleware(mw ...wish.Middleware) wish.Middleware {
 	return MiddlewareWithLogger(nil, mw...)
 }
 
+// Logger is the interface that wraps the basic Log method.
+type Logger interface {
+	Printf(format string, v ...interface{})
+}
+
 // MiddlewareWithLogger is a wish middleware that recovers from panics and log to
 // the provided logger.
-func MiddlewareWithLogger(logger *log.Logger, mw ...wish.Middleware) wish.Middleware {
+func MiddlewareWithLogger(logger Logger, mw ...wish.Middleware) wish.Middleware {
 	if logger == nil {
-		logger = log.Default()
+		logger = log.StandardLog()
 	}
 	h := func(ssh.Session) {}
 	for _, m := range mw {
@@ -28,7 +33,11 @@ func MiddlewareWithLogger(logger *log.Logger, mw ...wish.Middleware) wish.Middle
 			func() {
 				defer func() {
 					if r := recover(); r != nil {
-						logger.Printf("panic: %v\n%s", r, string(debug.Stack()))
+						logger.Printf(
+							"panic: %v\n%s",
+							r,
+							string(debug.Stack()),
+						)
 					}
 				}()
 				h(s)
diff --git a/recover/recover_test.go b/recover/recover_test.go
index bfd941c..2eac95c 100644
--- a/recover/recover_test.go
+++ b/recover/recover_test.go
@@ -3,8 +3,8 @@ package recover
 import (
 	"testing"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
diff --git a/scp/copy_from_client.go b/scp/copy_from_client.go
index ebc8301..d2d01bb 100644
--- a/scp/copy_from_client.go
+++ b/scp/copy_from_client.go
@@ -9,8 +9,9 @@ import (
 	"path/filepath"
 	"regexp"
 	"strconv"
+	"strings"
 
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/ssh"
 )
 
 var (
@@ -39,22 +40,23 @@ func copyFromClient(s ssh.Session, info Info, handler CopyFromClientHandler) err
 	)
 
 	for {
-		line, _, err := r.ReadLine()
+		line, err := r.ReadString('\n')
 		if err != nil {
 			if errors.Is(err, io.EOF) {
 				break
 			}
 			return fmt.Errorf("failed to read line: %w", err)
 		}
+		line = strings.TrimSuffix(line, "\n")
 
-		if matches := reTimestamp.FindAllStringSubmatch(string(line), 2); matches != nil {
+		if matches := reTimestamp.FindAllStringSubmatch(line, 2); matches != nil {
 			mtime, err = strconv.ParseInt(matches[0][1], 10, 64)
 			if err != nil {
-				return parseError{string(line)}
+				return parseError{line}
 			}
 			atime, err = strconv.ParseInt(matches[0][2], 10, 64)
 			if err != nil {
-				return parseError{string(line)}
+				return parseError{line}
 			}
 
 			// accepts the header
@@ -62,19 +64,19 @@ func copyFromClient(s ssh.Session, info Info, handler CopyFromClientHandler) err
 			continue
 		}
 
-		if matches := reNewFile.FindAllStringSubmatch(string(line), 3); matches != nil {
+		if matches := reNewFile.FindAllStringSubmatch(line, 3); matches != nil {
 			if len(matches) != 1 || len(matches[0]) != 4 {
-				return parseError{string(line)}
+				return parseError{line}
 			}
 
 			mode, err := strconv.ParseUint(matches[0][1], 8, 32)
 			if err != nil {
-				return parseError{string(line)}
+				return parseError{line}
 			}
 
 			size, err := strconv.ParseInt(matches[0][2], 10, 64)
 			if err != nil {
-				return parseError{string(line)}
+				return parseError{line}
 			}
 			name := matches[0][3]
 
@@ -107,14 +109,14 @@ func copyFromClient(s ssh.Session, info Info, handler CopyFromClientHandler) err
 			continue
 		}
 
-		if matches := reNewFolder.FindAllStringSubmatch(string(line), 2); matches != nil {
+		if matches := reNewFolder.FindAllStringSubmatch(line, 2); matches != nil {
 			if len(matches) != 1 || len(matches[0]) != 3 {
-				return parseError{string(line)}
+				return parseError{line}
 			}
 
 			mode, err := strconv.ParseUint(matches[0][1], 8, 32)
 			if err != nil {
-				return parseError{string(line)}
+				return parseError{line}
 			}
 			name := matches[0][2]
 
@@ -136,7 +138,7 @@ func copyFromClient(s ssh.Session, info Info, handler CopyFromClientHandler) err
 			continue
 		}
 
-		if string(line) == "E" {
+		if line == "E" {
 			path = filepath.Dir(path)
 
 			// says 'hey im done'
@@ -144,7 +146,7 @@ func copyFromClient(s ssh.Session, info Info, handler CopyFromClientHandler) err
 			continue
 		}
 
-		return fmt.Errorf("unhandled input: %q", string(line))
+		return fmt.Errorf("unhandled input: %q", line)
 	}
 
 	_, _ = s.Write(NULL)
diff --git a/scp/copy_to_client.go b/scp/copy_to_client.go
index d6aeefb..70ef867 100644
--- a/scp/copy_to_client.go
+++ b/scp/copy_to_client.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"io/fs"
 
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/ssh"
 )
 
 func copyToClient(s ssh.Session, info Info, handler CopyToClientHandler) error {
@@ -18,7 +18,9 @@ func copyToClient(s ssh.Session, info Info, handler CopyToClientHandler) error {
 
 	rootEntry := &RootEntry{}
 	var closers []func() error
-	defer closeAll(closers)
+	defer func() {
+		closeAll(closers)
+	}()
 
 	for _, match := range matches {
 		if !info.Recursive {
diff --git a/scp/filesystem.go b/scp/filesystem.go
index d4b1569..72f4903 100644
--- a/scp/filesystem.go
+++ b/scp/filesystem.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/ssh"
 )
 
 type fileSystemHandler struct{ root string }
@@ -61,7 +61,14 @@ func (h *fileSystemHandler) Glob(_ ssh.Session, s string) ([]string, error) {
 }
 
 func (h *fileSystemHandler) WalkDir(_ ssh.Session, path string, fn fs.WalkDirFunc) error {
-	return filepath.WalkDir(h.prefixed(path), fn)
+	return filepath.WalkDir(h.prefixed(path), func(path string, d fs.DirEntry, err error) error {
+		// if h.root is ./foo/bar, we don't want to server `bar` as the root,
+		// but instead its contents.
+		if path == h.root {
+			return err
+		}
+		return fn(path, d, err)
+	})
 }
 
 func (h *fileSystemHandler) NewDirEntry(_ ssh.Session, name string) (*DirEntry, error) {
@@ -113,9 +120,13 @@ func (h *fileSystemHandler) Write(_ ssh.Session, entry *FileEntry) (int64, error
 	if err != nil {
 		return 0, fmt.Errorf("failed to open file: %q: %w", entry.Filepath, err)
 	}
+	defer f.Close() //nolint:errcheck
 	written, err := io.Copy(f, entry.Reader)
 	if err != nil {
 		return 0, fmt.Errorf("failed to write file: %q: %w", entry.Filepath, err)
 	}
+	if err := f.Close(); err != nil {
+		return 0, fmt.Errorf("failed to close file: %q: %w", entry.Filepath, err)
+	}
 	return written, h.chtimes(entry.Filepath, entry.Mtime, entry.Atime)
 }
diff --git a/scp/filesystem_test.go b/scp/filesystem_test.go
index 1bf3f31..a0ced6a 100644
--- a/scp/filesystem_test.go
+++ b/scp/filesystem_test.go
@@ -71,8 +71,8 @@ func TestFilesystem(t *testing.T) {
 
 			session := setup(t, h, nil)
 			bts, err := session.CombinedOutput("scp -r -f a")
-			requireEqualGolden(t, bts)
 			is.NoErr(err)
+			requireEqualGolden(t, bts)
 		})
 
 		t.Run("recursive glob", func(t *testing.T) {
@@ -88,8 +88,8 @@ func TestFilesystem(t *testing.T) {
 
 			session := setup(t, h, nil)
 			bts, err := session.CombinedOutput("scp -r -f a/*")
-			requireEqualGolden(t, bts)
 			is.NoErr(err)
+			requireEqualGolden(t, bts)
 		})
 
 		t.Run("recursive invalid file", func(t *testing.T) {
@@ -102,6 +102,23 @@ func TestFilesystem(t *testing.T) {
 			_, err := session.CombinedOutput("scp -r -f a")
 			is.True(err != nil)
 		})
+
+		t.Run("recursive folder", func(t *testing.T) {
+			is := is.New(t)
+
+			dir := t.TempDir()
+			h := NewFileSystemHandler(dir)
+
+			is.NoErr(os.MkdirAll(filepath.Join(dir, "a/b/c/d/e"), 0o755))
+			is.NoErr(os.WriteFile(filepath.Join(dir, "a/b/c.txt"), []byte("c text file"), 0o644))
+			is.NoErr(os.WriteFile(filepath.Join(dir, "a/b/c/d/e/e.txt"), []byte("e text file"), 0o644))
+			chtimesTree(t, dir, atime, mtime)
+
+			session := setup(t, h, nil)
+			bts, err := session.CombinedOutput("scp -r -f /")
+			is.NoErr(err)
+			requireEqualGolden(t, bts)
+		})
 	})
 
 	t.Run("scp -t", func(t *testing.T) {
@@ -209,7 +226,7 @@ func TestFilesystem(t *testing.T) {
 				err := h.Mkdir(nil, &DirEntry{
 					Name:     "foo",
 					Filepath: "foo/bar/baz",
-					Mode:     0755,
+					Mode:     0o755,
 				})
 				is.True(err != nil) // should err
 			})
@@ -222,7 +239,7 @@ func TestFilesystem(t *testing.T) {
 				_, err := h.Write(nil, &FileEntry{
 					Name:     "foo.txt",
 					Filepath: "baz/foo.txt",
-					Mode:     0644,
+					Mode:     0o644,
 					Size:     10,
 				})
 				is.True(err != nil) // should err
@@ -234,23 +251,21 @@ func TestFilesystem(t *testing.T) {
 				_, err := h.Write(nil, &FileEntry{
 					Name:     "foo.txt",
 					Filepath: "foo.txt",
-					Mode:     0644,
+					Mode:     0o644,
 					Size:     10,
 					Reader:   iotest.ErrReader(fmt.Errorf("fake err")),
 				})
 				is.True(err != nil) // should err
 			})
 		})
-
 	})
-
 }
 
 func chtimesTree(tb testing.TB, dir string, atime, mtime time.Time) {
-	is := is.New(tb)
-
-	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
-		is.NoErr(os.Chtimes(path, atime, mtime))
-		return nil
-	})
+	is.New(tb).NoErr(filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		return os.Chtimes(path, atime, mtime)
+	}))
 }
diff --git a/scp/fs.go b/scp/fs.go
index fe3aa0a..1c9fede 100644
--- a/scp/fs.go
+++ b/scp/fs.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"io/fs"
 
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/ssh"
 )
 
 type fsHandler struct{ fsys fs.FS }
@@ -26,6 +26,7 @@ func (h *fsHandler) WalkDir(_ ssh.Session, path string, fn fs.WalkDirFunc) error
 }
 
 func (h *fsHandler) NewDirEntry(_ ssh.Session, path string) (*DirEntry, error) {
+	path = normalizePath(path)
 	info, err := fs.Stat(h.fsys, path)
 	if err != nil {
 		return nil, fmt.Errorf("failed to open dir: %q: %w", path, err)
diff --git a/scp/fs_test.go b/scp/fs_test.go
index 8471b81..2c03982 100644
--- a/scp/fs_test.go
+++ b/scp/fs_test.go
@@ -67,8 +67,8 @@ func TestFS(t *testing.T) {
 
 		session := setup(t, h, nil)
 		bts, err := session.CombinedOutput("scp -r -f a")
-		requireEqualGolden(t, bts)
 		is.NoErr(err)
+		requireEqualGolden(t, bts)
 	})
 
 	t.Run("recursive glob", func(t *testing.T) {
@@ -84,8 +84,25 @@ func TestFS(t *testing.T) {
 
 		session := setup(t, h, nil)
 		bts, err := session.CombinedOutput("scp -r -f a/*")
+		is.NoErr(err)
 		requireEqualGolden(t, bts)
+	})
+
+	t.Run("recursive folder", func(t *testing.T) {
+		is := is.New(t)
+
+		dir := t.TempDir()
+		h := NewFileSystemHandler(dir)
+
+		is.NoErr(os.MkdirAll(filepath.Join(dir, "a/b/c/d/e"), 0o755))
+		is.NoErr(os.WriteFile(filepath.Join(dir, "a/b/c.txt"), []byte("c text file"), 0o644))
+		is.NoErr(os.WriteFile(filepath.Join(dir, "a/b/c/d/e/e.txt"), []byte("e text file"), 0o644))
+		chtimesTree(t, dir, atime, mtime)
+
+		session := setup(t, h, nil)
+		bts, err := session.CombinedOutput("scp -r -f /")
 		is.NoErr(err)
+		requireEqualGolden(t, bts)
 	})
 
 	t.Run("recursive invalid file", func(t *testing.T) {
diff --git a/scp/scp.go b/scp/scp.go
index 03d3f71..21e6370 100644
--- a/scp/scp.go
+++ b/scp/scp.go
@@ -6,11 +6,12 @@ import (
 	"io"
 	"io/fs"
 	"path/filepath"
+	"runtime"
 	"strconv"
 	"strings"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
-	"github.com/gliderlabs/ssh"
 )
 
 // CopyToClientHandler is a handler that can be implemented to handle files
@@ -19,7 +20,7 @@ type CopyToClientHandler interface {
 	// Glob should be implemented if you want to provide server-side globbing
 	// support.
 	//
-	// A minimal implementation to disable it ist to return `[]string{s}, nil`.
+	// A minimal implementation to disable it is to return `[]string{s}, nil`.
 	//
 	// Note: if your other functions expect a relative path, make sure that
 	// your Glob implementation returns relative paths as well.
@@ -150,7 +151,7 @@ type RootEntry []Entry
 // Appennd the given entry to a child directory, or the the itself if
 // none matches.
 func (e *RootEntry) Append(entry Entry) {
-	parent := filepath.Dir(entry.path())
+	parent := normalizePath(filepath.Dir(entry.path()))
 
 	for _, child := range *e {
 		switch dir := child.(type) {
@@ -159,7 +160,7 @@ func (e *RootEntry) Append(entry Entry) {
 				dir.Children = append(dir.Children, entry)
 				return
 			}
-			if strings.HasPrefix(parent, dir.Filepath) {
+			if strings.HasPrefix(parent, normalizePath(dir.Filepath)) {
 				dir.Append(entry)
 				return
 			}
@@ -220,7 +221,7 @@ func (e *DirEntry) Write(w io.Writer) error {
 
 // Appends an entry to the folder or their children.
 func (e *DirEntry) Append(entry Entry) {
-	parent := filepath.Dir(entry.path())
+	parent := normalizePath(filepath.Dir(entry.path()))
 
 	for _, child := range e.Children {
 		switch dir := child.(type) {
@@ -229,7 +230,7 @@ func (e *DirEntry) Append(entry Entry) {
 				dir.Children = append(dir.Children, entry)
 				return
 			}
-			if strings.HasPrefix(parent, dir.path()) {
+			if strings.HasPrefix(parent, normalizePath(dir.path())) {
 				dir.Append(entry)
 				return
 			}
@@ -257,7 +258,7 @@ type Info struct {
 	// Ok is true if the current session is a SCP.
 	Ok bool
 
-	// Recursice is true if its a recursive SCP.
+	// Recursive is true if its a recursive SCP.
 	Recursive bool
 
 	// Path is the server path of the scp operation.
@@ -294,3 +295,11 @@ func GetInfo(cmd []string) Info {
 func octalPerms(info fs.FileMode) string {
 	return "0" + strconv.FormatUint(uint64(info.Perm()), 8)
 }
+
+func normalizePath(p string) string {
+	p = filepath.Clean(p)
+	if runtime.GOOS == "windows" {
+		return strings.ReplaceAll(p, "\\", "/")
+	}
+	return p
+}
diff --git a/scp/scp_test.go b/scp/scp_test.go
index a24c1a2..6fe34d4 100644
--- a/scp/scp_test.go
+++ b/scp/scp_test.go
@@ -4,10 +4,12 @@ import (
 	"bytes"
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
+	"github.com/google/go-cmp/cmp"
 	"github.com/matryer/is"
 	gossh "golang.org/x/crypto/ssh"
 )
@@ -115,7 +117,6 @@ func setup(tb testing.TB, rh CopyToClientHandler, wh CopyFromClientHandler) *gos
 	return testsession.New(tb, &ssh.Server{
 		Handler: Middleware(rh, wh)(func(s ssh.Session) {
 			s.Exit(0)
-			s.Close()
 		}),
 	}, nil)
 }
@@ -124,7 +125,17 @@ func requireEqualGolden(tb testing.TB, out []byte) {
 	tb.Helper()
 	is := is.New(tb)
 
-	out = bytes.ReplaceAll(out, NULL, []byte("<NULL>"))
+	fixOutput := func(bts []byte) []byte {
+		bts = bytes.ReplaceAll(bts, []byte("\r"), []byte(""))
+		if runtime.GOOS == "windows" {
+			// perms always come different on Windows because, well, its Windows.
+			bts = bytes.ReplaceAll(bts, []byte("0666"), []byte("0644"))
+			bts = bytes.ReplaceAll(bts, []byte("0777"), []byte("0755"))
+		}
+		return bytes.ReplaceAll(bts, NULL, []byte("<NULL>"))
+	}
+
+	out = fixOutput(out)
 	golden := "testdata/" + tb.Name() + ".test"
 	if os.Getenv("UPDATE") != "" {
 		is.NoErr(os.MkdirAll(filepath.Dir(golden), 0o755))
@@ -133,7 +144,9 @@ func requireEqualGolden(tb testing.TB, out []byte) {
 
 	gbts, err := os.ReadFile(golden)
 	is.NoErr(err)
+	gbts = fixOutput(gbts)
 
-	gbts = bytes.ReplaceAll(gbts, NULL, []byte("<NULL>"))
-	is.Equal(string(gbts), string(out))
+	if diff := cmp.Diff(string(gbts), string(out)); diff != "" {
+		tb.Fatal("files do not match:", diff)
+	}
 }
diff --git a/scp/testdata/TestFS/recursive_folder.test b/scp/testdata/TestFS/recursive_folder.test
new file mode 100644
index 0000000..5621c00
--- /dev/null
+++ b/scp/testdata/TestFS/recursive_folder.test
@@ -0,0 +1,19 @@
+T1323853868 0 1323853868 0
+D0755 0 a
+T1323853868 0 1323853868 0
+D0755 0 b
+T1323853868 0 1323853868 0
+D0755 0 c
+T1323853868 0 1323853868 0
+D0755 0 d
+T1323853868 0 1323853868 0
+D0755 0 e
+T1323853868 0 1323853868 0
+C0644 11 e.txt
+e text file<NULL>E
+E
+E
+T1323853868 0 1323853868 0
+C0644 11 c.txt
+c text file<NULL>E
+E
diff --git a/scp/testdata/TestFilesystem/scp_-f/recursive_folder.test b/scp/testdata/TestFilesystem/scp_-f/recursive_folder.test
new file mode 100644
index 0000000..5621c00
--- /dev/null
+++ b/scp/testdata/TestFilesystem/scp_-f/recursive_folder.test
@@ -0,0 +1,19 @@
+T1323853868 0 1323853868 0
+D0755 0 a
+T1323853868 0 1323853868 0
+D0755 0 b
+T1323853868 0 1323853868 0
+D0755 0 c
+T1323853868 0 1323853868 0
+D0755 0 d
+T1323853868 0 1323853868 0
+D0755 0 e
+T1323853868 0 1323853868 0
+C0644 11 e.txt
+e text file<NULL>E
+E
+E
+T1323853868 0 1323853868 0
+C0644 11 c.txt
+c text file<NULL>E
+E
diff --git a/testdata/authorized_keys b/testdata/authorized_keys
index 2f08859..225dc32 100644
--- a/testdata/authorized_keys
+++ b/testdata/authorized_keys
@@ -3,4 +3,6 @@ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQChxV3pJRnXP7crH+4xxH8skCF/Bs8JX8VTjlS4dpLY
 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCdJkpQAr3zhC+grKMexj8zgJIuAQ/2LR59RvXemEAovd671Et356cmHnCDmUvUlH/70xQdyL3n68tzu2ZEzKheQP5vz05CAFXTi7rlMvhtz632mLMPlU3lGuP+A6rzqNSnTtrIa2Q3Fe2ir6N+ad782J8g6frGJaVfA/G7j/M1JwyDJWzUS3HvDHDO+qFze71h0/o9W1+VoRaSfD67BzPQumkEkt/CilSPU8VKRP3q/FIeIrgTBhNh17SX/qlnyrJipDTF1QtXUOK4H5TsEE0S13z8a4Wo37kRWQPxdjWyfX9tBjsN86n+R7OGSXXdi10n9THrisdgx2GKsk1HjY+u5YlDpDysFLBs6j4nWeTxnrjgx6HUqvMk3mdqrAKHTglt34OUQtB463GMgCW85w+ni8ebPKlt5YQsXalilcoI4K7fakyXe+o9Y0sCwE3SLXEJhtd/Esz1pVzvMBCshpRknBPFh/gs/i1YuL0SJqI2BGBFs0d/ARwqUQSoXXBTJPc= k5@test
 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ7InQIj/ROngoWWb6kXTcTJd8+u5skDfGm8JJxRugMB k2@test
 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDgteu96TZLd3iG11D5NqBsQRvhW2I6iD/ycwOiWFjFyv4MAHaDFiIazbeQSbi++1+5vspeNuv9AKJFgG0SpnjMLQM0rJb5DIsuRxGOAS/oh82yNCxYcW2+eXcqUDL4V+fZ6eIqtSIBrPQY89/CbZ4nFtw7+941gmFa2+7Wj9vLk4GTiyu/jQsbGnAZUCMvce1jFZ9XDMYSYzXEtkqhBT6eYDd7xMQejovszJfPqlKDxpMZxpaDsQGf+00IJPZUUxkX62eAmrlX1q4XO+m2zIjGpf/gdNKHEMXQrvBWdvwg0rat2i+PCW4Rbwx7wHBBWPRqEPjcVTfwvOWoZGGU3TSX8M7Gcj+ZvAD/uV7DWcNi61Obtw/6PXYvKFZWcZ1sHxUTI84CUcVLSL55hOtJqCuJXmUdKdBcJLyo3NValIjIZn+ljn6biVAr00nGo06nO4j2eTE2ZLOZFEB6rHuf1iaT18EiJgnJGrB7HY4+KUoUIzmvzQrKxxLbIe957hnx+TE= k6@test
+
+# a commented line, and the previous line was empty
 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMJlb/qf2B2kMNdBxfpCQqI2ctPcsOkdZGVh5zTRhKtH k3@test
diff --git a/testsession/testsession.go b/testsession/testsession.go
index 40c7798..5e10a28 100644
--- a/testsession/testsession.go
+++ b/testsession/testsession.go
@@ -1,13 +1,13 @@
 // Package testsession provides utilities to test SSH sessions.
 //
-// more or less copied from gliderlabs/ssh tests
+// more or less copied from charmbracelet/ssh tests
 package testsession
 
 import (
 	"net"
 	"testing"
 
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/ssh"
 	gossh "golang.org/x/crypto/ssh"
 )
 
@@ -45,6 +45,8 @@ func newLocalListener(tb testing.TB) net.Listener {
 			tb.Fatalf("failed to listen on a port: %v", err)
 		}
 	}
+
+	tb.Cleanup(func() { _ = l.Close() })
 	return l
 }
 
diff --git a/testsession/testsession_test.go b/testsession/testsession_test.go
index ab11191..d302909 100644
--- a/testsession/testsession_test.go
+++ b/testsession/testsession_test.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/ssh"
 )
 
 func TestSession(t *testing.T) {
diff --git a/wish.go b/wish.go
index 51ac209..dbfc8f2 100644
--- a/wish.go
+++ b/wish.go
@@ -5,7 +5,7 @@ import (
 	"io"
 
 	"github.com/charmbracelet/keygen"
-	"github.com/gliderlabs/ssh"
+	"github.com/charmbracelet/ssh"
 )
 
 // Middleware is a function that takes an ssh.Handler and returns an
@@ -26,11 +26,11 @@ func NewServer(ops ...ssh.Option) (*ssh.Server, error) {
 		}
 	}
 	if len(s.HostSigners) == 0 {
-		k, err := keygen.New("", nil, keygen.Ed25519)
+		k, err := keygen.New("id_ed25519", keygen.WithKeyType(keygen.Ed25519), keygen.WithWrite())
 		if err != nil {
 			return nil, err
 		}
-		err = s.SetOption(WithHostKeyPEM(k.PrivateKeyPEM()))
+		err = s.SetOption(WithHostKeyPEM(k.RawPrivateKey()))
 		if err != nil {
 			return nil, err
 		}
diff --git a/wish_test.go b/wish_test.go
index 32cccc9..f9f77e2 100755
--- a/wish_test.go
+++ b/wish_test.go
@@ -1,30 +1,33 @@
-// go:genarate mockgen -package mocks -destination mocks/session.go github.com/gliderlabs/ssh Session
+// go:generate mockgen -package mocks -destination mocks/session.go github.com/charmbracelet/ssh Session
 package wish
 
 import (
 	"bytes"
 	"errors"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish/testsession"
-	"github.com/gliderlabs/ssh"
 )
 
 func TestNewServer(t *testing.T) {
-	_, err := NewServer()
+	fp := filepath.Join(t.TempDir(), "id_ed25519")
+	_, err := NewServer(WithHostKeyPath(fp))
 	if err != nil {
 		t.Fatal(err)
 	}
 }
 
 func TestNewServerWithOptions(t *testing.T) {
-	_, err := NewServer(
+	fp := filepath.Join(t.TempDir(), "id_ed25519")
+	if _, err := NewServer(
+		WithHostKeyPath(fp),
 		WithMaxTimeout(time.Second),
 		WithAddress(":2222"),
-	)
-	if err != nil {
+	); err != nil {
 		t.Fatal(err)
 	}
 }

More details

Full run details

Historical runs