Repository: charmbracelet/soft-serve

Issue:
Error: User not found
Hi, on a fresh install of soft server on Fedora Server, after setting SOFT_SERVE_INITIAL_ADMIN_KEYS='<contents of my pubkey' when I try to run ```ssh -i <path to my key> -p 23231 localhost user``` I get ```Error: user not found``` 

**To Reproduce**
Steps to reproduce the behavior:
1. Follow the steps in the readme for installing on fedora server
2. run ```SOFT_SERVE_INITIAL_ADMIN_KEYS='<contents of my pubkey' ```
3. run ```soft serve``` (I have tried using the default location and a different location on different installs)
4. open a new shell on the same machine (when I ran this as a systemd service I ignored this step) 
5. run ```ssh -i <path to my key> -p 23231 localhost user``` 
6. ```Error: user not found```  returned


**Environment (please complete the following information):**
 - OS:  Fedora Server
 - Shell: oh-my-zsh

**Additional context**
I have tried installing this a few times, and have also tried hosting it on a domain instead of locally, which returns the same error, although if I run ```ssh -i <path to my key> -p 23231 localhost``` I do get into the tui interface of soft serve

I have also tried running this as a systemd service and putting the enviroment admin keys command in there, as well as adding ssh keys to the additional ssh keys in the config.yaml with the same issue 

Sorry if the formatting/anything else is wrong, this is the first time I've rasied a github issue

Thanks!



Discussion:
@Piker98988: Hey! There is another issue open like yours. Let me link it: 
https://github.com/charmbracelet/soft-serve/issues/497
I'm having the same issue, I'm trying to look into it but i don't know much of what's going on with it...

Solution patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0fe1440..271179a 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,29 +1,53 @@
 version: 2
+
 updates:
   - package-ecosystem: "gomod"
     directory: "/"
     schedule:
-      interval: "daily"
+      interval: "weekly"
+      day: "monday"
+      time: "05:00"
+      timezone: "America/New_York"
     labels:
       - "dependencies"
     commit-message:
       prefix: "chore"
       include: "scope"
+    groups:
+      all:
+        patterns:
+          - "*"
+
   - package-ecosystem: "github-actions"
     directory: "/"
     schedule:
-      interval: "daily"
+      interval: "weekly"
+      day: "monday"
+      time: "05:00"
+      timezone: "America/New_York"
     labels:
       - "dependencies"
     commit-message:
       prefix: "chore"
       include: "scope"
+    groups:
+      all:
+        patterns:
+          - "*"
+
   - package-ecosystem: "docker"
     directory: "/"
     schedule:
-      interval: "daily"
+      interval: "weekly"
+      day: "monday"
+      time: "05:00"
+      timezone: "America/New_York"
     labels:
       - "dependencies"
     commit-message:
       prefix: "chore"
       include: "scope"
+    groups:
+      all:
+        patterns:
+          - "*"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8ada161..f74b6a7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -31,9 +31,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: Install Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version: ^1
           cache: true
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 46c625c..38575ed 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -25,10 +25,10 @@ jobs:
           --health-retries 5
     runs-on: ${{ matrix.os }}
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version: ^1
 
diff --git a/.github/workflows/dependabot-sync.yml b/.github/workflows/dependabot-sync.yml
new file mode 100644
index 0000000..9b08259
--- /dev/null
+++ b/.github/workflows/dependabot-sync.yml
@@ -0,0 +1,17 @@
+name: dependabot-sync
+on:
+  schedule:
+    - cron: "0 0 * * 0" # every Sunday at midnight
+  workflow_dispatch: # allows manual triggering
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  dependabot-sync:
+    uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
+    with:
+      repo_name: ${{ github.event.repository.name }}
+    secrets:
+      gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
diff --git a/.github/workflows/lint-soft.yml b/.github/workflows/lint-soft.yml
deleted file mode 100644
index 826981b..0000000
--- a/.github/workflows/lint-soft.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: lint-soft
-
-on:
-  push:
-    branches:
-      - "main"
-  pull_request:
-
-permissions:
-  contents: read
-  # Optional: allow read access to pull request. Use with `only-new-issues` option.
-  pull-requests: read
-
-jobs:
-  golangci:
-    name: lint-soft
-    runs-on: ubuntu-latest
-    steps:
-      - name: Install Go
-        uses: actions/setup-go@v5
-        with:
-          go-version: ^1
-
-      - uses: actions/checkout@v4
-      - name: golangci-lint
-        uses: golangci/golangci-lint-action@v6
-        with:
-          # Optional: golangci-lint command line arguments.
-          args: --config .golangci-soft.yml --issues-exit-code=0
-          # Optional: show only new issues if it's a pull request. The default value is `false`.
-          only-new-issues: true
diff --git a/.github/workflows/lint-sync.yml b/.github/workflows/lint-sync.yml
new file mode 100644
index 0000000..ecf8580
--- /dev/null
+++ b/.github/workflows/lint-sync.yml
@@ -0,0 +1,14 @@
+name: lint-sync
+on:
+  schedule:
+    # every Sunday at midnight
+    - cron: "0 0 * * 0"
+  workflow_dispatch: # allows manual triggering
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  lint:
+    uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 2d40010..a1d6d0e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,31 +1,8 @@
 name: lint
-
 on:
   push:
-    branches:
-      - "main"
   pull_request:
 
-permissions:
-  contents: read
-  # Optional: allow read access to pull request. Use with `only-new-issues` option.
-  pull-requests: read
-
 jobs:
-  golangci:
-    name: lint
-    runs-on: ubuntu-latest
-    steps:
-      - name: Install Go
-        uses: actions/setup-go@v5
-        with:
-          go-version: ^1
-
-      - uses: actions/checkout@v4
-      - name: golangci-lint
-        uses: golangci/golangci-lint-action@v6
-        with:
-          # Optional: golangci-lint command line arguments.
-          #args:
-          # Optional: show only new issues if it's a pull request. The default value is `false`.
-          only-new-issues: true
+  lint:
+    uses: charmbracelet/meta/.github/workflows/lint.yml@main
diff --git a/.golangci-soft.yml b/.golangci-soft.yml
deleted file mode 100644
index 5f3cebf..0000000
--- a/.golangci-soft.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-run:
-  tests: false
-  timeout: 5m
-
-issues:
-  include:
-    - EXC0001
-    - EXC0005
-    - EXC0011
-    - EXC0012
-    - EXC0013
-
-  max-issues-per-linter: 0
-  max-same-issues: 0
-
-linters:
-  enable:
-    - exhaustive
-    - goconst
-    - godot
-    - godox
-    - mnd
-    - gomoddirectives
-    - goprintffuncname
-    - misspell
-    - nakedret
-    - nestif
-    - noctx
-    - nolintlint
-    - prealloc
-    - wrapcheck
-
-  # disable default linters, they are already enabled in .golangci.yml
-  disable:
-    - errcheck
-    - gosimple
-    - govet
-    - ineffassign
-    - staticcheck
-    - typecheck
-    - unused
diff --git a/.golangci.yml b/.golangci.yml
index bf67859..929cb0a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,27 +1,22 @@
+version: "2"
 run:
   tests: false
-  timeout: 5m
-
-issues:
-  include:
-    - EXC0001
-    - EXC0005
-    - EXC0011
-    - EXC0012
-    - EXC0013
-
-  max-issues-per-linter: 0
-  max-same-issues: 0
-
 linters:
   enable:
     - bodyclose
-    - exportloopref
-    - gofumpt
-    - goimports
+    - exhaustive
+    - goconst
+    - godot
+    - gomoddirectives
+    - goprintffuncname
     - gosec
+    - misspell
+    - nakedret
+    - nestif
     - nilerr
-    - predeclared
+    - noctx
+    - nolintlint
+    - prealloc
     - revive
     - rowserrcheck
     - sqlclosecheck
@@ -29,10 +24,21 @@ linters:
     - unconvert
     - unparam
     - whitespace
-
-severity:
-  default-severity: error
-  rules:
-    - linters:
-        - revive
-      severity: info
+    - wrapcheck
+  exclusions:
+    rules:
+      - text: '(slog|log)\.\w+'
+        linters:
+          - noctx
+    generated: lax
+    presets:
+      - common-false-positives
+issues:
+  max-issues-per-linter: 0
+  max-same-issues: 0
+formatters:
+  enable:
+    - gofumpt
+    - goimports
+  exclusions:
+    generated: lax
diff --git a/.goreleaser.yml b/.goreleaser.yml
index e90386e..ac8b219 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,3 +1,7 @@
+# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
+
+version: 2
+
 includes:
   - from_url:
       url: charmbracelet/meta/main/goreleaser-soft-serve.yaml
@@ -10,6 +14,3 @@ variables:
   maintainer: "Ayman Bagabas <ayman@charm.sh>"
   brew_commit_author_name: "Ayman Bagabas"
   brew_commit_author_email: "ayman@charm.sh"
-
-# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
-
diff --git a/README.md b/README.md
index cbefcd7..0c5414b 100644
--- a/README.md
+++ b/README.md
@@ -110,7 +110,7 @@ and database.
 
 By default, program configuration is stored within the `data` directory. But,
 this can be overridden by setting a custom path to a config file with `SOFT_SERVE_CONFIG_LOCATION`
-that is pre-created. If a config file pointed to by `SOFT_SERVE_CONFIG_LOCATION`, 
+that is pre-created. If a config file pointed to by `SOFT_SERVE_CONFIG_LOCATION`,
 the default location within the `data` dir is used for generating a default config.
 
 To change the default data path use `SOFT_SERVE_DATA_PATH` environment variable.
@@ -204,6 +204,34 @@ http:
   # Make sure to use https:// if you are using TLS.
   public_url: "http://localhost:23232"
 
+  # The cross-origin request security options
+  cors:
+    # The allowed cross-origin headers
+    allowed_headers:
+      - "Accept"
+      - "Accept-Language"
+      - "Content-Language"
+      - "Content-Type"
+      - "Origin"
+      - "X-Requested-With"
+      - "User-Agent"
+      - "Authorization"
+      - "Access-Control-Request-Method"
+      - "Access-Control-Allow-Origin"
+
+    # The allowed cross-origin URLs
+    allowed_origins:
+      - "http://localhost:23232" # always allowed
+      # - "https://example.com"
+
+    # The allowed cross-origin methods
+    allowed_methods:
+      - "GET"
+      - "HEAD"
+      - "POST"
+      - "PUT"
+      - "OPTIONS"
+
 # The database configuration.
 db:
   # The database driver to use.
@@ -287,13 +315,17 @@ Soft Serve at its core manages your server authentication and authorization. Aut
 
 To manage the server users, access, and repos, you can use the SSH command line interface.
 
-Try `ssh localhost -i ~/.ssh/id_ed25519 -p 23231 help` for more info. Make sure
+Try `ssh localhost -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -p 23231 help` for more info. Make sure
 you use your key here.
 
+> **Note** The `IdentitiesOnly` option is used to prevent SSH from using any
+> other keys in your `~/.ssh` directory. This is useful when you have multiple
+> keys, and you want to use a specific key for Soft Serve.
+
 For ease of use, instead of specifying the key, port, and hostname every time
 you SSH into Soft Serve, add your own Soft Serve instance entry to your SSH
 config. For instance, to use `ssh soft` instead of typing `ssh localhost -i
-~/.ssh/id_ed25519 -p 23231`, we can define a `soft` entry in our SSH config
+~/.ssh/id_ed25519 -o IdentitiesOnly=yes -p 23231`, we can define a `soft` entry in our SSH config
 file `~/.ssh/config`.
 
 ```conf
@@ -301,6 +333,7 @@ Host soft
   HostName localhost
   Port 23231
   IdentityFile ~/.ssh/id_ed25519
+  IdentitiesOnly yes
 ```
 
 Now, we can do `ssh soft` to SSH into Soft Serve. Since `git` is also aware of
@@ -313,7 +346,7 @@ git clone ssh://soft/dotfiles
 git push origin main
 ```
 
-> **Note** The `-i` part will be omitted in the examples below for brevity. You
+> **Note** The `-i` and `-o` parts will be omitted in the examples below for brevity. You
 > can add your server settings to your sshconfig for quicker access.
 
 ### Authentication
@@ -771,6 +804,12 @@ If you’re curious about the inner workings of this problem have a look at:
 - https://go-review.googlesource.com/c/crypto/+/220037
 - https://github.com/golang/crypto/pull/197
 
+## Contributing
+
+See [contributing][contribute].
+
+[contribute]: https://github.com/charmbracelet/soft-serve/contribute
+
 ## Feedback
 
 We’d love to hear your thoughts on this project. Feel free to drop us a note!
diff --git a/cmd/soft/browse/browse.go b/cmd/soft/browse/browse.go
index eebf288..ecb3d53 100644
--- a/cmd/soft/browse/browse.go
+++ b/cmd/soft/browse/browse.go
@@ -5,9 +5,9 @@ import (
 	"path/filepath"
 	"time"
 
-	"github.com/charmbracelet/bubbles/key"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -39,9 +39,8 @@ var Command = &cobra.Command{
 
 		// Bubble Tea uses Termenv default output so we have to use the same
 		// thing here.
-		output := lipgloss.DefaultRenderer()
 		ctx := cmd.Context()
-		c := common.NewCommon(ctx, output, 0, 0)
+		c := common.NewCommon(ctx, 0, 0)
 		c.HideCloneCmd = true
 		comps := []common.TabComponent{
 			repo.NewReadme(c),
@@ -152,7 +151,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.WindowSizeMsg:
 		m.SetSize(msg.Width, msg.Height)
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, m.common.KeyMap.Back) && m.error != nil:
 			m.error = nil
@@ -166,12 +165,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m.common.Zone.Close()
 			return m, tea.Quit
 		}
-	case tea.MouseMsg:
-		if msg.Action != tea.MouseActionPress {
-			break
-		}
-		switch msg.Button {
-		case tea.MouseButtonLeft:
+	case tea.MouseClickMsg:
+		mouse := msg.Mouse()
+		switch mouse.Button {
+		case tea.MouseLeft:
 			switch {
 			case m.common.Zone.Get("footer").InBounds(msg):
 				cmds = append(cmds, footer.ToggleFooterCmd)
diff --git a/cmd/soft/hook/hook.go b/cmd/soft/hook/hook.go
index 43718ed..8a6fb87 100644
--- a/cmd/soft/hook/hook.go
+++ b/cmd/soft/hook/hook.go
@@ -12,7 +12,7 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/cmd"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
@@ -65,6 +65,8 @@ var (
 		// This is set in the server before invoking git-receive-pack/git-upload-pack
 		repoName := os.Getenv("SOFT_SERVE_REPO_NAME")
 
+		logger := log.FromContext(ctx).With("repo", repoName)
+
 		stdin := cmd.InOrStdin()
 		stdout := cmd.OutOrStdout()
 		stderr := cmd.ErrOrStderr()
@@ -80,9 +82,11 @@ var (
 			scanner := bufio.NewScanner(stdin)
 			for scanner.Scan() {
 				buf.Write(scanner.Bytes())
+				buf.WriteByte('\n')
 				fields := strings.Fields(scanner.Text())
 				if len(fields) != 3 {
-					return fmt.Errorf("invalid hook input: %s", scanner.Text())
+					logger.Error(fmt.Sprintf("invalid %s hook input", cmdName), "input", scanner.Text())
+					continue
 				}
 				opts = append(opts, hooks.HookArg{
 					OldSha:  fields[0],
@@ -99,7 +103,8 @@ var (
 			}
 		case hooks.UpdateHook:
 			if len(args) != 3 {
-				return fmt.Errorf("invalid update hook input: %s", args)
+				logger.Error("invalid update hook input", "input", args)
+				break
 			}
 
 			hks.Update(ctx, stdout, stderr, repoName, hooks.HookArg{
@@ -115,7 +120,7 @@ var (
 		if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
 			// If the custom hook is executable, run it
 			if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
-				return fmt.Errorf("failed to run custom hook: %w", err)
+				logger.Error("failed to run custom hook", "err", err)
 			}
 		}
 
diff --git a/cmd/soft/main.go b/cmd/soft/main.go
index 9849e8a..9b248fd 100644
--- a/cmd/soft/main.go
+++ b/cmd/soft/main.go
@@ -7,7 +7,8 @@ import (
 	"runtime/debug"
 	"strconv"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/colorprofile"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/cmd/soft/admin"
 	"github.com/charmbracelet/soft-serve/cmd/soft/browse"
 	"github.com/charmbracelet/soft-serve/cmd/soft/hook"
@@ -18,7 +19,6 @@ import (
 	"github.com/charmbracelet/soft-serve/pkg/version"
 	mcobra "github.com/muesli/mango-cobra"
 	"github.com/muesli/roff"
-	"github.com/muesli/termenv"
 	"github.com/spf13/cobra"
 	"go.uber.org/automaxprocs/maxprocs"
 )
@@ -67,7 +67,7 @@ var (
 
 func init() {
 	if noColor, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_NO_COLOR")); noColor {
-		common.DefaultColorProfile = termenv.Ascii
+		common.DefaultColorProfile = colorprofile.NoTTY
 	}
 
 	rootCmd.AddCommand(
diff --git a/cmd/soft/serve/serve.go b/cmd/soft/serve/serve.go
index a8546ab..8a670cc 100644
--- a/cmd/soft/serve/serve.go
+++ b/cmd/soft/serve/serve.go
@@ -107,7 +107,13 @@ var (
 				doneOnce()
 			}()
 
-			<-done
+			select {
+			case err := <-lch:
+				if err != nil {
+					return fmt.Errorf("server error: %w", err)
+				}
+			case <-done:
+			}
 
 			ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
 			defer cancel()
@@ -115,8 +121,7 @@ var (
 				return err
 			}
 
-			// wait for serve to finish
-			return <-lch
+			return nil
 		},
 	}
 )
diff --git a/cmd/soft/serve/server.go b/cmd/soft/serve/server.go
index 76e55f5..56402f1 100644
--- a/cmd/soft/serve/server.go
+++ b/cmd/soft/serve/server.go
@@ -6,7 +6,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
diff --git a/git/tree.go b/git/tree.go
index d5fbe96..7bf5b4d 100644
--- a/git/tree.go
+++ b/git/tree.go
@@ -170,7 +170,7 @@ func (e *TreeEntry) Mode() fs.FileMode {
 	case git.EntryTree:
 		return fs.ModeDir | fs.ModePerm
 	default:
-		return fs.FileMode(m)
+		return fs.FileMode(m) //nolint:gosec
 	}
 }
 
diff --git a/go.mod b/go.mod
index d866701..32d1535 100644
--- a/go.mod
+++ b/go.mod
@@ -1,36 +1,29 @@
 module github.com/charmbracelet/soft-serve
 
-go 1.22
-
-toolchain go1.23.2
-
-require (
-	github.com/charmbracelet/bubbles v0.20.0
-	github.com/charmbracelet/bubbletea v1.2.4
-	github.com/charmbracelet/glamour v0.8.0
-	github.com/charmbracelet/lipgloss v1.0.0
-	github.com/charmbracelet/wish v1.4.4
-	github.com/dustin/go-humanize v1.0.1
-	github.com/go-git/go-git/v5 v5.13.1
-	github.com/matryer/is v1.4.1
-	github.com/muesli/reflow v0.3.0
-	github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
-	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
-)
+go 1.23.0
 
 require (
-	github.com/alecthomas/chroma/v2 v2.14.0
-	github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2
+	github.com/alecthomas/chroma/v2 v2.20.0
+	github.com/aymanbagabas/bubblezone/v2 v2.0.0-20250319214444-bb232f16d5e3
+	github.com/aymanbagabas/git-module v1.8.4-0.20250826192401-1f81c5471e53
 	github.com/caarlos0/duration v0.0.0-20240108180406-5d492514f3c7
 	github.com/caarlos0/env/v11 v11.3.1
-	github.com/caarlos0/tablewriter v0.1.0
+	github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603123720-56bbc4a1ba66
+	github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250617194119-3f1d09f7d826
+	github.com/charmbracelet/colorprofile v0.3.2
 	github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92
-	github.com/charmbracelet/keygen v0.5.1
-	github.com/charmbracelet/log v0.4.0
-	github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c
-	github.com/go-jose/go-jose/v3 v3.0.3
+	github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
+	github.com/charmbracelet/keygen v0.5.3
+	github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71
+	github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
+	github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894
+	github.com/charmbracelet/wish/v2 v2.0.0-20250505151211-5996fc7c1f33
+	github.com/charmbracelet/x/ansi v0.10.1
+	github.com/dustin/go-humanize v1.0.1
+	github.com/go-git/go-git/v5 v5.16.2
+	github.com/go-jose/go-jose/v3 v3.0.4
 	github.com/gobwas/glob v0.2.3
-	github.com/golang-jwt/jwt/v5 v5.2.1
+	github.com/golang-jwt/jwt/v5 v5.3.0
 	github.com/google/go-querystring v1.1.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
@@ -38,77 +31,72 @@ require (
 	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/jmoiron/sqlx v1.4.0
 	github.com/lib/pq v1.10.9
-	github.com/lrstanley/bubblezone v0.0.0-20240723130623-7fd58a7b1f91
+	github.com/matryer/is v1.4.1
 	github.com/muesli/mango-cobra v1.2.0
+	github.com/muesli/reflow v0.3.0
 	github.com/muesli/roff v0.1.0
-	github.com/prometheus/client_golang v1.20.5
+	github.com/prometheus/client_golang v1.23.0
 	github.com/robfig/cron/v3 v3.0.1
-	github.com/rogpeppe/go-internal v1.13.1
-	github.com/spf13/cobra v1.8.1
+	github.com/rogpeppe/go-internal v1.14.1
+	github.com/sergi/go-diff v1.4.0
+	github.com/spf13/cobra v1.9.1
 	go.uber.org/automaxprocs v1.6.0
-	golang.org/x/crypto v0.31.0
-	golang.org/x/sync v0.10.0
+	golang.org/x/crypto v0.41.0
+	golang.org/x/sync v0.16.0
 	gopkg.in/yaml.v3 v3.0.1
-	modernc.org/sqlite v1.34.4
+	modernc.org/sqlite v1.38.2
 )
 
 require (
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
 	github.com/atotto/clipboard v0.1.4 // indirect
-	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/charmbracelet/x/ansi v0.4.5 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
 	github.com/charmbracelet/x/conpty v0.1.0 // indirect
 	github.com/charmbracelet/x/errors v0.0.0-20240725160154-f9f6568126ec // indirect
-	github.com/charmbracelet/x/input v0.2.0 // indirect
+	github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
+	github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 // indirect
 	github.com/charmbracelet/x/term v0.2.1 // indirect
 	github.com/charmbracelet/x/termios v0.1.0 // indirect
+	github.com/charmbracelet/x/windows v0.2.1 // indirect
 	github.com/creack/pty v1.1.21 // indirect
-	github.com/dlclark/regexp2 v1.11.2 // indirect
-	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+	github.com/dlclark/regexp2 v1.11.5 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-logfmt/logfmt v0.6.0 // indirect
 	github.com/gorilla/css v1.0.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/klauspost/compress v1.17.9 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 // indirect
 	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
-	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/mango v0.2.0 // indirect
 	github.com/muesli/mango-pflag v0.1.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
-	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.55.0 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sahilm/fuzzy v0.1.1 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
-	github.com/yuin/goldmark v1.7.4 // indirect
-	github.com/yuin/goldmark-emoji v1.0.3 // indirect
-	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
-	golang.org/x/net v0.33.0 // indirect
-	golang.org/x/sys v0.28.0 // indirect
-	golang.org/x/term v0.27.0 // indirect
-	golang.org/x/text v0.21.0 // indirect
-	golang.org/x/tools v0.23.0 // indirect
-	google.golang.org/protobuf v1.34.2 // indirect
+	github.com/yuin/goldmark v1.7.8 // indirect
+	github.com/yuin/goldmark-emoji v1.0.5 // indirect
+	golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
+	golang.org/x/net v0.42.0 // indirect
+	golang.org/x/sys v0.35.0 // indirect
+	golang.org/x/text v0.28.0 // indirect
+	golang.org/x/tools v0.35.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect
-	modernc.org/libc v1.55.3 // indirect
-	modernc.org/mathutil v1.6.0 // indirect
-	modernc.org/memory v1.8.0 // indirect
-	modernc.org/strutil v1.2.0 // indirect
-	modernc.org/token v1.1.0 // indirect
+	modernc.org/libc v1.66.3 // indirect
+	modernc.org/mathutil v1.7.1 // indirect
+	modernc.org/memory v1.11.0 // indirect
 )
diff --git a/go.sum b/go.sum
index dc37827..ba32b46 100644
--- a/go.sum
+++ b/go.sum
@@ -1,19 +1,19 @@
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
-github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
-github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
-github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
-github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
-github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
+github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
+github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
+github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 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/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
-github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2 h1:3w5KT+shE3hzWhORGiu2liVjEoaCEXm9uZP47+Gw4So=
-github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2/go.mod h1:d4gQ7/3/S2sPq4NnKdtAgUOVr6XtLpWFtxyVV5/+76U=
-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/aymanbagabas/bubblezone/v2 v2.0.0-20250319214444-bb232f16d5e3 h1:1z2ihw0YUYUhNmRaavyXvG9ZU/9Tj0vj6sA3z5DFIJ8=
+github.com/aymanbagabas/bubblezone/v2 v2.0.0-20250319214444-bb232f16d5e3/go.mod h1:sJwqZoo/BSKSizmr0pSJ758RuRsnjlkrOaxPtwlWtOs=
+github.com/aymanbagabas/git-module v1.8.4-0.20250826192401-1f81c5471e53 h1:KfKp+gVsQtuM9qb8Putvkx1jjAWqlvI1vdv5x9hdFoQ=
+github.com/aymanbagabas/git-module v1.8.4-0.20250826192401-1f81c5471e53/go.mod h1:d4gQ7/3/S2sPq4NnKdtAgUOVr6XtLpWFtxyVV5/+76U=
 github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
 github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -24,80 +24,84 @@ github.com/caarlos0/duration v0.0.0-20240108180406-5d492514f3c7 h1:kJP/C2eL9DCKr
 github.com/caarlos0/duration v0.0.0-20240108180406-5d492514f3c7/go.mod h1:mSkwb/eZEwOJJJ4tqAKiuhLIPe0e9+FKhlU0oMCpbf8=
 github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
 github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
-github.com/caarlos0/tablewriter v0.1.0 h1:HWwl/Zh3GKgVejSeG8lKHc28YBbI7bLRW2tgvxFF2DA=
-github.com/caarlos0/tablewriter v0.1.0/go.mod h1:oZ3/mQeP+SC5c1Dr6zv/6jCf0dfsUWq+PuwNw8l3ir0=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
-github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
-github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
-github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603123720-56bbc4a1ba66 h1:LDUnLzW0DZ4riy4juBAXJWeE94vvVChgZwzhZL/4wuk=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603123720-56bbc4a1ba66/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250617194119-3f1d09f7d826 h1:pQxCWMojVjHePqGzWsANhplourZYsD6AuR6eR2Hi5yc=
+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250617194119-3f1d09f7d826/go.mod h1:bfzSaUDPMKrWDjD6wo/ato9lfDdEX83rZgwcXHYWJ98=
+github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
+github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
 github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92 h1:KtQlsiHfY3K4AoIEh0yUE/wCLHteZ9EzV1hKmx+p7U8=
 github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
-github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
-github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
-github.com/charmbracelet/keygen v0.5.1 h1:zBkkYPtmKDVTw+cwUyY6ZwGDhRxXkEp0Oxs9sqMLqxI=
-github.com/charmbracelet/keygen v0.5.1/go.mod h1:zznJVmK/GWB6dAtjluqn2qsttiCBhA5MZSiwb80fcHw=
-github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
-github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
-github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
-github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
-github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c h1:treQxMBdI2PaD4eOYfFux8stfCkUxhuUxaqGcxKqVpI=
-github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c/go.mod h1:CY1xbl2z+ZeBmNWItKZyxx0zgDgnhmR57+DTsHOobJ4=
-github.com/charmbracelet/wish v1.4.4 h1:wtfoAMkf8Db9zi+9Lme2f7XKMxL6BqfgDWbqcTUHLaU=
-github.com/charmbracelet/wish v1.4.4/go.mod h1:XB8v51UxIFMRlUod9lLaAgOsj/wpe+qW9HjsoYIiNMo=
-github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM=
-github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
+github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
+github.com/charmbracelet/keygen v0.5.3 h1:2MSDC62OUbDy6VmjIE2jM24LuXUvKywLCmaJDmr/Z/4=
+github.com/charmbracelet/keygen v0.5.3/go.mod h1:TcpNoMAO5GSmhx3SgcEMqCrtn8BahKhB8AlwnLjRUpk=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 h1:X0tsNa2UHCKNw+illiavosasVzqioRo32SRV35iwr2I=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
+github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
+github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
+github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894 h1:Ffon9TbltLGBsT6XE//YvNuu4OAaThXioqalhH11xEw=
+github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894/go.mod h1:hg+I6gvlMl16nS9ZzQNgBIrrCasGwEw0QiLsDcP01Ko=
+github.com/charmbracelet/wish/v2 v2.0.0-20250505151211-5996fc7c1f33 h1:/TlxP6Lw9Z7NJCQ7TSbuvfO3/AL0UXGDPwH1zO9pV8U=
+github.com/charmbracelet/wish/v2 v2.0.0-20250505151211-5996fc7c1f33/go.mod h1:Tssk2d4IKqV3tNiPgwrYSvJMQu+nN9J4JlGmYaXpiR4=
+github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
+github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
+github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
 github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
 github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
 github.com/charmbracelet/x/errors v0.0.0-20240725160154-f9f6568126ec h1:O8c7pFFK0imuHH5JBqv5smlbVoFn4CZKGjtvCQKu1WE=
 github.com/charmbracelet/x/errors v0.0.0-20240725160154-f9f6568126ec/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
-github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
-github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
-github.com/charmbracelet/x/input v0.2.0 h1:1Sv+y/flcqUfUH2PXNIDKDIdT2G8smOnGOgawqhwy8A=
-github.com/charmbracelet/x/input v0.2.0/go.mod h1:KUSFIS6uQymtnr5lHVSOK9j8RvwTD4YHnWnzJUYnd/M=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
+github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 h1:BxAEmOBIDajkgao3EsbBxKQCYvgYPGdT62WASLvtf4Y=
+github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86/go.mod h1:62Rp/6EtTxoeJDSdtpA3tJp3y3ZRpsiekBSje+K8htA=
 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
 github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
 github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k=
 github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
+github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 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/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68=
-github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
 github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
-github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
-github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
-github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
-github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
+github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
+github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
+github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
+github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 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/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
-github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -114,8 +118,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
 github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -127,16 +131,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lrstanley/bubblezone v0.0.0-20240723130623-7fd58a7b1f91 h1:NIU7JKWRT1sQcopTxZJ6h5zNAaTy1WE1i6phrMKj8mw=
-github.com/lrstanley/bubblezone v0.0.0-20240723130623-7fd58a7b1f91/go.mod h1:fMHACHXouhQO+NLAFvHEeKdVSzG7L/O1khqsvswCTmk=
 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.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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -147,8 +147,6 @@ github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEd
 github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
 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/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
@@ -161,8 +159,6 @@ 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/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
 github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
-github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
-github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -173,14 +169,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
-github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
-github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
-github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
-github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -189,17 +185,17 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
 github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
-github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
-github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
-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/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
+github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/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=
@@ -215,73 +211,74 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
-github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
-github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
+github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
-golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
+golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
 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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210809222454-d867a43fc93e/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-20220722155257-8c9f86f7a55f/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
 golang.org/x/text v0.3.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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
-golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
+golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -293,29 +290,29 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
-modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
-modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
-modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
-modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
-modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
-modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
-modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
-modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e h1:WPC4v0rNIFb2PY+nBBEEKyugPPRHPzUgyN3xZPpGK58=
-modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
-modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
-modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
-modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
-modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
-modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
-modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
-modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
-modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
-modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
-modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
-modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
-modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
-modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
+modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
+modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
+modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
+modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
+modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
+modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/pkg/backend/access_token.go b/pkg/backend/access_token.go
index 281ad8f..b8db671 100644
--- a/pkg/backend/access_token.go
+++ b/pkg/backend/access_token.go
@@ -7,12 +7,14 @@ import (
 
 	"github.com/charmbracelet/soft-serve/pkg/db"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
+	"github.com/charmbracelet/soft-serve/pkg/utils"
 )
 
 // CreateAccessToken creates an access token for user.
 func (b *Backend) CreateAccessToken(ctx context.Context, user proto.User, name string, expiresAt time.Time) (string, error) {
 	token := GenerateToken()
 	tokenHash := HashToken(token)
+	name = utils.Sanitize(name)
 
 	if err := b.db.TransactionContext(ctx, func(tx *db.Tx) error {
 		_, err := b.store.CreateAccessToken(ctx, tx, name, user.ID(), tokenHash, expiresAt)
diff --git a/pkg/backend/auth.go b/pkg/backend/auth.go
index 2371334..7c8158f 100644
--- a/pkg/backend/auth.go
+++ b/pkg/backend/auth.go
@@ -5,7 +5,7 @@ import (
 	"crypto/sha256"
 	"encoding/hex"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"golang.org/x/crypto/bcrypt"
 )
 
diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go
index ba8796b..579d4ce 100644
--- a/pkg/backend/backend.go
+++ b/pkg/backend/backend.go
@@ -3,7 +3,7 @@ package backend
 import (
 	"context"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
 	"github.com/charmbracelet/soft-serve/pkg/store"
diff --git a/pkg/backend/repo.go b/pkg/backend/repo.go
index bff5658..e4eb3a7 100644
--- a/pkg/backend/repo.go
+++ b/pkg/backend/repo.go
@@ -10,6 +10,7 @@ import (
 	"path"
 	"path/filepath"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/charmbracelet/soft-serve/git"
@@ -24,10 +25,6 @@ import (
 	"github.com/charmbracelet/soft-serve/pkg/webhook"
 )
 
-func (d *Backend) reposPath() string {
-	return filepath.Join(d.cfg.DataPath, "repos")
-}
-
 // CreateRepository creates a new repository.
 //
 // It implements backend.Backend.
@@ -37,8 +34,7 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.
 		return nil, err
 	}
 
-	repo := name + ".git"
-	rp := filepath.Join(d.reposPath(), repo)
+	rp := filepath.Join(d.repoPath(name))
 
 	var userID int64
 	if user != nil {
@@ -78,7 +74,7 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.
 			}
 		}
 
-		return hooks.GenerateHooks(ctx, d.cfg, repo)
+		return hooks.GenerateHooks(ctx, d.cfg, name)
 	}); err != nil {
 		d.logger.Debug("failed to create repository in database", "err", err)
 		err = db.WrapError(err)
@@ -100,8 +96,7 @@ func (d *Backend) ImportRepository(_ context.Context, name string, user proto.Us
 		return nil, err
 	}
 
-	repo := name + ".git"
-	rp := filepath.Join(d.reposPath(), repo)
+	rp := filepath.Join(d.repoPath(name))
 
 	tid := "import:" + name
 	if d.manager.Exists(tid) {
@@ -217,8 +212,7 @@ func (d *Backend) ImportRepository(_ context.Context, name string, user proto.Us
 // It implements backend.Backend.
 func (d *Backend) DeleteRepository(ctx context.Context, name string) error {
 	name = utils.SanitizeRepo(name)
-	repo := name + ".git"
-	rp := filepath.Join(d.reposPath(), repo)
+	rp := filepath.Join(d.repoPath(name))
 
 	user := proto.UserFromContext(ctx)
 	r, err := d.Repository(ctx, name)
@@ -330,10 +324,8 @@ func (d *Backend) RenameRepository(ctx context.Context, oldName string, newName
 		return nil
 	}
 
-	oldRepo := oldName + ".git"
-	newRepo := newName + ".git"
-	op := filepath.Join(d.reposPath(), oldRepo)
-	np := filepath.Join(d.reposPath(), newRepo)
+	op := filepath.Join(d.repoPath(oldName))
+	np := filepath.Join(d.repoPath(newName))
 	if _, err := os.Stat(op); err != nil {
 		return proto.ErrRepoNotFound
 	}
@@ -389,7 +381,7 @@ func (d *Backend) Repositories(ctx context.Context) ([]proto.Repository, error)
 		for _, m := range ms {
 			r := &repo{
 				name: m.Name,
-				path: filepath.Join(d.reposPath(), m.Name+".git"),
+				path: filepath.Join(d.repoPath(m.Name)),
 				repo: m,
 			}
 
@@ -418,7 +410,7 @@ func (d *Backend) Repository(ctx context.Context, name string) (proto.Repository
 		return r, nil
 	}
 
-	rp := filepath.Join(d.reposPath(), name+".git")
+	rp := filepath.Join(d.repoPath(name))
 	if _, err := os.Stat(rp); err != nil {
 		if !errors.Is(err, fs.ErrNotExist) {
 			d.logger.Errorf("failed to stat repository path: %v", err)
@@ -552,7 +544,8 @@ func (d *Backend) SetHidden(ctx context.Context, name string, hidden bool) error
 // It implements backend.Backend.
 func (d *Backend) SetDescription(ctx context.Context, name string, desc string) error {
 	name = utils.SanitizeRepo(name)
-	rp := filepath.Join(d.reposPath(), name+".git")
+	desc = utils.Sanitize(desc)
+	rp := filepath.Join(d.repoPath(name))
 
 	// Delete cache
 	d.cache.Delete(name)
@@ -572,7 +565,7 @@ func (d *Backend) SetDescription(ctx context.Context, name string, desc string)
 // It implements backend.Backend.
 func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) error {
 	name = utils.SanitizeRepo(name)
-	rp := filepath.Join(d.reposPath(), name+".git")
+	rp := filepath.Join(d.repoPath(name))
 
 	// Delete cache
 	d.cache.Delete(name)
@@ -625,6 +618,7 @@ func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) err
 // It implements backend.Backend.
 func (d *Backend) SetProjectName(ctx context.Context, repo string, name string) error {
 	repo = utils.SanitizeRepo(repo)
+	name = utils.Sanitize(name)
 
 	// Delete cache
 	d.cache.Delete(repo)
@@ -636,6 +630,13 @@ func (d *Backend) SetProjectName(ctx context.Context, repo string, name string)
 	)
 }
 
+// repoPath returns the path to a repository.
+func (d *Backend) repoPath(name string) string {
+	name = utils.SanitizeRepo(name)
+	rn := strings.ReplaceAll(name, "/", string(os.PathSeparator))
+	return filepath.Join(filepath.Join(d.cfg.DataPath, "repos"), rn+".git")
+}
+
 var _ proto.Repository = (*repo)(nil)
 
 // repo is a Git repository with metadata stored in a SQLite database.
@@ -743,7 +744,7 @@ func (r *repo) writeLastModified(t time.Time) error {
 		return err
 	}
 
-	return os.WriteFile(fp, []byte(t.Format(time.RFC3339)), os.ModePerm)
+	return os.WriteFile(fp, []byte(t.Format(time.RFC3339)), os.ModePerm) //nolint:gosec
 }
 
 func readOneline(path string) (string, error) {
diff --git a/pkg/backend/user.go b/pkg/backend/user.go
index 4ad846c..736842f 100644
--- a/pkg/backend/user.go
+++ b/pkg/backend/user.go
@@ -279,6 +279,7 @@ func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.Publ
 //
 // It implements backend.Backend.
 func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {
+	username = utils.Sanitize(username)
 	username = strings.ToLower(username)
 	if err := utils.ValidateUsername(username); err != nil {
 		return nil, err
diff --git a/pkg/backend/webhooks.go b/pkg/backend/webhooks.go
index 78126b2..f217c30 100644
--- a/pkg/backend/webhooks.go
+++ b/pkg/backend/webhooks.go
@@ -4,11 +4,12 @@ import (
 	"context"
 	"encoding/json"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/db"
 	"github.com/charmbracelet/soft-serve/pkg/db/models"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/store"
+	"github.com/charmbracelet/soft-serve/pkg/utils"
 	"github.com/charmbracelet/soft-serve/pkg/webhook"
 	"github.com/google/uuid"
 )
@@ -17,6 +18,7 @@ import (
 func (b *Backend) CreateWebhook(ctx context.Context, repo proto.Repository, url string, contentType webhook.ContentType, secret string, events []webhook.Event, active bool) error {
 	dbx := db.FromContext(ctx)
 	datastore := store.FromContext(ctx)
+	url = utils.Sanitize(url)
 
 	return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
 		lastID, err := datastore.CreateWebhook(ctx, tx, repo.ID(), url, secret, int(contentType), active)
@@ -54,7 +56,7 @@ func (b *Backend) Webhook(ctx context.Context, repo proto.Repository, id int64)
 
 		wh = webhook.Hook{
 			Webhook:     h,
-			ContentType: webhook.ContentType(h.ContentType),
+			ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
 			Events:      make([]webhook.Event, len(events)),
 		}
 		for i, e := range events {
@@ -105,7 +107,7 @@ func (b *Backend) ListWebhooks(ctx context.Context, repo proto.Repository) ([]we
 
 		hooks[i] = webhook.Hook{
 			Webhook:     h,
-			ContentType: webhook.ContentType(h.ContentType),
+			ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
 			Events:      events,
 		}
 	}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 05dd2c3..6c1355e 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -61,6 +61,15 @@ type GitConfig struct {
 	MaxConnections int `env:"MAX_CONNECTIONS" yaml:"max_connections"`
 }
 
+// CORSConfig is the CORS configuration for the server.
+type CORSConfig struct {
+	AllowedHeaders []string `env:"ALLOWED_HEADERS" yaml:"allowed_headers"`
+
+	AllowedOrigins []string `env:"ALLOWED_ORIGINS" yaml:"allowed_origins"`
+
+	AllowedMethods []string `env:"ALLOWED_METHODS" yaml:"allowed_methods"`
+}
+
 // HTTPConfig is the HTTP configuration for the server.
 type HTTPConfig struct {
 	// Enabled toggles the HTTP server on/off
@@ -77,6 +86,9 @@ type HTTPConfig struct {
 
 	// PublicURL is the public URL of the HTTP server.
 	PublicURL string `env:"PUBLIC_URL" yaml:"public_url"`
+
+	// CORS is the cross-origin configuration for the HTTP server.
+	CORS CORSConfig `envPrefix:"CORS_" yaml:"cors"`
 }
 
 // StatsConfig is the configuration for the stats server.
@@ -196,6 +208,9 @@ func (c *Config) Environ() []string {
 		fmt.Sprintf("SOFT_SERVE_HTTP_TLS_KEY_PATH=%s", c.HTTP.TLSKeyPath),
 		fmt.Sprintf("SOFT_SERVE_HTTP_TLS_CERT_PATH=%s", c.HTTP.TLSCertPath),
 		fmt.Sprintf("SOFT_SERVE_HTTP_PUBLIC_URL=%s", c.HTTP.PublicURL),
+		fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS=%s", strings.Join(c.HTTP.CORS.AllowedHeaders, ",")),
+		fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS=%s", strings.Join(c.HTTP.CORS.AllowedOrigins, ",")),
+		fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS=%s", strings.Join(c.HTTP.CORS.AllowedMethods, ",")),
 		fmt.Sprintf("SOFT_SERVE_STATS_ENABLED=%t", c.Stats.Enabled),
 		fmt.Sprintf("SOFT_SERVE_STATS_LISTEN_ADDR=%s", c.Stats.ListenAddr),
 		fmt.Sprintf("SOFT_SERVE_LOG_FORMAT=%s", c.Log.Format),
@@ -355,6 +370,11 @@ func DefaultConfig() *Config {
 			Enabled:    true,
 			ListenAddr: ":23232",
 			PublicURL:  "http://localhost:23232",
+			CORS: CORSConfig{
+				AllowedHeaders: []string{"Accept", "Accept-Language", "Content-Language", "Content-Type", "Origin", "X-Requested-With", "User-Agent", "Authorization", "Access-Control-Request-Method", "Access-Control-Allow-Origin"},
+				AllowedMethods: []string{"GET", "HEAD", "POST", "PUT", "OPTIONS"},
+				AllowedOrigins: []string{"http://localhost:23232"},
+			},
 		},
 		Stats: StatsConfig{
 			Enabled:    true,
@@ -423,6 +443,8 @@ func (c *Config) Validate() error {
 
 	c.InitialAdminKeys = pks
 
+	c.HTTP.CORS.AllowedOrigins = append([]string{c.HTTP.PublicURL}, c.HTTP.CORS.AllowedOrigins...)
+
 	return nil
 }
 
diff --git a/pkg/config/file.go b/pkg/config/file.go
index 55e45c7..3710069 100644
--- a/pkg/config/file.go
+++ b/pkg/config/file.go
@@ -89,6 +89,32 @@ http:
   # Make sure to use https:// if you are using TLS.
   public_url: "{{ .HTTP.PublicURL }}"
 
+  # The cross-origin request security options
+  cors:
+    # The allowed cross-origin headers
+    allowed_headers:
+       - "Accept"
+       - "Accept-Language"
+       - "Content-Language"
+       - "Content-Type"
+       - "Origin"
+       - "X-Requested-With"
+       - "User-Agent"
+       - "Authorization"
+       - "Access-Control-Request-Method"
+       - "Access-Control-Allow-Origin"
+    # The allowed cross-origin URLs
+    allowed_origins:
+       - "{{ .HTTP.PublicURL }}" # always allowed
+       # - "https://example.com"
+    # The allowed cross-origin methods
+    allowed_methods:
+       - "GET"
+       - "HEAD"
+       - "POST"
+       - "PUT"
+       - "OPTIONS"
+
 # The stats server configuration.
 stats:
   # Enable the stats server.
diff --git a/pkg/cron/cron.go b/pkg/cron/cron.go
index 6fed17b..fa290a3 100644
--- a/pkg/cron/cron.go
+++ b/pkg/cron/cron.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/robfig/cron/v3"
 )
 
diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go
index e4e6340..30faadd 100644
--- a/pkg/daemon/daemon.go
+++ b/pkg/daemon/daemon.go
@@ -3,6 +3,7 @@ package daemon
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"path/filepath"
@@ -11,7 +12,7 @@ import (
 	"sync/atomic"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/access"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
@@ -43,16 +44,18 @@ var ErrServerClosed = fmt.Errorf("git: %w", net.ErrClosed)
 
 // GitDaemon represents a Git daemon.
 type GitDaemon struct {
-	ctx      context.Context
-	addr     string
-	finished chan struct{}
-	conns    connections
-	cfg      *config.Config
-	be       *backend.Backend
-	wg       sync.WaitGroup
-	once     sync.Once
-	logger   *log.Logger
-	done     atomic.Bool // indicates if the server has been closed
+	ctx       context.Context
+	addr      string
+	finished  chan struct{}
+	conns     connections
+	cfg       *config.Config
+	be        *backend.Backend
+	wg        sync.WaitGroup
+	once      sync.Once
+	logger    *log.Logger
+	done      atomic.Bool // indicates if the server has been closed
+	listeners []net.Listener
+	liMu      sync.Mutex
 }
 
 // NewDaemon returns a new Git daemon.
@@ -91,7 +94,9 @@ func (d *GitDaemon) Serve(listener net.Listener) error {
 
 	d.wg.Add(1)
 	defer d.wg.Done()
-	defer listener.Close() //nolint:errcheck
+	d.liMu.Lock()
+	d.listeners = append(d.listeners, listener)
+	d.liMu.Unlock()
 
 	var tempDelay time.Duration
 	for {
@@ -109,7 +114,7 @@ func (d *GitDaemon) Serve(listener net.Listener) error {
 				} else {
 					tempDelay *= 2
 				}
-				if max := 1 * time.Second; tempDelay > max {
+				if max := 1 * time.Second; tempDelay > max { //nolint:revive
 					tempDelay = max
 				}
 				time.Sleep(tempDelay)
@@ -321,11 +326,20 @@ func (d *GitDaemon) closeListener() error {
 	if d.done.Load() {
 		return ErrServerClosed
 	}
+	var err error
+	d.liMu.Lock()
+	for _, l := range d.listeners {
+		if err = l.Close(); err != nil {
+			err = errors.Join(err, fmt.Errorf("close listener %s: %w", l.Addr(), err))
+		}
+	}
+	d.listeners = d.listeners[:0]
+	d.liMu.Unlock()
 	d.once.Do(func() {
-		close(d.finished)
 		d.done.Store(true)
+		close(d.finished)
 	})
-	return nil
+	return err
 }
 
 // Shutdown gracefully shuts down the daemon.
@@ -337,8 +351,8 @@ func (d *GitDaemon) Shutdown(ctx context.Context) error {
 	err := d.closeListener()
 	finished := make(chan struct{}, 1)
 	go func() {
+		defer close(finished)
 		d.wg.Wait()
-		finished <- struct{}{}
 	}()
 	select {
 	case <-ctx.Done():
diff --git a/pkg/db/db.go b/pkg/db/db.go
index 968d042..7d5fc2e 100644
--- a/pkg/db/db.go
+++ b/pkg/db/db.go
@@ -6,7 +6,7 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/jmoiron/sqlx"
 	_ "github.com/lib/pq"  // postgres driver
diff --git a/pkg/db/logger.go b/pkg/db/logger.go
index 821576d..605ec13 100644
--- a/pkg/db/logger.go
+++ b/pkg/db/logger.go
@@ -5,7 +5,7 @@ import (
 	"database/sql"
 	"strings"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/jmoiron/sqlx"
 )
 
diff --git a/pkg/db/migrate/0003_migrate_lfs_objects.go b/pkg/db/migrate/0003_migrate_lfs_objects.go
index d7acd7b..c89775d 100644
--- a/pkg/db/migrate/0003_migrate_lfs_objects.go
+++ b/pkg/db/migrate/0003_migrate_lfs_objects.go
@@ -6,7 +6,7 @@ import (
 	"path/filepath"
 	"strconv"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
 	"github.com/charmbracelet/soft-serve/pkg/db/models"
diff --git a/pkg/db/migrate/migrate.go b/pkg/db/migrate/migrate.go
index 18bc178..8273e45 100644
--- a/pkg/db/migrate/migrate.go
+++ b/pkg/db/migrate/migrate.go
@@ -6,7 +6,7 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/db"
 )
 
diff --git a/pkg/git/git.go b/pkg/git/git.go
index ed922e2..7b18cf2 100644
--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 
 	gitm "github.com/aymanbagabas/git-module"
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 )
diff --git a/pkg/git/lfs.go b/pkg/git/lfs.go
index 3d3ae6f..212ad08 100644
--- a/pkg/git/lfs.go
+++ b/pkg/git/lfs.go
@@ -12,7 +12,7 @@ import (
 	"time"
 
 	"github.com/charmbracelet/git-lfs-transfer/transfer"
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
 	"github.com/charmbracelet/soft-serve/pkg/db/models"
diff --git a/pkg/git/lfs_auth.go b/pkg/git/lfs_auth.go
index edf0785..32a8e6c 100644
--- a/pkg/git/lfs_auth.go
+++ b/pkg/git/lfs_auth.go
@@ -7,7 +7,7 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/jwk"
 	"github.com/charmbracelet/soft-serve/pkg/lfs"
diff --git a/pkg/git/lfs_log.go b/pkg/git/lfs_log.go
index 979932d..fed14ba 100644
--- a/pkg/git/lfs_log.go
+++ b/pkg/git/lfs_log.go
@@ -2,7 +2,7 @@ package git
 
 import (
 	"github.com/charmbracelet/git-lfs-transfer/transfer"
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 )
 
 type lfsLogger struct {
diff --git a/pkg/git/service.go b/pkg/git/service.go
index 2457642..af589c2 100644
--- a/pkg/git/service.go
+++ b/pkg/git/service.go
@@ -10,7 +10,7 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 )
 
 // Service is a Git daemon service.
diff --git a/pkg/hooks/gen.go b/pkg/hooks/gen.go
index 9e44504..d06661b 100644
--- a/pkg/hooks/gen.go
+++ b/pkg/hooks/gen.go
@@ -7,7 +7,7 @@ import (
 	"path/filepath"
 	"text/template"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/utils"
 )
@@ -48,7 +48,7 @@ func GenerateHooks(_ context.Context, cfg *config.Config, repo string) error {
 		hp := filepath.Join(hooksPath, hook)
 
 		// Write the hooks primary script
-		if err := os.WriteFile(hp, []byte(hookTemplate), os.ModePerm); err != nil {
+		if err := os.WriteFile(hp, []byte(hookTemplate), os.ModePerm); err != nil { //nolint:gosec
 			return err
 		}
 
diff --git a/pkg/jobs/mirror.go b/pkg/jobs/mirror.go
index 16ea923..9ebfdd8 100644
--- a/pkg/jobs/mirror.go
+++ b/pkg/jobs/mirror.go
@@ -7,7 +7,7 @@ import (
 	"runtime"
 	"strings"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
diff --git a/pkg/lfs/basic_transfer.go b/pkg/lfs/basic_transfer.go
index 609197c..d3e4100 100644
--- a/pkg/lfs/basic_transfer.go
+++ b/pkg/lfs/basic_transfer.go
@@ -9,7 +9,7 @@ import (
 	"io"
 	"net/http"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 )
 
 // BasicTransferAdapter implements the "basic" adapter
diff --git a/pkg/lfs/http_client.go b/pkg/lfs/http_client.go
index a8b5503..c78a2e4 100644
--- a/pkg/lfs/http_client.go
+++ b/pkg/lfs/http_client.go
@@ -8,7 +8,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 )
 
 // httpClient is a Git LFS client to communicate with a LFS source API.
diff --git a/pkg/log/log.go b/pkg/log/log.go
index b9134ea..0c15525 100644
--- a/pkg/log/log.go
+++ b/pkg/log/log.go
@@ -5,7 +5,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 )
 
diff --git a/pkg/ssh/cmd/blob.go b/pkg/ssh/cmd/blob.go
index 9678a37..1aa8fbb 100644
--- a/pkg/ssh/cmd/blob.go
+++ b/pkg/ssh/cmd/blob.go
@@ -2,8 +2,8 @@ package cmd
 
 import (
 	"fmt"
+	"os"
 
-	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -12,12 +12,16 @@ import (
 )
 
 // blobCommand returns a command that prints the contents of a file.
-func blobCommand(renderer *lipgloss.Renderer) *cobra.Command {
+func blobCommand() *cobra.Command {
 	var linenumber bool
 	var color bool
 	var raw bool
+	var noColor bool
+	if testrun, ok := os.LookupEnv("SOFT_SERVE_NO_COLOR"); ok && testrun == "1" {
+		noColor = true
+	}
 
-	styles := styles.DefaultStyles(renderer)
+	styles := styles.DefaultStyles()
 	cmd := &cobra.Command{
 		Use:               "blob REPOSITORY [REFERENCE] [PATH]",
 		Aliases:           []string{"cat", "show"},
@@ -84,7 +88,7 @@ func blobCommand(renderer *lipgloss.Renderer) *cobra.Command {
 					return fmt.Errorf("binary file: use --raw to print")
 				}
 			} else {
-				if color {
+				if color && !noColor {
 					c, err = common.FormatHighlight(fp, c)
 					if err != nil {
 						return err
@@ -92,7 +96,7 @@ func blobCommand(renderer *lipgloss.Renderer) *cobra.Command {
 				}
 
 				if linenumber {
-					c, _ = common.FormatLineNumber(styles, c, color)
+					c, _ = common.FormatLineNumber(styles, c, color && !noColor)
 				}
 
 				cmd.Println(c)
diff --git a/pkg/ssh/cmd/commit.go b/pkg/ssh/cmd/commit.go
index 52af672..2dceace 100644
--- a/pkg/ssh/cmd/commit.go
+++ b/pkg/ssh/cmd/commit.go
@@ -5,22 +5,22 @@ import (
 	"strings"
 	"time"
 
-	gansi "github.com/charmbracelet/glamour/ansi"
-	"github.com/charmbracelet/lipgloss"
+	gansi "github.com/charmbracelet/glamour/v2/ansi"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	"github.com/charmbracelet/soft-serve/pkg/ui/styles"
+	"github.com/charmbracelet/soft-serve/pkg/utils"
 	"github.com/spf13/cobra"
 )
 
 // commitCommand returns a command that prints the contents of a commit.
-func commitCommand(renderer *lipgloss.Renderer) *cobra.Command {
+func commitCommand() *cobra.Command {
 	var color bool
 	var patchOnly bool
 
 	cmd := &cobra.Command{
-		Use:               "commit SHA",
+		Use:               "commit repo SHA",
 		Short:             "Print out the contents of a diff",
 		Args:              cobra.ExactArgs(2),
 		PersistentPreRunE: checkIfReadable,
@@ -55,14 +55,14 @@ func commitCommand(renderer *lipgloss.Renderer) *cobra.Command {
 				return err
 			}
 
-			commonStyle := styles.DefaultStyles(renderer)
+			commonStyle := styles.DefaultStyles()
 			style := commonStyle.Log
 
 			s := strings.Builder{}
 			commitLine := "commit " + commitSHA
-			authorLine := "Author: " + commit.Author.Name
+			authorLine := "Author: " + utils.Sanitize(commit.Author.Name)
 			dateLine := "Date:   " + commit.Committer.When.UTC().Format(time.UnixDate)
-			msgLine := strings.ReplaceAll(commit.Message, "\r\n", "\n")
+			msgLine := strings.ReplaceAll(utils.Sanitize(commit.Message), "\r\n", "\n")
 			statsLine := renderStats(diff, commonStyle, color)
 			diffLine := renderDiff(patch, color)
 
diff --git a/pkg/ssh/cmd/git.go b/pkg/ssh/cmd/git.go
index 7265c9a..620b9f0 100644
--- a/pkg/ssh/cmd/git.go
+++ b/pkg/ssh/cmd/git.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/access"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
diff --git a/pkg/ssh/cmd/repo.go b/pkg/ssh/cmd/repo.go
index 6a2bb54..fa3e72f 100644
--- a/pkg/ssh/cmd/repo.go
+++ b/pkg/ssh/cmd/repo.go
@@ -4,14 +4,13 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/spf13/cobra"
 )
 
 // RepoCommand returns a command for managing repositories.
-func RepoCommand(renderer *lipgloss.Renderer) *cobra.Command {
+func RepoCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:     "repo",
 		Aliases: []string{"repos", "repository", "repositories"},
@@ -19,10 +18,10 @@ func RepoCommand(renderer *lipgloss.Renderer) *cobra.Command {
 	}
 
 	cmd.AddCommand(
-		blobCommand(renderer),
+		blobCommand(),
 		branchCommand(),
 		collabCommand(),
-		commitCommand(renderer),
+		commitCommand(),
 		createCommand(),
 		deleteCommand(),
 		descriptionCommand(),
diff --git a/pkg/ssh/cmd/tag.go b/pkg/ssh/cmd/tag.go
index 009ac03..fc21ce0 100644
--- a/pkg/ssh/cmd/tag.go
+++ b/pkg/ssh/cmd/tag.go
@@ -3,7 +3,7 @@ package cmd
 import (
 	"strings"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
diff --git a/pkg/ssh/cmd/token.go b/pkg/ssh/cmd/token.go
index ddb4d19..6a11ebf 100644
--- a/pkg/ssh/cmd/token.go
+++ b/pkg/ssh/cmd/token.go
@@ -6,7 +6,7 @@ import (
 	"time"
 
 	"github.com/caarlos0/duration"
-	"github.com/caarlos0/tablewriter"
+	"github.com/charmbracelet/lipgloss/v2/table"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/dustin/go-humanize"
@@ -92,28 +92,25 @@ func TokenCommand() *cobra.Command {
 			}
 
 			now := time.Now()
-			return tablewriter.Render(
-				cmd.OutOrStdout(),
-				tokens,
-				[]string{"ID", "Name", "Created At", "Expires In"},
-				func(t proto.AccessToken) ([]string, error) {
-					expiresAt := "-"
-					if !t.ExpiresAt.IsZero() {
-						if now.After(t.ExpiresAt) {
-							expiresAt = "expired"
-						} else {
-							expiresAt = humanize.Time(t.ExpiresAt)
-						}
+			table := table.New().Headers("ID", "Name", "Created At", "Expires In")
+			for _, token := range tokens {
+				expiresAt := "-"
+				if !token.ExpiresAt.IsZero() {
+					if now.After(token.ExpiresAt) {
+						expiresAt = "expired"
+					} else {
+						expiresAt = humanize.Time(token.ExpiresAt)
 					}
+				}
 
-					return []string{
-						strconv.FormatInt(t.ID, 10),
-						t.Name,
-						humanize.Time(t.CreatedAt),
-						expiresAt,
-					}, nil
-				},
-			)
+				table = table.Row(strconv.FormatInt(token.ID, 10),
+					token.Name,
+					humanize.Time(token.CreatedAt),
+					expiresAt,
+				)
+			}
+			cmd.Println(table)
+			return nil
 		},
 	}
 
diff --git a/pkg/ssh/cmd/tree.go b/pkg/ssh/cmd/tree.go
index aec94e9..e5fc5fe 100644
--- a/pkg/ssh/cmd/tree.go
+++ b/pkg/ssh/cmd/tree.go
@@ -92,7 +92,7 @@ func treeCommand() *cobra.Command {
 				if size == 0 {
 					ssize = "-"
 				} else {
-					ssize = humanize.Bytes(uint64(size))
+					ssize = humanize.Bytes(uint64(size)) //nolint:gosec
 				}
 				cmd.Printf("%s\t%s\t %s\n", ent.Mode(), ssize, common.UnquoteFilename(ent.Name()))
 			}
diff --git a/pkg/ssh/cmd/webhooks.go b/pkg/ssh/cmd/webhooks.go
index e33ca9b..7421270 100644
--- a/pkg/ssh/cmd/webhooks.go
+++ b/pkg/ssh/cmd/webhooks.go
@@ -5,8 +5,9 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/caarlos0/tablewriter"
+	"github.com/charmbracelet/lipgloss/v2/table"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
+	"github.com/charmbracelet/soft-serve/pkg/utils"
 	"github.com/charmbracelet/soft-serve/pkg/webhook"
 	"github.com/dustin/go-humanize"
 	"github.com/google/uuid"
@@ -60,28 +61,24 @@ func webhookListCommand() *cobra.Command {
 				return err
 			}
 
-			return tablewriter.Render(
-				cmd.OutOrStdout(),
-				webhooks,
-				[]string{"ID", "URL", "Events", "Active", "Created At", "Updated At"},
-				func(h webhook.Hook) ([]string, error) {
-					events := make([]string, len(h.Events))
-					for i, e := range h.Events {
-						events[i] = e.String()
-					}
-
-					row := []string{
-						strconv.FormatInt(h.ID, 10),
-						h.URL,
-						strings.Join(events, ","),
-						strconv.FormatBool(h.Active),
-						humanize.Time(h.CreatedAt),
-						humanize.Time(h.UpdatedAt),
-					}
+			table := table.New().Headers("ID", "URL", "Events", "Active", "Created At", "Updated At")
+			for _, h := range webhooks {
+				events := make([]string, len(h.Events))
+				for i, e := range h.Events {
+					events[i] = e.String()
+				}
 
-					return row, nil
-				},
-			)
+				table = table.Row(
+					strconv.FormatInt(h.ID, 10),
+					utils.Sanitize(h.URL),
+					strings.Join(events, ","),
+					strconv.FormatBool(h.Active),
+					humanize.Time(h.CreatedAt),
+					humanize.Time(h.UpdatedAt),
+				)
+			}
+			cmd.Println(table)
+			return nil
 		},
 	}
 
@@ -126,7 +123,8 @@ func webhookCreateCommand() *cobra.Command {
 				return webhook.ErrInvalidContentType
 			}
 
-			return be.CreateWebhook(ctx, repo, strings.TrimSpace(args[1]), ct, secret, evs, active)
+			url := utils.Sanitize(args[1])
+			return be.CreateWebhook(ctx, repo, strings.TrimSpace(url), ct, secret, evs, active)
 		},
 	}
 
@@ -290,24 +288,21 @@ func webhookDeliveriesListCommand() *cobra.Command {
 				return err
 			}
 
-			return tablewriter.Render(
-				cmd.OutOrStdout(),
-				dels,
-				[]string{"Status", "ID", "Event", "Created At"},
-				func(d webhook.Delivery) ([]string, error) {
-					status := "❌"
-					if d.ResponseStatus >= 200 && d.ResponseStatus < 300 {
-						status = "✅"
-					}
-
-					return []string{
-						status,
-						d.ID.String(),
-						d.Event.String(),
-						humanize.Time(d.CreatedAt),
-					}, nil
-				},
-			)
+			table := table.New().Headers("Status", "ID", "Event", "Created At")
+			for _, d := range dels {
+				status := "❌"
+				if d.ResponseStatus >= 200 && d.ResponseStatus < 300 {
+					status = "✅"
+				}
+				table = table.Row(
+					status,
+					d.ID.String(),
+					d.Event.String(),
+					humanize.Time(d.CreatedAt),
+				)
+			}
+			cmd.Println(table)
+			return nil
 		},
 	}
 
@@ -368,34 +363,34 @@ func webhookDeliveriesGetCommand() *cobra.Command {
 			}
 
 			out := cmd.OutOrStdout()
-			fmt.Fprintf(out, "ID: %s\n", del.ID)
-			fmt.Fprintf(out, "Event: %s\n", del.Event)
-			fmt.Fprintf(out, "Request URL: %s\n", del.RequestURL)
-			fmt.Fprintf(out, "Request Method: %s\n", del.RequestMethod)
-			fmt.Fprintf(out, "Request Error: %s\n", del.RequestError.String)
-			fmt.Fprintf(out, "Request Headers:\n")
+			fmt.Fprintf(out, "ID: %s\n", del.ID)                             //nolint:errcheck
+			fmt.Fprintf(out, "Event: %s\n", del.Event)                       //nolint:errcheck
+			fmt.Fprintf(out, "Request URL: %s\n", del.RequestURL)            //nolint:errcheck
+			fmt.Fprintf(out, "Request Method: %s\n", del.RequestMethod)      //nolint:errcheck
+			fmt.Fprintf(out, "Request Error: %s\n", del.RequestError.String) //nolint:errcheck
+			fmt.Fprintf(out, "Request Headers:\n")                           //nolint:errcheck
 			reqHeaders := strings.Split(del.RequestHeaders, "\n")
 			for _, h := range reqHeaders {
-				fmt.Fprintf(out, "  %s\n", h)
+				fmt.Fprintf(out, "  %s\n", h) //nolint:errcheck
 			}
 
-			fmt.Fprintf(out, "Request Body:\n")
+			fmt.Fprintf(out, "Request Body:\n") //nolint:errcheck
 			reqBody := strings.Split(del.RequestBody, "\n")
 			for _, b := range reqBody {
-				fmt.Fprintf(out, "  %s\n", b)
+				fmt.Fprintf(out, "  %s\n", b) //nolint:errcheck
 			}
 
-			fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus)
-			fmt.Fprintf(out, "Response Headers:\n")
+			fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus) //nolint:errcheck
+			fmt.Fprintf(out, "Response Headers:\n")                       //nolint:errcheck
 			resHeaders := strings.Split(del.ResponseHeaders, "\n")
 			for _, h := range resHeaders {
-				fmt.Fprintf(out, "  %s\n", h)
+				fmt.Fprintf(out, "  %s\n", h) //nolint:errcheck
 			}
 
-			fmt.Fprintf(out, "Response Body:\n")
+			fmt.Fprintf(out, "Response Body:\n") //nolint:errcheck
 			resBody := strings.Split(del.ResponseBody, "\n")
 			for _, b := range resBody {
-				fmt.Fprintf(out, "  %s\n", b)
+				fmt.Fprintf(out, "  %s\n", b) //nolint:errcheck
 			}
 
 			return nil
diff --git a/pkg/ssh/middleware.go b/pkg/ssh/middleware.go
index 1bd93f9..32c31af 100644
--- a/pkg/ssh/middleware.go
+++ b/pkg/ssh/middleware.go
@@ -2,10 +2,9 @@ package ssh
 
 import (
 	"fmt"
-	"os"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
@@ -14,9 +13,7 @@ import (
 	"github.com/charmbracelet/soft-serve/pkg/sshutils"
 	"github.com/charmbracelet/soft-serve/pkg/store"
 	"github.com/charmbracelet/ssh"
-	"github.com/charmbracelet/wish"
-	bm "github.com/charmbracelet/wish/bubbletea"
-	"github.com/muesli/termenv"
+	"github.com/charmbracelet/wish/v2"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 	"github.com/spf13/cobra"
@@ -91,12 +88,6 @@ func CommandMiddleware(sh ssh.Handler) ssh.Handler {
 		ctx := s.Context()
 		cfg := config.FromContext(ctx)
 
-		renderer := bm.MakeRenderer(s)
-		if testrun, ok := os.LookupEnv("SOFT_SERVE_NO_COLOR"); ok && testrun == "1" {
-			// Disable colors when running tests.
-			renderer.SetColorProfile(termenv.Ascii)
-		}
-
 		args := s.Command()
 		cliCommandCounter.WithLabelValues(cmd.CommandName(args)).Inc()
 		rootCmd := &cobra.Command{
@@ -111,7 +102,7 @@ func CommandMiddleware(sh ssh.Handler) ssh.Handler {
 			cmd.GitUploadPackCommand(),
 			cmd.GitUploadArchiveCommand(),
 			cmd.GitReceivePackCommand(),
-			cmd.RepoCommand(renderer),
+			cmd.RepoCommand(),
 			cmd.SettingsCommand(),
 			cmd.UserCommand(),
 			cmd.InfoCommand(),
diff --git a/pkg/ssh/session.go b/pkg/ssh/session.go
index aecd461..892da8b 100644
--- a/pkg/ssh/session.go
+++ b/pkg/ssh/session.go
@@ -1,19 +1,17 @@
 package ssh
 
 import (
-	"os"
 	"time"
 
-	tea "github.com/charmbracelet/bubbletea"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/pkg/access"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	"github.com/charmbracelet/ssh"
-	"github.com/charmbracelet/wish"
-	bm "github.com/charmbracelet/wish/bubbletea"
-	"github.com/muesli/termenv"
+	"github.com/charmbracelet/wish/v2"
+	bm "github.com/charmbracelet/wish/v2/bubbletea"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 )
@@ -54,22 +52,18 @@ func SessionHandler(s ssh.Session) *tea.Program {
 		}
 	}
 
-	renderer := bm.MakeRenderer(s)
-	if testrun, ok := os.LookupEnv("SOFT_SERVE_NO_COLOR"); ok && testrun == "1" {
-		// Disable colors when running tests.
-		renderer.SetColorProfile(termenv.Ascii)
-	}
-
-	c := common.NewCommon(ctx, renderer, pty.Window.Width, pty.Window.Height)
-	c.SetValue(common.ConfigKey, cfg)
-	m := NewUI(c, initialRepo)
 	opts := bm.MakeOptions(s)
 	opts = append(opts,
 		tea.WithAltScreen(),
 		tea.WithoutCatchPanics(),
 		tea.WithMouseCellMotion(),
 		tea.WithContext(ctx),
+		tea.WithColorProfile(common.DefaultColorProfile),
 	)
+
+	c := common.NewCommon(ctx, pty.Window.Width, pty.Window.Height)
+	c.SetValue(common.ConfigKey, cfg)
+	m := NewUI(c, initialRepo)
 	p := tea.NewProgram(m, opts...)
 
 	tuiSessionCounter.WithLabelValues(initialRepo, pty.Term).Inc()
diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go
index f63b0d3..10d5ede 100644
--- a/pkg/ssh/ssh.go
+++ b/pkg/ssh/ssh.go
@@ -5,22 +5,20 @@ import (
 	"fmt"
 	"net"
 	"os"
-	"runtime"
 	"strconv"
 	"time"
 
 	"github.com/charmbracelet/keygen"
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/store"
-	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	"github.com/charmbracelet/ssh"
-	"github.com/charmbracelet/wish"
-	bm "github.com/charmbracelet/wish/bubbletea"
-	rm "github.com/charmbracelet/wish/recover"
+	"github.com/charmbracelet/wish/v2"
+	bm "github.com/charmbracelet/wish/v2/bubbletea"
+	rm "github.com/charmbracelet/wish/v2/recover"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 	gossh "golang.org/x/crypto/ssh"
@@ -71,7 +69,7 @@ func NewSSHServer(ctx context.Context) (*SSHServer, error) {
 		rm.MiddlewareWithLogger(
 			logger,
 			// BubbleTea middleware.
-			bm.MiddlewareWithProgramHandler(SessionHandler, common.DefaultColorProfile),
+			bm.MiddlewareWithProgramHandler(SessionHandler),
 			// CLI middleware.
 			CommandMiddleware,
 			// Logging middleware.
@@ -93,11 +91,10 @@ func NewSSHServer(ctx context.Context) (*SSHServer, error) {
 		wish.WithHostKeyPath(cfg.SSH.KeyPath),
 		wish.WithMiddleware(mw...),
 	}
-	if runtime.GOOS == "windows" {
-		opts = append(opts, ssh.EmulatePty())
-	} else {
-		opts = append(opts, ssh.AllocatePty())
-	}
+
+	// TODO: Support a real PTY in future version.
+	opts = append(opts, ssh.EmulatePty())
+
 	s.srv, err = wish.NewServer(opts...)
 	if err != nil {
 		return nil, err
diff --git a/pkg/ssh/ui.go b/pkg/ssh/ui.go
index 31f6a25..e08cc89 100644
--- a/pkg/ssh/ui.go
+++ b/pkg/ssh/ui.go
@@ -3,10 +3,10 @@ package ssh
 import (
 	"errors"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -179,40 +179,34 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				cmds = append(cmds, cmd)
 			}
 		}
-	case tea.KeyMsg, tea.MouseMsg:
-		switch msg := msg.(type) {
-		case tea.KeyMsg:
+	case tea.KeyPressMsg:
+		switch {
+		case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
+			ui.error = nil
+			ui.state = readyState
+			// Always show the footer on error.
+			ui.showFooter = ui.footer.ShowAll()
+		case key.Matches(msg, ui.common.KeyMap.Help):
+			cmds = append(cmds, footer.ToggleFooterCmd)
+		case key.Matches(msg, ui.common.KeyMap.Quit):
+			if !ui.IsFiltering() {
+				// Stop bubblezone background workers.
+				ui.common.Zone.Close()
+				return ui, tea.Quit
+			}
+		case ui.activePage == repoPage &&
+			ui.pages[ui.activePage].(*repo.Repo).Path() == "" &&
+			key.Matches(msg, ui.common.KeyMap.Back):
+			ui.activePage = selectionPage
+			// Always show the footer on selection page.
+			ui.showFooter = true
+		}
+	case tea.MouseClickMsg:
+		switch msg.Button {
+		case tea.MouseLeft:
 			switch {
-			case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
-				ui.error = nil
-				ui.state = readyState
-				// Always show the footer on error.
-				ui.showFooter = ui.footer.ShowAll()
-			case key.Matches(msg, ui.common.KeyMap.Help):
+			case ui.common.Zone.Get("footer").InBounds(msg):
 				cmds = append(cmds, footer.ToggleFooterCmd)
-			case key.Matches(msg, ui.common.KeyMap.Quit):
-				if !ui.IsFiltering() {
-					// Stop bubblezone background workers.
-					ui.common.Zone.Close()
-					return ui, tea.Quit
-				}
-			case ui.activePage == repoPage &&
-				ui.pages[ui.activePage].(*repo.Repo).Path() == "" &&
-				key.Matches(msg, ui.common.KeyMap.Back):
-				ui.activePage = selectionPage
-				// Always show the footer on selection page.
-				ui.showFooter = true
-			}
-		case tea.MouseMsg:
-			if msg.Action != tea.MouseActionPress {
-				break
-			}
-			switch msg.Button {
-			case tea.MouseButtonLeft:
-				switch {
-				case ui.common.Zone.Get("footer").InBounds(msg):
-					cmds = append(cmds, footer.ToggleFooterCmd)
-				}
 			}
 		}
 	case footer.ToggleFooterMsg:
diff --git a/pkg/store/database/database.go b/pkg/store/database/database.go
index 02c4bd1..e41087c 100644
--- a/pkg/store/database/database.go
+++ b/pkg/store/database/database.go
@@ -3,7 +3,7 @@ package database
 import (
 	"context"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
 	"github.com/charmbracelet/soft-serve/pkg/store"
diff --git a/pkg/ui/common/common.go b/pkg/ui/common/common.go
index 93d40f5..d6e0a77 100644
--- a/pkg/ui/common/common.go
+++ b/pkg/ui/common/common.go
@@ -5,16 +5,14 @@ import (
 	"fmt"
 
 	"github.com/alecthomas/chroma/v2/lexers"
-	"github.com/charmbracelet/lipgloss"
-	"github.com/charmbracelet/log"
+	zone "github.com/aymanbagabas/bubblezone/v2"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/ui/keymap"
 	"github.com/charmbracelet/soft-serve/pkg/ui/styles"
 	"github.com/charmbracelet/ssh"
-	zone "github.com/lrstanley/bubblezone"
-	"github.com/muesli/termenv"
 )
 
 type contextKey struct {
@@ -34,27 +32,23 @@ type Common struct {
 	Styles        *styles.Styles
 	KeyMap        *keymap.KeyMap
 	Zone          *zone.Manager
-	Renderer      *lipgloss.Renderer
-	Output        *termenv.Output
 	Logger        *log.Logger
 	HideCloneCmd  bool
 }
 
 // NewCommon returns a new Common struct.
-func NewCommon(ctx context.Context, out *lipgloss.Renderer, width, height int) Common {
+func NewCommon(ctx context.Context, width, height int) Common {
 	if ctx == nil {
 		ctx = context.TODO()
 	}
 	return Common{
-		ctx:      ctx,
-		Width:    width,
-		Height:   height,
-		Renderer: out,
-		Output:   out.Output(),
-		Styles:   styles.DefaultStyles(out),
-		KeyMap:   keymap.DefaultKeyMap(),
-		Zone:     zone.New(),
-		Logger:   log.FromContext(ctx).WithPrefix("ui"),
+		ctx:    ctx,
+		Width:  width,
+		Height: height,
+		Styles: styles.DefaultStyles(),
+		KeyMap: keymap.DefaultKeyMap(),
+		Zone:   zone.New(),
+		Logger: log.FromContext(ctx).WithPrefix("ui"),
 	}
 }
 
@@ -123,3 +117,9 @@ func IsFileMarkdown(content, ext string) bool {
 	}
 	return lang == "markdown"
 }
+
+// ScrollPercent returns a string representing the scroll percentage of the
+// viewport.
+func ScrollPercent(position int) string {
+	return fmt.Sprintf("≡ %d%%", position)
+}
diff --git a/pkg/ui/common/component.go b/pkg/ui/common/component.go
index 6fe7761..4b152a4 100644
--- a/pkg/ui/common/component.go
+++ b/pkg/ui/common/component.go
@@ -1,13 +1,14 @@
 package common
 
 import (
-	"github.com/charmbracelet/bubbles/help"
-	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/bubbles/v2/help"
+	tea "github.com/charmbracelet/bubbletea/v2"
 )
 
 // Component represents a Bubble Tea model that implements a SetSize function.
 type Component interface {
 	tea.Model
+	tea.ViewModel
 	help.KeyMap
 	SetSize(width, height int)
 }
diff --git a/pkg/ui/common/error.go b/pkg/ui/common/error.go
index 753f5f2..926df1d 100644
--- a/pkg/ui/common/error.go
+++ b/pkg/ui/common/error.go
@@ -3,7 +3,7 @@ package common
 import (
 	"errors"
 
-	tea "github.com/charmbracelet/bubbletea"
+	tea "github.com/charmbracelet/bubbletea/v2"
 )
 
 // ErrMissingRepo indicates that the requested repository could not be found.
diff --git a/pkg/ui/common/format.go b/pkg/ui/common/format.go
index 4344e05..f82c71a 100644
--- a/pkg/ui/common/format.go
+++ b/pkg/ui/common/format.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/alecthomas/chroma/v2/lexers"
-	gansi "github.com/charmbracelet/glamour/ansi"
+	gansi "github.com/charmbracelet/glamour/v2/ansi"
 	"github.com/charmbracelet/soft-serve/pkg/ui/styles"
 )
 
diff --git a/pkg/ui/common/style.go b/pkg/ui/common/style.go
index 06d7ce3..1f2032f 100644
--- a/pkg/ui/common/style.go
+++ b/pkg/ui/common/style.go
@@ -1,13 +1,13 @@
 package common
 
 import (
-	gansi "github.com/charmbracelet/glamour/ansi"
-	"github.com/charmbracelet/glamour/styles"
-	"github.com/muesli/termenv"
+	"github.com/charmbracelet/colorprofile"
+	gansi "github.com/charmbracelet/glamour/v2/ansi"
+	"github.com/charmbracelet/glamour/v2/styles"
 )
 
 // DefaultColorProfile is the default color profile used by the SSH server.
-var DefaultColorProfile = termenv.ANSI256
+var DefaultColorProfile = colorprofile.ANSI256
 
 func strptr(s string) *string {
 	return &s
@@ -17,29 +17,20 @@ func strptr(s string) *string {
 func StyleConfig() gansi.StyleConfig {
 	noColor := strptr("")
 	s := styles.DarkStyleConfig
-	s.H1.BackgroundColor = noColor
-	s.H1.Prefix = "# "
-	s.H1.Suffix = ""
-	s.H1.Color = strptr("39")
-	s.Document.StylePrimitive.Color = noColor
-	s.CodeBlock.Chroma.Text.Color = noColor
-	s.CodeBlock.Chroma.Name.Color = noColor
 	// This fixes an issue with the default style config. For example
 	// highlighting empty spaces with red in Dockerfile type.
 	s.CodeBlock.Chroma.Error.BackgroundColor = noColor
 	return s
 }
 
-// StyleRenderer returns a new Glamour renderer with the DefaultColorProfile.
+// StyleRenderer returns a new Glamour renderer.
 func StyleRenderer() gansi.RenderContext {
 	return StyleRendererWithStyles(StyleConfig())
 }
 
-// StyleRendererWithStyles returns a new Glamour renderer with the
-// DefaultColorProfile and styles.
+// StyleRendererWithStyles returns a new Glamour renderer.
 func StyleRendererWithStyles(styles gansi.StyleConfig) gansi.RenderContext {
 	return gansi.NewRenderContext(gansi.Options{
-		ColorProfile: DefaultColorProfile,
-		Styles:       styles,
+		Styles: styles,
 	})
 }
diff --git a/pkg/ui/common/utils.go b/pkg/ui/common/utils.go
index dac9ac2..d5b9c91 100644
--- a/pkg/ui/common/utils.go
+++ b/pkg/ui/common/utils.go
@@ -9,11 +9,11 @@ import (
 )
 
 // TruncateString is a convenient wrapper around truncate.TruncateString.
-func TruncateString(s string, max int) string {
+func TruncateString(s string, max int) string { //nolint:revive
 	if max < 0 {
-		max = 0
+		max = 0 //nolint:revive
 	}
-	return truncate.StringWithTail(s, uint(max), "…")
+	return truncate.StringWithTail(s, uint(max), "…") //nolint:gosec
 }
 
 // RepoURL returns the URL of the repository.
diff --git a/pkg/ui/components/code/code.go b/pkg/ui/components/code/code.go
index af76972..6da77c6 100644
--- a/pkg/ui/components/code/code.go
+++ b/pkg/ui/components/code/code.go
@@ -6,13 +6,12 @@ import (
 	"sync"
 
 	"github.com/alecthomas/chroma/v2/lexers"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/glamour"
-	gansi "github.com/charmbracelet/glamour/ansi"
-	"github.com/charmbracelet/lipgloss"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/glamour/v2"
+	gansi "github.com/charmbracelet/glamour/v2/ansi"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	vp "github.com/charmbracelet/soft-serve/pkg/ui/components/viewport"
-	"github.com/muesli/termenv"
 )
 
 const (
@@ -77,7 +76,11 @@ func (r *Code) SetSideNote(s string) tea.Cmd {
 
 // Init implements tea.Model.
 func (r *Code) Init() tea.Cmd {
-	w := r.common.Width
+	// XXX: We probably won't need the GetHorizontalFrameSize margin
+	// subtraction if we get the new viewport soft wrapping to play nicely with
+	// Glamour. This also introduces a bug where when it soft wraps, the
+	// viewport scrolls left/right for 2 columns on each side of the screen.
+	w := r.common.Width - r.common.Styles.App.GetHorizontalFrameSize()
 	content := r.content
 	if content == "" {
 		r.Viewport.Model.SetContent(r.NoContentStyle.String())
@@ -110,7 +113,7 @@ func (r *Code) Init() tea.Cmd {
 
 	if r.sidenote != "" {
 		lines := strings.Split(r.sidenote, "\n")
-		sideNoteWidth := int(math.Ceil(float64(r.Model.Width) * r.SideNotePercent))
+		sideNoteWidth := int(math.Ceil(float64(r.Model.Width()) * r.SideNotePercent))
 		for i, l := range lines {
 			lines[i] = common.TruncateString(l, sideNoteWidth)
 		}
@@ -121,7 +124,7 @@ func (r *Code) Init() tea.Cmd {
 	// https://github.com/muesli/reflow/issues/43
 	//
 	// TODO: solve this upstream in Glamour/Reflow.
-	content = r.common.Renderer.NewStyle().Width(w).Render(content)
+	content = lipgloss.NewStyle().Width(w).Render(content)
 
 	r.Viewport.Model.SetContent(content)
 
@@ -169,26 +172,6 @@ func (r *Code) HalfViewUp() {
 	r.Viewport.HalfViewUp()
 }
 
-// ViewUp moves the viewport up by a page.
-func (r *Code) ViewUp() []string {
-	return r.Viewport.ViewUp()
-}
-
-// ViewDown moves the viewport down by a page.
-func (r *Code) ViewDown() []string {
-	return r.Viewport.ViewDown()
-}
-
-// LineUp moves the viewport up by the given number of lines.
-func (r *Code) LineUp(n int) []string {
-	return r.Viewport.LineUp(n)
-}
-
-// LineDown moves the viewport down by the given number of lines.
-func (r *Code) LineDown(n int) []string {
-	return r.Viewport.LineDown(n)
-}
-
 // ScrollPercent returns the viewport's scroll percentage.
 func (r *Code) ScrollPercent() float64 {
 	return r.Viewport.ScrollPercent()
@@ -244,8 +227,7 @@ func (r *Code) renderFile(path, content string) (string, error) {
 		var m uint
 		st.CodeBlock.Margin = &m
 		rc = gansi.NewRenderContext(gansi.Options{
-			ColorProfile: termenv.TrueColor,
-			Styles:       st,
+			Styles: st,
 		})
 	}
 	err := formatter.Render(&s, rc)
diff --git a/pkg/ui/components/footer/footer.go b/pkg/ui/components/footer/footer.go
index 44c800f..05e1160 100644
--- a/pkg/ui/components/footer/footer.go
+++ b/pkg/ui/components/footer/footer.go
@@ -1,10 +1,10 @@
 package footer
 
 import (
-	"github.com/charmbracelet/bubbles/help"
-	"github.com/charmbracelet/bubbles/key"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/help"
+	"github.com/charmbracelet/bubbles/v2/key"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 )
 
diff --git a/pkg/ui/components/header/header.go b/pkg/ui/components/header/header.go
index 7de5114..63e47a4 100644
--- a/pkg/ui/components/header/header.go
+++ b/pkg/ui/components/header/header.go
@@ -3,7 +3,7 @@ package header
 import (
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 )
 
diff --git a/pkg/ui/components/selector/selector.go b/pkg/ui/components/selector/selector.go
index 72239d4..e42ee35 100644
--- a/pkg/ui/components/selector/selector.go
+++ b/pkg/ui/components/selector/selector.go
@@ -3,9 +3,9 @@ package selector
 import (
 	"sync"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 )
 
@@ -230,16 +230,14 @@ func (s *Selector) Init() tea.Cmd {
 func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	cmds := make([]tea.Cmd, 0)
 	switch msg := msg.(type) {
-	case tea.MouseMsg:
-		if msg.Action != tea.MouseActionPress {
-			break
-		}
-		switch msg.Button {
-		case tea.MouseButtonWheelUp:
+	case tea.MouseClickMsg:
+		m := msg.Mouse()
+		switch m.Button {
+		case tea.MouseWheelUp:
 			s.CursorUp()
-		case tea.MouseButtonWheelDown:
+		case tea.MouseWheelDown:
 			s.CursorDown()
-		case tea.MouseButtonLeft:
+		case tea.MouseLeft:
 			curIdx := s.Index()
 			for i, item := range s.Items() {
 				item, _ := item.(IdentifiableItem)
@@ -254,7 +252,7 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				}
 			}
 		}
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		filterState := s.FilterState()
 		switch {
 		case key.Matches(msg, s.common.KeyMap.Help):
diff --git a/pkg/ui/components/statusbar/statusbar.go b/pkg/ui/components/statusbar/statusbar.go
index 2b9e188..f913e64 100644
--- a/pkg/ui/components/statusbar/statusbar.go
+++ b/pkg/ui/components/statusbar/statusbar.go
@@ -1,10 +1,10 @@
 package statusbar
 
 import (
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
-	"github.com/muesli/reflow/truncate"
+	"github.com/charmbracelet/x/ansi"
 )
 
 // Model is a status bar model.
@@ -75,12 +75,12 @@ func (s *Model) View() string {
 	}
 	branch := st.StatusBarBranch.Render(s.extra)
 	maxWidth := s.common.Width - w(key) - w(info) - w(branch) - w(help)
-	v := truncate.StringWithTail(s.value, uint(maxWidth-st.StatusBarValue.GetHorizontalFrameSize()), "…")
+	v := ansi.Truncate(s.value, maxWidth-st.StatusBarValue.GetHorizontalFrameSize(), "…")
 	value := st.StatusBarValue.
 		Width(maxWidth).
 		Render(v)
 
-	return s.common.Renderer.NewStyle().MaxWidth(s.common.Width).
+	return lipgloss.NewStyle().MaxWidth(s.common.Width).
 		Render(
 			lipgloss.JoinHorizontal(lipgloss.Top,
 				key,
diff --git a/pkg/ui/components/tabs/tabs.go b/pkg/ui/components/tabs/tabs.go
index bd31566..36f0a19 100644
--- a/pkg/ui/components/tabs/tabs.go
+++ b/pkg/ui/components/tabs/tabs.go
@@ -3,8 +3,8 @@ package tabs
 import (
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 )
 
@@ -54,7 +54,7 @@ func (t *Tabs) Init() tea.Cmd {
 func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	cmds := make([]tea.Cmd, 0)
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch msg.String() {
 		case "tab":
 			t.activeTab = (t.activeTab + 1) % len(t.tabs)
@@ -63,12 +63,9 @@ func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			t.activeTab = (t.activeTab - 1 + len(t.tabs)) % len(t.tabs)
 			cmds = append(cmds, t.activeTabCmd)
 		}
-	case tea.MouseMsg:
-		if msg.Action != tea.MouseActionPress {
-			break
-		}
+	case tea.MouseClickMsg:
 		switch msg.Button {
-		case tea.MouseButtonLeft:
+		case tea.MouseLeft:
 			for i, tab := range t.tabs {
 				if t.common.Zone.Get(tab).InBounds(msg) {
 					t.activeTab = i
@@ -109,7 +106,7 @@ func (t *Tabs) View() string {
 			s.WriteString(sep.String())
 		}
 	}
-	return t.common.Renderer.NewStyle().
+	return lipgloss.NewStyle().
 		MaxWidth(t.common.Width).
 		Render(s.String())
 }
diff --git a/pkg/ui/components/viewport/viewport.go b/pkg/ui/components/viewport/viewport.go
index cd7b6c9..95bd06b 100644
--- a/pkg/ui/components/viewport/viewport.go
+++ b/pkg/ui/components/viewport/viewport.go
@@ -1,9 +1,9 @@
 package viewport
 
 import (
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/viewport"
-	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/viewport"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 )
 
@@ -15,7 +15,9 @@ type Viewport struct {
 
 // New returns a new Viewport.
 func New(c common.Common) *Viewport {
-	vp := viewport.New(c.Width, c.Height)
+	vp := viewport.New()
+	vp.SetWidth(c.Width)
+	vp.SetHeight(c.Height)
 	vp.MouseWheelEnabled = true
 	return &Viewport{
 		common: c,
@@ -26,8 +28,8 @@ func New(c common.Common) *Viewport {
 // SetSize implements common.Component.
 func (v *Viewport) SetSize(width, height int) {
 	v.common.SetSize(width, height)
-	v.Model.Width = width
-	v.Model.Height = height
+	v.Model.SetWidth(width)
+	v.Model.SetHeight(height)
 }
 
 // Init implements tea.Model.
@@ -38,7 +40,7 @@ func (v *Viewport) Init() tea.Cmd {
 // Update implements tea.Model.
 func (v *Viewport) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, v.common.KeyMap.GotoTop):
 			v.GotoTop()
@@ -73,32 +75,12 @@ func (v *Viewport) GotoBottom() {
 
 // HalfViewDown moves the viewport down by half the viewport height.
 func (v *Viewport) HalfViewDown() {
-	v.Model.HalfViewDown()
+	v.Model.HalfPageDown()
 }
 
 // HalfViewUp moves the viewport up by half the viewport height.
 func (v *Viewport) HalfViewUp() {
-	v.Model.HalfViewUp()
-}
-
-// ViewUp moves the viewport up by a page.
-func (v *Viewport) ViewUp() []string {
-	return v.Model.ViewUp()
-}
-
-// ViewDown moves the viewport down by a page.
-func (v *Viewport) ViewDown() []string {
-	return v.Model.ViewDown()
-}
-
-// LineUp moves the viewport up by the given number of lines.
-func (v *Viewport) LineUp(n int) []string {
-	return v.Model.LineUp(n)
-}
-
-// LineDown moves the viewport down by the given number of lines.
-func (v *Viewport) LineDown(n int) []string {
-	return v.Model.LineDown(n)
+	v.Model.HalfPageUp()
 }
 
 // ScrollPercent returns the viewport's scroll percentage.
diff --git a/pkg/ui/keymap/keymap.go b/pkg/ui/keymap/keymap.go
index ae09eaa..707a558 100644
--- a/pkg/ui/keymap/keymap.go
+++ b/pkg/ui/keymap/keymap.go
@@ -1,6 +1,6 @@
 package keymap
 
-import "github.com/charmbracelet/bubbles/key"
+import "github.com/charmbracelet/bubbles/v2/key"
 
 // KeyMap is a map of key bindings for the UI.
 type KeyMap struct {
@@ -198,7 +198,7 @@ func DefaultKeyMap() *KeyMap {
 			"right",
 		),
 		key.WithHelp(
-			"→",
+			"→/l",
 			"select",
 		),
 	)
@@ -210,7 +210,7 @@ func DefaultKeyMap() *KeyMap {
 			"backspace",
 		),
 		key.WithHelp(
-			"←",
+			"←/h",
 			"back",
 		),
 	)
diff --git a/pkg/ui/pages/repo/files.go b/pkg/ui/pages/repo/files.go
index fd43969..7ae477e 100644
--- a/pkg/ui/pages/repo/files.go
+++ b/pkg/ui/pages/repo/files.go
@@ -7,9 +7,9 @@ import (
 	"strings"
 
 	gitm "github.com/aymanbagabas/git-module"
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/spinner"
-	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/spinner"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -151,17 +151,7 @@ func (f *Files) ShortHelp() []key.Binding {
 func (f *Files) FullHelp() [][]key.Binding {
 	b := make([][]key.Binding, 0)
 	copyKey := f.common.KeyMap.Copy
-	actionKeys := []key.Binding{
-		copyKey,
-	}
-	if !f.code.UseGlamour {
-		actionKeys = append(actionKeys, lineNo)
-	}
-	actionKeys = append(actionKeys, blameView)
-	if common.IsFileMarkdown(f.currentContent.content, f.currentContent.ext) &&
-		!f.blameView {
-		actionKeys = append(actionKeys, preview)
-	}
+	actionKeys := []key.Binding{}
 	switch f.activeView {
 	case filesViewFiles:
 		copyKey.SetHelp("c", "copy name")
@@ -183,6 +173,14 @@ func (f *Files) FullHelp() [][]key.Binding {
 			},
 		}...)
 	case filesViewContent:
+		if !f.code.UseGlamour {
+			actionKeys = append(actionKeys, lineNo)
+		}
+		actionKeys = append(actionKeys, blameView)
+		if common.IsFileMarkdown(f.currentContent.content, f.currentContent.ext) &&
+			!f.blameView {
+			actionKeys = append(actionKeys, preview)
+		}
 		copyKey.SetHelp("c", "copy content")
 		k := f.code.KeyMap
 		b = append(b, []key.Binding{
@@ -203,6 +201,9 @@ func (f *Files) FullHelp() [][]key.Binding {
 			},
 		}...)
 	}
+	actionKeys = append([]key.Binding{
+		copyKey,
+	}, actionKeys...)
 	return append(b, actionKeys)
 }
 
@@ -264,7 +265,7 @@ func (f *Files) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case filesViewFiles, filesViewContent:
 			cmds = append(cmds, f.deselectItemCmd())
 		}
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch f.activeView {
 		case filesViewFiles:
 			switch {
@@ -383,7 +384,7 @@ func (f *Files) StatusBarInfo() string {
 	case filesViewFiles:
 		return fmt.Sprintf("# %d/%d", f.selector.Index()+1, len(f.selector.VisibleItems()))
 	case filesViewContent:
-		return fmt.Sprintf("☰ %d%%", f.code.ScrollPosition())
+		return common.ScrollPercent(f.code.ScrollPosition())
 	default:
 		return ""
 	}
diff --git a/pkg/ui/pages/repo/filesitem.go b/pkg/ui/pages/repo/filesitem.go
index a32fcea..5c3e560 100644
--- a/pkg/ui/pages/repo/filesitem.go
+++ b/pkg/ui/pages/repo/filesitem.go
@@ -6,10 +6,10 @@ import (
 	"io/fs"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	"github.com/dustin/go-humanize"
@@ -82,7 +82,7 @@ func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 		return nil
 	}
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, d.common.KeyMap.Copy):
 			return copyCmd(item.entry.Name(), fmt.Sprintf("File name %q copied to clipboard", item.entry.Name()))
@@ -101,7 +101,7 @@ func (d FileItemDelegate) Render(w io.Writer, m list.Model, index int, listItem
 	s := d.common.Styles.Tree
 
 	name := i.Title()
-	size := humanize.Bytes(uint64(i.entry.Size()))
+	size := humanize.Bytes(uint64(i.entry.Size())) //nolint:gosec
 	size = strings.ReplaceAll(size, " ", "")
 	sizeLen := lipgloss.Width(size)
 	if i.entry.IsTree() {
@@ -139,7 +139,7 @@ func (d FileItemDelegate) Render(w io.Writer, m list.Model, index int, listItem
 	name = nameStyle.Render(name)
 	size = sizeStyle.Render(size)
 	modeStr := modeStyle.Render(mode.String())
-	truncate := d.common.Renderer.NewStyle().MaxWidth(m.Width() -
+	truncate := lipgloss.NewStyle().MaxWidth(m.Width() -
 		s.Selector.GetHorizontalFrameSize() -
 		s.Selector.GetWidth())
 	//nolint:errcheck
diff --git a/pkg/ui/pages/repo/log.go b/pkg/ui/pages/repo/log.go
index 8de8aa7..e38399f 100644
--- a/pkg/ui/pages/repo/log.go
+++ b/pkg/ui/pages/repo/log.go
@@ -5,11 +5,11 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/spinner"
-	tea "github.com/charmbracelet/bubbletea"
-	gansi "github.com/charmbracelet/glamour/ansi"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/spinner"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	gansi "github.com/charmbracelet/glamour/v2/ansi"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -117,9 +117,12 @@ func (l *Log) ShortHelp() []key.Binding {
 			copyKey,
 		}
 	case logViewDiff:
+		copyKey := l.common.KeyMap.Copy
+		copyKey.SetHelp("c", "copy diff")
 		return []key.Binding{
 			l.common.KeyMap.UpDown,
 			l.common.KeyMap.BackItem,
+			copyKey,
 			l.common.KeyMap.GotoTop,
 			l.common.KeyMap.GotoBottom,
 		}
@@ -154,9 +157,12 @@ func (l *Log) FullHelp() [][]key.Binding {
 			},
 		}...)
 	case logViewDiff:
+		copyKey := l.common.KeyMap.Copy
+		copyKey.SetHelp("c", "copy diff")
 		k := l.vp.KeyMap
 		b = append(b, []key.Binding{
 			l.common.KeyMap.BackItem,
+			copyKey,
 		})
 		b = append(b, [][]key.Binding{
 			{
@@ -221,11 +227,11 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if i != nil {
 			l.activeCommit = i.(LogItem).Commit
 		}
-	case tea.KeyMsg, tea.MouseMsg:
+	case tea.KeyPressMsg, tea.MouseClickMsg:
 		switch l.activeView {
 		case logViewCommits:
 			switch kmsg := msg.(type) {
-			case tea.KeyMsg:
+			case tea.KeyPressMsg:
 				switch {
 				case key.Matches(kmsg, l.common.KeyMap.SelectItem):
 					cmds = append(cmds, l.selector.SelectItemCmd)
@@ -248,10 +254,14 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			cmds = append(cmds, cmd)
 		case logViewDiff:
 			switch kmsg := msg.(type) {
-			case tea.KeyMsg:
+			case tea.KeyPressMsg:
 				switch {
 				case key.Matches(kmsg, l.common.KeyMap.BackItem):
 					l.goBack()
+				case key.Matches(kmsg, l.common.KeyMap.Copy):
+					if l.currentDiff != nil {
+						cmds = append(cmds, copyCmd(l.currentDiff.Patch(), "Commit diff copied to clipboard"))
+					}
 				}
 			}
 		}
diff --git a/pkg/ui/pages/repo/logitem.go b/pkg/ui/pages/repo/logitem.go
index ecdc4f1..1aefd53 100644
--- a/pkg/ui/pages/repo/logitem.go
+++ b/pkg/ui/pages/repo/logitem.go
@@ -6,10 +6,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	"github.com/muesli/reflow/truncate"
@@ -62,7 +62,7 @@ func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 		return nil
 	}
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, d.common.KeyMap.Copy):
 			return copyCmd(item.Hash(), "Commit hash copied to clipboard")
@@ -130,7 +130,7 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
 	}
 	who += styles.Desc.Render("on ") + styles.Keyword.Render(date)
 	who = common.TruncateString(who, m.Width()-horizontalFrameSize)
-	fmt.Fprint(w,
+	fmt.Fprint(w, //nolint:errcheck
 		d.common.Zone.Mark(
 			i.ID(),
 			styles.Base.Render(
@@ -138,7 +138,7 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
 					truncate.String(fmt.Sprintf("%s%s",
 						title,
 						hash,
-					), uint(m.Width()-horizontalFrameSize)),
+					), uint(m.Width()-horizontalFrameSize)), //nolint:gosec
 					who,
 				),
 			),
diff --git a/pkg/ui/pages/repo/readme.go b/pkg/ui/pages/repo/readme.go
index 74c5be2..13f4cb4 100644
--- a/pkg/ui/pages/repo/readme.go
+++ b/pkg/ui/pages/repo/readme.go
@@ -1,12 +1,11 @@
 package repo
 
 import (
-	"fmt"
 	"path/filepath"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/spinner"
-	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/spinner"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -157,7 +156,7 @@ func (r *Readme) StatusBarValue() string {
 
 // StatusBarInfo implements statusbar.StatusBar.
 func (r *Readme) StatusBarInfo() string {
-	return fmt.Sprintf("☰ %d%%", r.code.ScrollPosition())
+	return common.ScrollPercent(r.code.ScrollPosition())
 }
 
 func (r *Readme) updateReadmeCmd() tea.Msg {
diff --git a/pkg/ui/pages/repo/refs.go b/pkg/ui/pages/repo/refs.go
index f6d9c56..45a6b18 100644
--- a/pkg/ui/pages/repo/refs.go
+++ b/pkg/ui/pages/repo/refs.go
@@ -5,9 +5,9 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/spinner"
-	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/spinner"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -152,7 +152,7 @@ func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				switchTabCmd(&Files{}),
 			)
 		}
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, r.common.KeyMap.SelectItem):
 			cmds = append(cmds, r.selector.SelectItemCmd)
diff --git a/pkg/ui/pages/repo/refsitem.go b/pkg/ui/pages/repo/refsitem.go
index 7d109de..e890150 100644
--- a/pkg/ui/pages/repo/refsitem.go
+++ b/pkg/ui/pages/repo/refsitem.go
@@ -6,10 +6,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	"github.com/dustin/go-humanize"
@@ -83,7 +83,7 @@ func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 		return nil
 	}
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, d.common.KeyMap.Copy):
 			return copyCmd(item.ID(), fmt.Sprintf("Reference %q copied to clipboard", item.ID()))
@@ -191,13 +191,13 @@ func (d RefItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
 			PaddingLeft(1).
 			Render(sha)
 	}
-	fmt.Fprint(w,
+	fmt.Fprint(w, //nolint:errcheck
 		d.common.Zone.Mark(
 			i.ID(),
 			st.Base.Render(
 				lipgloss.JoinHorizontal(lipgloss.Top,
 					truncate.String(selector+ref+desc+hash,
-						uint(m.Width()-horizontalFrameSize)),
+						uint(m.Width()-horizontalFrameSize)), //nolint:gosec
 				),
 			),
 		),
diff --git a/pkg/ui/pages/repo/repo.go b/pkg/ui/pages/repo/repo.go
index 1f32632..e9e5535 100644
--- a/pkg/ui/pages/repo/repo.go
+++ b/pkg/ui/pages/repo/repo.go
@@ -4,11 +4,11 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/help"
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/spinner"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/help"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/spinner"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -171,7 +171,7 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 	case tabs.ActiveTabMsg:
 		r.activeTab = int(msg)
-	case tea.KeyMsg, tea.MouseMsg:
+	case tea.KeyPressMsg, tea.MouseClickMsg:
 		t, cmd := r.tabs.Update(msg)
 		r.tabs = t.(*tabs.Tabs)
 		if cmd != nil {
@@ -185,17 +185,14 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 		}
 		switch msg := msg.(type) {
-		case tea.MouseMsg:
-			if msg.Action != tea.MouseActionPress {
-				break
-			}
+		case tea.MouseClickMsg:
 			switch msg.Button {
-			case tea.MouseButtonLeft:
+			case tea.MouseLeft:
 				switch {
 				case r.common.Zone.Get("repo-help").InBounds(msg):
 					cmds = append(cmds, footer.ToggleFooterCmd)
 				}
-			case tea.MouseButtonRight:
+			case tea.MouseRight:
 				switch {
 				case r.common.Zone.Get("repo-main").InBounds(msg):
 					cmds = append(cmds, goBackCmd)
@@ -203,7 +200,7 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 		}
 		switch msg := msg.(type) {
-		case tea.KeyMsg:
+		case tea.KeyPressMsg:
 			switch {
 			case key.Matches(msg, r.common.KeyMap.Back):
 				cmds = append(cmds, goBackCmd)
@@ -212,7 +209,7 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case CopyMsg:
 		txt := msg.Text
 		if cfg := r.common.Config(); cfg != nil {
-			r.common.Output.Copy(txt)
+			cmds = append(cmds, tea.SetClipboard(txt))
 		}
 		r.statusbar.SetStatus("", msg.Message, "", "")
 	case ReadmeMsg:
@@ -274,10 +271,10 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	// Update the status bar on these events
 	// Must come after we've updated the active tab
 	switch msg.(type) {
-	case RepoMsg, RefMsg, tabs.ActiveTabMsg, tea.KeyMsg, tea.MouseMsg,
-		FileItemsMsg, FileContentMsg, FileBlameMsg, selector.ActiveMsg,
-		LogItemsMsg, GoBackMsg, LogDiffMsg, EmptyRepoMsg,
-		StashListMsg, StashPatchMsg:
+	case RepoMsg, RefMsg, tabs.ActiveTabMsg, tea.KeyPressMsg,
+		tea.MouseClickMsg, tea.MouseWheelMsg, FileItemsMsg, FileContentMsg,
+		FileBlameMsg, selector.ActiveMsg, LogItemsMsg, GoBackMsg, LogDiffMsg,
+		EmptyRepoMsg, StashListMsg, StashPatchMsg:
 		r.setStatusBarInfo()
 	}
 
@@ -326,7 +323,7 @@ func (r *Repo) headerView() string {
 	if r.selectedRepo == nil {
 		return ""
 	}
-	truncate := r.common.Renderer.NewStyle().MaxWidth(r.common.Width)
+	truncate := lipgloss.NewStyle().MaxWidth(r.common.Width)
 	header := r.selectedRepo.ProjectName()
 	if header == "" {
 		header = r.selectedRepo.Name()
@@ -340,13 +337,13 @@ func (r *Repo) headerView() string {
 		)
 	}
 	urlStyle := r.common.Styles.URLStyle.
-		Width(r.common.Width - lipgloss.Width(desc) - 1).
+		Width(r.common.Width - lipgloss.Width(header) - 1).
 		Align(lipgloss.Right)
 	var url string
 	if cfg := r.common.Config(); cfg != nil {
 		url = r.common.CloneCmd(cfg.SSH.PublicURL, r.selectedRepo.Name())
 	}
-	url = common.TruncateString(url, r.common.Width-lipgloss.Width(desc)-1)
+	url = common.TruncateString(url, r.common.Width-lipgloss.Width(header)-1)
 	url = r.common.Zone.Mark(
 		fmt.Sprintf("%s-url", r.selectedRepo.Name()),
 		urlStyle.Render(url),
diff --git a/pkg/ui/pages/repo/stash.go b/pkg/ui/pages/repo/stash.go
index 667026b..10b0bbf 100644
--- a/pkg/ui/pages/repo/stash.go
+++ b/pkg/ui/pages/repo/stash.go
@@ -4,10 +4,10 @@ import (
 	"fmt"
 
 	gitm "github.com/aymanbagabas/git-module"
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/spinner"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/spinner"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -128,7 +128,7 @@ func (s *Stash) StatusBarInfo() string {
 		}
 		return fmt.Sprintf("p. %d/%d", s.list.Page()+1, totalPages)
 	case stashStatePatch:
-		return fmt.Sprintf("☰ %d%%", s.code.ScrollPosition())
+		return common.ScrollPercent(s.code.ScrollPosition())
 	default:
 		return ""
 	}
@@ -165,7 +165,7 @@ func (s *Stash) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				cmds = append(cmds, cmd)
 			}
 		}
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch s.state {
 		case stashStateList:
 			switch {
diff --git a/pkg/ui/pages/repo/stashitem.go b/pkg/ui/pages/repo/stashitem.go
index cc2d3f3..a094040 100644
--- a/pkg/ui/pages/repo/stashitem.go
+++ b/pkg/ui/pages/repo/stashitem.go
@@ -5,9 +5,9 @@ import (
 	"io"
 
 	gitm "github.com/aymanbagabas/git-module"
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 )
 
@@ -65,7 +65,7 @@ func (d StashItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 	}
 
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, d.common.KeyMap.Copy):
 			return copyCmd(item.Title(), fmt.Sprintf("Stash message %q copied to clipboard", item.Title()))
@@ -93,7 +93,7 @@ func (d StashItemDelegate) Render(w io.Writer, m list.Model, index int, listItem
 
 	selector = s.Selector.Render(selector)
 	title := st.Render(item.Title())
-	fmt.Fprint(w, d.common.Zone.Mark(
+	fmt.Fprint(w, d.common.Zone.Mark( //nolint:errcheck
 		item.ID(),
 		common.TruncateString(fmt.Sprintf("%s%s",
 			selector,
diff --git a/pkg/ui/pages/selection/item.go b/pkg/ui/pages/selection/item.go
index 4cd12a9..7b5b828 100644
--- a/pkg/ui/pages/selection/item.go
+++ b/pkg/ui/pages/selection/item.go
@@ -7,10 +7,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 	"github.com/dustin/go-humanize"
@@ -135,12 +135,14 @@ func (d *ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 		return nil
 	}
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, d.common.KeyMap.Copy):
 			d.copiedIdx = idx
-			d.common.Output.Copy(item.Command())
-			return m.SetItem(idx, item)
+			return tea.Batch(
+				tea.SetClipboard(item.Command()),
+				m.SetItem(idx, item),
+			)
 		}
 	}
 	return nil
@@ -212,7 +214,7 @@ func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem lis
 	}
 	cmd = common.TruncateString(cmd, m.Width()-styles.Base.GetHorizontalFrameSize())
 	s.WriteString(cmdStyler(cmd))
-	fmt.Fprint(w,
+	fmt.Fprint(w, //nolint:errcheck
 		d.common.Zone.Mark(i.ID(),
 			styles.Base.Render(s.String()),
 		),
diff --git a/pkg/ui/pages/selection/selection.go b/pkg/ui/pages/selection/selection.go
index 7144244..fdabb15 100644
--- a/pkg/ui/pages/selection/selection.go
+++ b/pkg/ui/pages/selection/selection.go
@@ -4,10 +4,10 @@ import (
 	"fmt"
 	"sort"
 
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/list"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/v2/key"
+	"github.com/charmbracelet/bubbles/v2/list"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/soft-serve/pkg/access"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/ui/common"
@@ -51,7 +51,7 @@ func New(c common.Common) *Selection {
 		ts[i] = b.String()
 	}
 	t := tabs.New(c, ts)
-	t.TabSeparator = c.Renderer.NewStyle()
+	t.TabSeparator = lipgloss.NewStyle()
 	t.TabInactive = c.Styles.TopLevelNormalTab
 	t.TabActive = c.Styles.TopLevelActiveTab
 	t.TabDot = c.Styles.TopLevelActiveTabDot
@@ -250,9 +250,9 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if cmd != nil {
 			cmds = append(cmds, cmd)
 		}
-	case tea.KeyMsg, tea.MouseMsg:
+	case tea.KeyPressMsg, tea.MouseMsg:
 		switch msg := msg.(type) {
-		case tea.KeyMsg:
+		case tea.KeyPressMsg:
 			switch {
 			case key.Matches(msg, s.common.KeyMap.Back):
 				cmds = append(cmds, s.selector.Init())
@@ -289,15 +289,15 @@ func (s *Selection) View() string {
 	wm, hm := s.getMargins()
 	switch s.activePane {
 	case selectorPane:
-		ss := s.common.Renderer.NewStyle().
+		ss := lipgloss.NewStyle().
 			Width(s.common.Width - wm).
 			Height(s.common.Height - hm)
 		view = ss.Render(s.selector.View())
 	case readmePane:
-		rs := s.common.Renderer.NewStyle().
+		rs := lipgloss.NewStyle().
 			Height(s.common.Height - hm)
 		status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
-		readmeStatus := s.common.Renderer.NewStyle().
+		readmeStatus := lipgloss.NewStyle().
 			Align(lipgloss.Right).
 			Width(s.common.Width - wm).
 			Foreground(s.common.Styles.InactiveBorderColor).
diff --git a/pkg/ui/styles/styles.go b/pkg/ui/styles/styles.go
index 293b152..46b8cff 100644
--- a/pkg/ui/styles/styles.go
+++ b/pkg/ui/styles/styles.go
@@ -1,7 +1,9 @@
 package styles
 
 import (
-	"github.com/charmbracelet/lipgloss"
+	"image/color"
+
+	"github.com/charmbracelet/lipgloss/v2"
 )
 
 // XXX: For now, this is in its own package so that it can be shared between
@@ -9,8 +11,8 @@ import (
 
 // Styles defines styles for the UI.
 type Styles struct {
-	ActiveBorderColor   lipgloss.Color
-	InactiveBorderColor lipgloss.Color
+	ActiveBorderColor   color.Color
+	InactiveBorderColor color.Color
 
 	App                  lipgloss.Style
 	ServerName           lipgloss.Style
@@ -165,7 +167,7 @@ type Styles struct {
 }
 
 // DefaultStyles returns default styles for the UI.
-func DefaultStyles(r *lipgloss.Renderer) *Styles {
+func DefaultStyles() *Styles {
 	highlightColor := lipgloss.Color("210")
 	highlightColorDim := lipgloss.Color("174")
 	selectorColor := lipgloss.Color("167")
@@ -176,10 +178,10 @@ func DefaultStyles(r *lipgloss.Renderer) *Styles {
 	s.ActiveBorderColor = lipgloss.Color("62")
 	s.InactiveBorderColor = lipgloss.Color("241")
 
-	s.App = r.NewStyle().
+	s.App = lipgloss.NewStyle().
 		Margin(1, 2)
 
-	s.ServerName = r.NewStyle().
+	s.ServerName = lipgloss.NewStyle().
 		Height(1).
 		MarginLeft(1).
 		MarginBottom(1).
@@ -188,29 +190,29 @@ func DefaultStyles(r *lipgloss.Renderer) *Styles {
 		Foreground(lipgloss.Color("229")).
 		Bold(true)
 
-	s.TopLevelNormalTab = r.NewStyle().
+	s.TopLevelNormalTab = lipgloss.NewStyle().
 		MarginRight(2)
 
 	s.TopLevelActiveTab = s.TopLevelNormalTab.
 		Foreground(lipgloss.Color("36"))
 
-	s.TopLevelActiveTabDot = r.NewStyle().
+	s.TopLevelActiveTabDot = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("36"))
 
-	s.RepoSelector.Normal.Base = r.NewStyle().
+	s.RepoSelector.Normal.Base = lipgloss.NewStyle().
 		PaddingLeft(1).
 		Border(lipgloss.Border{Left: " "}, false, false, false, true).
 		Height(3)
 
-	s.RepoSelector.Normal.Title = r.NewStyle().Bold(true)
+	s.RepoSelector.Normal.Title = lipgloss.NewStyle().Bold(true)
 
-	s.RepoSelector.Normal.Desc = r.NewStyle().
+	s.RepoSelector.Normal.Desc = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("243"))
 
-	s.RepoSelector.Normal.Command = r.NewStyle().
+	s.RepoSelector.Normal.Command = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("132"))
 
-	s.RepoSelector.Normal.Updated = r.NewStyle().
+	s.RepoSelector.Normal.Updated = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("243"))
 
 	s.RepoSelector.Active.Base = s.RepoSelector.Normal.Base.
@@ -229,78 +231,78 @@ func DefaultStyles(r *lipgloss.Renderer) *Styles {
 	s.RepoSelector.Active.Command = s.RepoSelector.Normal.Command.
 		Foreground(lipgloss.Color("204"))
 
-	s.MenuItem = r.NewStyle().
+	s.MenuItem = lipgloss.NewStyle().
 		PaddingLeft(1).
 		Border(lipgloss.Border{
 			Left: " ",
 		}, false, false, false, true).
 		Height(3)
 
-	s.MenuLastUpdate = r.NewStyle().
+	s.MenuLastUpdate = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("241")).
 		Align(lipgloss.Right)
 
-	s.Repo.Base = r.NewStyle()
+	s.Repo.Base = lipgloss.NewStyle()
 
-	s.Repo.Title = r.NewStyle().
+	s.Repo.Title = lipgloss.NewStyle().
 		Padding(0, 2)
 
-	s.Repo.Command = r.NewStyle().
+	s.Repo.Command = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("168"))
 
-	s.Repo.Body = r.NewStyle().
+	s.Repo.Body = lipgloss.NewStyle().
 		Margin(1, 0)
 
-	s.Repo.Header = r.NewStyle().
+	s.Repo.Header = lipgloss.NewStyle().
 		MaxHeight(2).
 		Border(lipgloss.NormalBorder(), false, false, true, false).
 		BorderForeground(lipgloss.Color("236"))
 
-	s.Repo.HeaderName = r.NewStyle().
+	s.Repo.HeaderName = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("212")).
 		Bold(true)
 
-	s.Repo.HeaderDesc = r.NewStyle().
+	s.Repo.HeaderDesc = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("243"))
 
-	s.Footer = r.NewStyle().
+	s.Footer = lipgloss.NewStyle().
 		MarginTop(1).
 		Padding(0, 1).
 		Height(1)
 
-	s.Branch = r.NewStyle().
+	s.Branch = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("203")).
 		Background(lipgloss.Color("236")).
 		Padding(0, 1)
 
-	s.HelpKey = r.NewStyle().
+	s.HelpKey = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("241"))
 
-	s.HelpValue = r.NewStyle().
+	s.HelpValue = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("239"))
 
-	s.HelpDivider = r.NewStyle().
+	s.HelpDivider = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("237")).
 		SetString(" • ")
 
-	s.URLStyle = r.NewStyle().
+	s.URLStyle = lipgloss.NewStyle().
 		MarginLeft(1).
 		Foreground(lipgloss.Color("168"))
 
-	s.Error = r.NewStyle().
+	s.Error = lipgloss.NewStyle().
 		MarginTop(2)
 
-	s.ErrorTitle = r.NewStyle().
+	s.ErrorTitle = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("230")).
 		Background(lipgloss.Color("204")).
 		Bold(true).
 		Padding(0, 1)
 
-	s.ErrorBody = r.NewStyle().
+	s.ErrorBody = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("252")).
 		MarginLeft(2)
 
-	s.LogItem.Normal.Base = r.NewStyle().
+	s.LogItem.Normal.Base = lipgloss.NewStyle().
 		Border(lipgloss.Border{
 			Left: " ",
 		}, false, false, false, true).
@@ -315,113 +317,113 @@ func DefaultStyles(r *lipgloss.Renderer) *Styles {
 	s.LogItem.Active.Hash = s.LogItem.Normal.Hash.
 		Foreground(hashColor)
 
-	s.LogItem.Active.Hash = r.NewStyle().
+	s.LogItem.Active.Hash = lipgloss.NewStyle().
 		Bold(true).
 		Foreground(highlightColor)
 
-	s.LogItem.Normal.Title = r.NewStyle().
+	s.LogItem.Normal.Title = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("105"))
 
-	s.LogItem.Active.Title = r.NewStyle().
+	s.LogItem.Active.Title = lipgloss.NewStyle().
 		Foreground(highlightColor).
 		Bold(true)
 
-	s.LogItem.Normal.Desc = r.NewStyle().
+	s.LogItem.Normal.Desc = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("246"))
 
-	s.LogItem.Active.Desc = r.NewStyle().
+	s.LogItem.Active.Desc = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("95"))
 
 	s.LogItem.Active.Keyword = s.LogItem.Active.Desc.
 		Foreground(highlightColorDim)
 
-	s.LogItem.Normal.Hash = r.NewStyle().
+	s.LogItem.Normal.Hash = lipgloss.NewStyle().
 		Foreground(hashColor)
 
-	s.LogItem.Active.Hash = r.NewStyle().
+	s.LogItem.Active.Hash = lipgloss.NewStyle().
 		Foreground(highlightColor)
 
-	s.Log.Commit = r.NewStyle().
+	s.Log.Commit = lipgloss.NewStyle().
 		Margin(0, 2)
 
-	s.Log.CommitHash = r.NewStyle().
+	s.Log.CommitHash = lipgloss.NewStyle().
 		Foreground(hashColor).
 		Bold(true)
 
-	s.Log.CommitBody = r.NewStyle().
+	s.Log.CommitBody = lipgloss.NewStyle().
 		MarginTop(1).
 		MarginLeft(2)
 
-	s.Log.CommitStatsAdd = r.NewStyle().
+	s.Log.CommitStatsAdd = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("42")).
 		Bold(true)
 
-	s.Log.CommitStatsDel = r.NewStyle().
+	s.Log.CommitStatsDel = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("203")).
 		Bold(true)
 
-	s.Log.Paginator = r.NewStyle().
+	s.Log.Paginator = lipgloss.NewStyle().
 		Margin(0).
 		Align(lipgloss.Center)
 
-	s.Ref.Normal.Item = r.NewStyle()
+	s.Ref.Normal.Item = lipgloss.NewStyle()
 
-	s.Ref.ItemSelector = r.NewStyle().
+	s.Ref.ItemSelector = lipgloss.NewStyle().
 		Foreground(selectorColor).
 		SetString("> ")
 
-	s.Ref.Active.Item = r.NewStyle().
+	s.Ref.Active.Item = lipgloss.NewStyle().
 		Foreground(highlightColorDim)
 
-	s.Ref.Normal.Base = r.NewStyle()
+	s.Ref.Normal.Base = lipgloss.NewStyle()
 
-	s.Ref.Active.Base = r.NewStyle()
+	s.Ref.Active.Base = lipgloss.NewStyle()
 
-	s.Ref.Normal.ItemTag = r.NewStyle().
+	s.Ref.Normal.ItemTag = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("39"))
 
-	s.Ref.Active.ItemTag = r.NewStyle().
+	s.Ref.Active.ItemTag = lipgloss.NewStyle().
 		Bold(true).
 		Foreground(highlightColor)
 
-	s.Ref.Active.Item = r.NewStyle().
+	s.Ref.Active.Item = lipgloss.NewStyle().
 		Bold(true).
 		Foreground(highlightColor)
 
-	s.Ref.Normal.ItemDesc = r.NewStyle().
+	s.Ref.Normal.ItemDesc = lipgloss.NewStyle().
 		Faint(true)
 
-	s.Ref.Active.ItemDesc = r.NewStyle().
+	s.Ref.Active.ItemDesc = lipgloss.NewStyle().
 		Foreground(highlightColor).
 		Faint(true)
 
-	s.Ref.Normal.ItemHash = r.NewStyle().
+	s.Ref.Normal.ItemHash = lipgloss.NewStyle().
 		Foreground(hashColor).
 		Bold(true)
 
-	s.Ref.Active.ItemHash = r.NewStyle().
+	s.Ref.Active.ItemHash = lipgloss.NewStyle().
 		Foreground(highlightColor).
 		Bold(true)
 
 	s.Ref.Paginator = s.Log.Paginator
 
-	s.Ref.Selector = r.NewStyle()
+	s.Ref.Selector = lipgloss.NewStyle()
 
 	s.Tree.Selector = s.Tree.Normal.FileName.
 		Width(1).
 		Foreground(selectorColor)
 
-	s.Tree.Normal.FileName = r.NewStyle().
+	s.Tree.Normal.FileName = lipgloss.NewStyle().
 		MarginLeft(1)
 
 	s.Tree.Active.FileName = s.Tree.Normal.FileName.
 		Bold(true).
 		Foreground(highlightColor)
 
-	s.Tree.Normal.FileDir = r.NewStyle().
+	s.Tree.Normal.FileDir = lipgloss.NewStyle().
 		Foreground(lipgloss.Color("39"))
 
-	s.Tree.Active.FileDir = r.NewStyle().
+	s.Tree.Active.FileDir = lipgloss.NewStyle().
 		Foreground(highlightColor)
 
 	s.Tree.Normal.FileMode = s.Tree.Active.FileName.
@@ -437,87 +439,87 @@ func DefaultStyles(r *lipgloss.Renderer) *Styles {
 	s.Tree.Active.FileSize = s.Tree.Normal.FileName.
 		Foreground(highlightColorDim)
 
-	s.Tree.FileContent = r.NewStyle()
+	s.Tree.FileContent = lipgloss.NewStyle()
 
 	s.Tree.Paginator = s.Log.Paginator
 
-	s.Tree.Blame.Hash = r.NewStyle().
+	s.Tree.Blame.Hash = lipgloss.NewStyle().
 		Foreground(hashColor).
 		Bold(true)
 
-	s.Tree.Blame.Message = r.NewStyle()
+	s.Tree.Blame.Message = lipgloss.NewStyle()
 
-	s.Tree.Blame.Who = r.NewStyle().
+	s.Tree.Blame.Who = lipgloss.NewStyle().
 		Faint(true)
 
-	s.Spinner = r.NewStyle().
+	s.Spinner = lipgloss.NewStyle().
 		MarginTop(1).
 		MarginLeft(2).
 		Foreground(lipgloss.Color("205"))
 
-	s.SpinnerContainer = r.NewStyle()
+	s.SpinnerContainer = lipgloss.NewStyle()
 
-	s.NoContent = r.NewStyle().
+	s.NoContent = lipgloss.NewStyle().
 		MarginTop(1).
 		MarginLeft(2).
 		Foreground(lipgloss.Color("242"))
 
-	s.StatusBar = r.NewStyle().
+	s.StatusBar = lipgloss.NewStyle().
 		Height(1)
 
-	s.StatusBarKey = r.NewStyle().
+	s.StatusBarKey = lipgloss.NewStyle().
 		Bold(true).
 		Padding(0, 1).
 		Background(lipgloss.Color("206")).
 		Foreground(lipgloss.Color("228"))
 
-	s.StatusBarValue = r.NewStyle().
+	s.StatusBarValue = lipgloss.NewStyle().
 		Padding(0, 1).
 		Background(lipgloss.Color("235")).
 		Foreground(lipgloss.Color("243"))
 
-	s.StatusBarInfo = r.NewStyle().
+	s.StatusBarInfo = lipgloss.NewStyle().
 		Padding(0, 1).
 		Background(lipgloss.Color("212")).
 		Foreground(lipgloss.Color("230"))
 
-	s.StatusBarBranch = r.NewStyle().
+	s.StatusBarBranch = lipgloss.NewStyle().
 		Padding(0, 1).
 		Background(lipgloss.Color("62")).
 		Foreground(lipgloss.Color("230"))
 
-	s.StatusBarHelp = r.NewStyle().
+	s.StatusBarHelp = lipgloss.NewStyle().
 		Padding(0, 1).
 		Background(lipgloss.Color("237")).
 		Foreground(lipgloss.Color("243"))
 
-	s.Tabs = r.NewStyle().
+	s.Tabs = lipgloss.NewStyle().
 		Height(1)
 
-	s.TabInactive = r.NewStyle()
+	s.TabInactive = lipgloss.NewStyle()
 
-	s.TabActive = r.NewStyle().
+	s.TabActive = lipgloss.NewStyle().
 		Underline(true).
 		Foreground(lipgloss.Color("36"))
 
-	s.TabSeparator = r.NewStyle().
+	s.TabSeparator = lipgloss.NewStyle().
 		SetString("│").
 		Padding(0, 1).
 		Foreground(lipgloss.Color("238"))
 
-	s.Code.LineDigit = r.NewStyle().Foreground(lipgloss.Color("239"))
+	s.Code.LineDigit = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
 
-	s.Code.LineBar = r.NewStyle().Foreground(lipgloss.Color("236"))
+	s.Code.LineBar = lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
 
-	s.Stash.Normal.Message = r.NewStyle().MarginLeft(1)
+	s.Stash.Normal.Message = lipgloss.NewStyle().MarginLeft(1)
 
 	s.Stash.Active.Message = s.Stash.Normal.Message.Foreground(selectorColor)
 
-	s.Stash.Title = r.NewStyle().
+	s.Stash.Title = lipgloss.NewStyle().
 		Foreground(hashColor).
 		Bold(true)
 
-	s.Stash.Selector = r.NewStyle().
+	s.Stash.Selector = lipgloss.NewStyle().
 		Width(1).
 		Foreground(selectorColor)
 
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index a98cb13..2fb66e5 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -5,16 +5,27 @@ import (
 	"path"
 	"strings"
 	"unicode"
+
+	"github.com/charmbracelet/x/ansi"
 )
 
 // SanitizeRepo returns a sanitized version of the given repository name.
 func SanitizeRepo(repo string) string {
+	repo = Sanitize(repo)
+	// We need to use an absolute path for the path to be cleaned correctly.
 	repo = strings.TrimPrefix(repo, "/")
+	repo = "/" + repo
+
 	// We're using path instead of filepath here because this is not OS dependent
 	// looking at you Windows
 	repo = path.Clean(repo)
 	repo = strings.TrimSuffix(repo, ".git")
-	return repo
+	return repo[1:]
+}
+
+// Sanitize strips ANSI escape codes from the given string.
+func Sanitize(s string) string {
+	return ansi.Strip(s)
 }
 
 // ValidateUsername returns an error if any of the given usernames are invalid.
diff --git a/pkg/web/auth.go b/pkg/web/auth.go
index 96b2bab..0d8f6d4 100644
--- a/pkg/web/auth.go
+++ b/pkg/web/auth.go
@@ -7,7 +7,7 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
diff --git a/pkg/web/context.go b/pkg/web/context.go
index ccba927..725e80b 100644
--- a/pkg/web/context.go
+++ b/pkg/web/context.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"net/http"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
diff --git a/pkg/web/git.go b/pkg/web/git.go
index 1980f73..b01f3aa 100644
--- a/pkg/web/git.go
+++ b/pkg/web/git.go
@@ -14,7 +14,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	gitb "github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/pkg/access"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
@@ -124,7 +124,7 @@ var gitRoutes = []GitRoute{
 	{
 		method:  []string{http.MethodPost},
 		handler: serviceRpc,
-		path:    "/{service:(?:git-upload-pack|git-receive-pack)$}",
+		path:    "/{service:(?:git-upload-archive|git-upload-pack|git-receive-pack)$}",
 	},
 	{
 		method:  []string{http.MethodGet},
@@ -274,7 +274,7 @@ func withAccess(next http.Handler) http.HandlerFunc {
 			}
 
 			fallthrough
-		case service == git.UploadPackService:
+		case service == git.UploadPackService || service == git.UploadArchiveService:
 			if repo == nil {
 				// If the repo doesn't exist, return 404
 				renderNotFound(w, r)
@@ -394,7 +394,11 @@ func serviceRpc(w http.ResponseWriter, r *http.Request) {
 	cmd := git.ServiceCommand{
 		Stdout: &stdout,
 		Dir:    dir,
-		Args:   []string{"--stateless-rpc"},
+	}
+
+	switch service {
+	case git.UploadPackService, git.ReceivePackService:
+		cmd.Args = append(cmd.Args, "--stateless-rpc")
 	}
 
 	user := proto.UserFromContext(ctx)
diff --git a/pkg/web/git_lfs.go b/pkg/web/git_lfs.go
index dccd87f..9149d4f 100644
--- a/pkg/web/git_lfs.go
+++ b/pkg/web/git_lfs.go
@@ -13,7 +13,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/access"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
diff --git a/pkg/web/goget.go b/pkg/web/goget.go
index 3c9c497..b6678ff 100644
--- a/pkg/web/goget.go
+++ b/pkg/web/goget.go
@@ -6,7 +6,7 @@ import (
 	"path"
 	"text/template"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/utils"
diff --git a/pkg/web/health.go b/pkg/web/health.go
new file mode 100644
index 0000000..850e08b
--- /dev/null
+++ b/pkg/web/health.go
@@ -0,0 +1,34 @@
+package web
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/charmbracelet/log/v2"
+	"github.com/charmbracelet/soft-serve/pkg/db"
+	"github.com/gorilla/mux"
+)
+
+// HealthController registers the health check routes for the web server.
+func HealthController(_ context.Context, r *mux.Router) {
+	r.HandleFunc("/livez", getLiveness)
+	r.HandleFunc("/readyz", getReadiness)
+}
+
+func getLiveness(w http.ResponseWriter, _ *http.Request) {
+	renderStatus(http.StatusOK)(w, nil)
+}
+
+func getReadiness(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	logger := log.FromContext(ctx)
+	db := db.FromContext(ctx)
+
+	if err := db.PingContext(ctx); err != nil {
+		logger.Error("error getting db readiness", "err", err)
+		renderStatus(http.StatusServiceUnavailable)(w, nil)
+		return
+	}
+
+	renderStatus(http.StatusOK)(w, nil)
+}
diff --git a/pkg/web/http.go b/pkg/web/http.go
index 20fae6b..0cdc57b 100644
--- a/pkg/web/http.go
+++ b/pkg/web/http.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 )
 
diff --git a/pkg/web/logging.go b/pkg/web/logging.go
index 956ad9e..b9a264f 100644
--- a/pkg/web/logging.go
+++ b/pkg/web/logging.go
@@ -7,7 +7,7 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/dustin/go-humanize"
 )
 
@@ -81,7 +81,7 @@ func NewLoggingMiddleware(next http.Handler, logger *log.Logger) http.Handler {
 		elapsed := time.Since(start)
 		logger.Debug("response",
 			"status", fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)),
-			"bytes", humanize.Bytes(uint64(writer.bytes)),
+			"bytes", humanize.Bytes(uint64(writer.bytes)), //nolint:gosec
 			"time", elapsed)
 	})
 }
diff --git a/pkg/web/server.go b/pkg/web/server.go
index 74a04f5..9e7be2f 100644
--- a/pkg/web/server.go
+++ b/pkg/web/server.go
@@ -4,7 +4,8 @@ import (
 	"context"
 	"net/http"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
+	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/gorilla/handlers"
 	"github.com/gorilla/mux"
 )
@@ -14,6 +15,9 @@ func NewRouter(ctx context.Context) http.Handler {
 	logger := log.FromContext(ctx).WithPrefix("http")
 	router := mux.NewRouter()
 
+	// Health routes
+	HealthController(ctx, router)
+
 	// Git routes
 	GitController(ctx, router)
 
@@ -26,5 +30,12 @@ func NewRouter(ctx context.Context) http.Handler {
 	h = handlers.CompressHandler(h)
 	h = handlers.RecoveryHandler()(h)
 
+	cfg := config.FromContext(ctx)
+
+	h = handlers.CORS(handlers.AllowedHeaders(cfg.HTTP.CORS.AllowedHeaders),
+		handlers.AllowedOrigins(cfg.HTTP.CORS.AllowedOrigins),
+		handlers.AllowedMethods(cfg.HTTP.CORS.AllowedMethods),
+	)(h)
+
 	return h
 }
diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go
index 8bd28e9..15d93bb 100644
--- a/pkg/webhook/webhook.go
+++ b/pkg/webhook/webhook.go
@@ -59,7 +59,7 @@ func SendWebhook(ctx context.Context, w models.Webhook, event Event, payload int
 	dbx := db.FromContext(ctx)
 	datastore := store.FromContext(ctx)
 
-	contentType := ContentType(w.ContentType)
+	contentType := ContentType(w.ContentType) //nolint:gosec
 	switch contentType {
 	case ContentTypeJSON:
 		if err := json.NewEncoder(&buf).Encode(payload); err != nil {



Test patch:
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 8b84ed8..27c033c 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -79,3 +79,48 @@ func TestCustomConfigLocation(t *testing.T) {
 	cfg = DefaultConfig()
 	is.Equal(cfg.Name, "Soft Serve")
 }
+
+func TestParseMultipleHeaders(t *testing.T) {
+	is := is.New(t)
+	is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS", "Accept,Accept-Language,User-Agent"))
+	t.Cleanup(func() {
+		is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS"))
+	})
+	cfg := DefaultConfig()
+	is.NoErr(cfg.ParseEnv())
+	is.Equal(cfg.HTTP.CORS.AllowedHeaders, []string{
+		"Accept",
+		"Accept-Language",
+		"User-Agent",
+	})
+}
+
+func TestParseMultipleOrigins(t *testing.T) {
+	is := is.New(t)
+	is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS", "http://example.com,https://example.com"))
+	t.Cleanup(func() {
+		is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS"))
+	})
+	cfg := DefaultConfig()
+	is.NoErr(cfg.ParseEnv())
+	is.Equal(cfg.HTTP.CORS.AllowedOrigins, []string{
+		"http://localhost:23232",
+		"http://example.com",
+		"https://example.com",
+	})
+}
+
+func TestParseMultipleMethods(t *testing.T) {
+	is := is.New(t)
+	is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS", "GET,POST,PUT"))
+	t.Cleanup(func() {
+		is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS"))
+	})
+	cfg := DefaultConfig()
+	is.NoErr(cfg.ParseEnv())
+	is.Equal(cfg.HTTP.CORS.AllowedMethods, []string{
+		"GET",
+		"POST",
+		"PUT",
+	})
+}
diff --git a/pkg/cron/cron_test.go b/pkg/cron/cron_test.go
index c254191..b8151db 100644
--- a/pkg/cron/cron_test.go
+++ b/pkg/cron/cron_test.go
@@ -6,7 +6,7 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 )
 
 func TestCronLogger(t *testing.T) {
diff --git a/pkg/daemon/daemon_test.go b/pkg/daemon/daemon_test.go
index b9cb77c..8326e30 100644
--- a/pkg/daemon/daemon_test.go
+++ b/pkg/daemon/daemon_test.go
@@ -58,11 +58,7 @@ func TestMain(m *testing.M) {
 		log.Fatal(err)
 	}
 	testDaemon = d
-	go func() {
-		if err := d.ListenAndServe(); err != ErrServerClosed {
-			log.Fatal(err)
-		}
-	}()
+	go d.ListenAndServe() //nolint:errcheck
 	code := m.Run()
 	os.Unsetenv("SOFT_SERVE_DATA_PATH")
 	os.Unsetenv("SOFT_SERVE_GIT_MAX_CONNECTIONS")
@@ -81,7 +77,7 @@ func TestIdleTimeout(t *testing.T) {
 	for {
 		c, err = net.Dial("tcp", testDaemon.addr)
 		if err != nil && tries >= 3 {
-			t.Fatal(err)
+			t.Fatalf("failed to connect to daemon after %d tries: %v", tries, err)
 		}
 		tries++
 		if testDaemon.conns.Size() != 0 {
@@ -99,7 +95,7 @@ func TestIdleTimeout(t *testing.T) {
 func TestInvalidRepo(t *testing.T) {
 	c, err := net.Dial("tcp", testDaemon.addr)
 	if err != nil {
-		t.Fatal(err)
+		t.Fatalf("failed to connect to daemon: %v", err)
 	}
 	if err := pktline.NewEncoder(c).EncodeString("git-upload-pack /test.git\x00"); err != nil {
 		t.Fatalf("expected nil, got error: %v", err)
diff --git a/pkg/ssh/session_test.go b/pkg/ssh/session_test.go
index 792759c..f831327 100644
--- a/pkg/ssh/session_test.go
+++ b/pkg/ssh/session_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/pkg/backend"
 	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/db"
@@ -17,10 +17,9 @@ import (
 	"github.com/charmbracelet/soft-serve/pkg/store/database"
 	"github.com/charmbracelet/soft-serve/pkg/test"
 	"github.com/charmbracelet/ssh"
-	bm "github.com/charmbracelet/wish/bubbletea"
-	"github.com/charmbracelet/wish/testsession"
+	bm "github.com/charmbracelet/wish/v2/bubbletea"
+	"github.com/charmbracelet/wish/v2/testsession"
 	"github.com/matryer/is"
-	"github.com/muesli/termenv"
 	gossh "golang.org/x/crypto/ssh"
 	_ "modernc.org/sqlite" // sqlite driver
 )
@@ -79,7 +78,7 @@ func setup(tb testing.TB) (*gossh.Session, func() error) {
 	be := backend.New(ctx, cfg, dbx, dbstore)
 	ctx = backend.WithContext(ctx, be)
 	return testsession.New(tb, &ssh.Server{
-		Handler: ContextMiddleware(cfg, dbx, dbstore, be, log.Default())(bm.MiddlewareWithProgramHandler(SessionHandler, termenv.ANSI256)(func(s ssh.Session) {
+		Handler: ContextMiddleware(cfg, dbx, dbstore, be, log.Default())(bm.MiddlewareWithProgramHandler(SessionHandler)(func(s ssh.Session) {
 			_, _, active := s.Pty()
 			if !active {
 				os.Exit(1)
diff --git a/testscript/script_test.go b/testscript/script_test.go
index 6162caa..db1593f 100644
--- a/testscript/script_test.go
+++ b/testscript/script_test.go
@@ -274,7 +274,6 @@ func cmdUI(key ssh.Signer) func(ts *testscript.TestScript, neg bool, args []stri
 	}
 }
 
-// P.S. Windows sucks!
 func cmdDos2Unix(ts *testscript.TestScript, neg bool, args []string) {
 	if neg {
 		ts.Fatalf("unsupported: ! dos2unix")
diff --git a/testscript/testdata/http-cors.txtar b/testscript/testdata/http-cors.txtar
new file mode 100644
index 0000000..84c50e3
--- /dev/null
+++ b/testscript/testdata/http-cors.txtar
@@ -0,0 +1,81 @@
+# vi: set ft=conf
+
+# FIXME: don't skip windows
+[windows] skip 'curl makes github actions hang'
+
+# convert crlf to lf on windows
+[windows] dos2unix http1.txt http2.txt http3.txt goget.txt gitclone.txt
+
+# start soft serve
+exec soft serve &
+# wait for SSH server to start
+ensureserverrunning SSH_PORT
+
+# create user
+soft user create user1 --key "$USER1_AUTHORIZED_KEY"
+
+# create access token
+soft token create --expires-in '1h' 'repo2'
+cp stdout tokenfile
+envfile TOKEN=tokenfile
+soft token create --expires-in '1ns' 'repo2'
+cp stdout etokenfile
+envfile ETOKEN=etokenfile
+usoft token create 'repo2'
+cp stdout utokenfile
+envfile UTOKEN=utokenfile
+
+# push & create repo with some files, commits, tags...
+mkdir ./repo2
+git -c init.defaultBranch=master -C repo2 init
+mkfile ./repo2/README.md '# Project\nfoo'
+mkfile ./repo2/foo.png 'foo'
+mkfile ./repo2/bar.png 'bar'
+git -C repo2 remote add origin http://$TOKEN@localhost:$HTTP_PORT/repo2
+git -C repo2 lfs install --local
+git -C repo2 lfs track '*.png'
+git -C repo2 add -A
+git -C repo2 commit -m 'first'
+git -C repo2 tag v0.1.0
+git -C repo2 push origin HEAD
+git -C repo2 push origin HEAD --tags
+
+-- test 1 --
+# default public url is always allowed
+curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2/git-upload-pack -H 'Origin: http://localhost:23232' -H 'Access-Control-Request-Method: POST'
+stderr '.*200 OK.*'
+
+# stop the server
+stopserver
+
+-- test 2 --
+# by default the server does not allow example.com, so the response does not have the "Access-Control-Allow-Origin" header and cors will fail.
+
+# restart soft serve
+exec soft serve &
+# wait for SSH server to start
+ensureserverrunning SSH_PORT
+
+curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2/git-upload-pack -H 'Origin: https://example.com' -H 'Access-Control-Request-Method: POST'
+! stderr '.*Access-Control-Allow-Origin.*'
+
+# stop the server
+stopserver
+
+-- test 3 --
+# allow cross-origin OPTIONS requests for example.com
+env SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS="https://example.com"
+env SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS="GET,OPTIONS"
+env SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS="Origin,Access-Control-Request-Method"
+
+# restart soft serve
+exec soft serve &
+# wait for SSH server to start
+ensureserverrunning SSH_PORT
+
+curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://example.com' -H 'Access-Control-Request-Method: GET'
+stderr '.*200 OK.*'
+
+# stop the server
+[windows] stopserver
+[windows] ! stderr .
diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar
index 76feb78..fded61f 100644
--- a/testscript/testdata/mirror.txtar
+++ b/testscript/testdata/mirror.txtar
@@ -9,41 +9,41 @@ exec soft serve &
 ensureserverrunning SSH_PORT
 
 # import a repo
-soft repo import --mirror charmbracelet/catwalk https://github.com/charmbracelet/catwalk.git
+soft repo import --mirror charmbracelet/wizard-tutorial https://github.com/charmbracelet/wizard-tutorial.git
 
 # check empty description file
-readfile $DATA_PATH/repos/charmbracelet/catwalk.git/description ''
+readfile $DATA_PATH/repos/charmbracelet/wizard-tutorial.git/description ''
 
 # check repo info
-soft repo info charmbracelet/catwalk
+soft repo info charmbracelet/wizard-tutorial
 cmp stdout info1.txt
 
 # check repo list
 soft repo list
-stdout charmbracelet/catwalk
+stdout charmbracelet/wizard-tutorial
 
 # is-mirror?
-soft repo is-mirror charmbracelet/catwalk
+soft repo is-mirror charmbracelet/wizard-tutorial
 stdout true
 
 # set project name
-soft repo project-name charmbracelet/catwalk catwalk
+soft repo project-name charmbracelet/wizard-tutorial wizard-tutorial
 soft repo list
-stdout catwalk
+stdout wizard-tutorial
 
 
 # check description
-soft repo description charmbracelet/catwalk
+soft repo description charmbracelet/wizard-tutorial
 ! stdout .
 
 # set description
-soft repo description charmbracelet/catwalk "testing repo"
-soft repo description charmbracelet/catwalk
+soft repo description charmbracelet/wizard-tutorial "testing repo"
+soft repo description charmbracelet/wizard-tutorial
 stdout 'testing repo'
-readfile $DATA_PATH/repos/charmbracelet/catwalk.git/description 'testing repo'
+readfile $DATA_PATH/repos/charmbracelet/wizard-tutorial.git/description 'testing repo'
 
 # rename
-soft repo rename charmbracelet/catwalk charmbracelet/test
+soft repo rename charmbracelet/wizard-tutorial charmbracelet/test
 soft repo list
 stdout charmbracelet/test # TODO: shouldn't this still show the project-name?
 
@@ -76,8 +76,8 @@ soft repo info charmbracelet/test
 cmp stdout info2.txt
 
 # get a file
-soft repo blob charmbracelet/test LICENSE
-stdout '.*Creative Commons.*'
+soft repo blob charmbracelet/test README.md
+stdout '.*Wizard.*'
 
 # stop the server
 [windows] stopserver
@@ -86,7 +86,7 @@ stdout '.*Creative Commons.*'
 
 -- info1.txt --
 Project Name:
-Repository: charmbracelet/catwalk
+Repository: charmbracelet/wizard-tutorial
 Description:
 Private: false
 Hidden: false
@@ -96,7 +96,7 @@ Default Branch: main
 Branches:
   - main
 -- info2.txt --
-Project Name: catwalk
+Project Name: wizard-tutorial
 Repository: charmbracelet/test
 Description: testing repo
 Private: true
@@ -107,7 +107,9 @@ Default Branch: main
 Branches:
   - main
 -- tree.txt --
-drwxrwxrwx	-	 30k
-drwxrwxrwx	-	 50k
--rw-r--r--	19 kB	 LICENSE
--rw-r--r--	1.1 kB	 README.md
+-rw-r--r--	10 B	 .gitignore
+-rw-r--r--	1.3 kB	 README.md
+-rw-r--r--	970 B	 go.mod
+-rw-r--r--	5.3 kB	 go.sum
+-rw-r--r--	2.2 kB	 input.go
+-rw-r--r--	2.9 kB	 main.go
diff --git a/testscript/testdata/repo-collab.txtar b/testscript/testdata/repo-collab.txtar
index ead5fdc..9a1f75e 100644
--- a/testscript/testdata/repo-collab.txtar
+++ b/testscript/testdata/repo-collab.txtar
@@ -6,7 +6,7 @@ exec soft serve &
 ensureserverrunning SSH_PORT
 
 # setup
-soft repo import test https://github.com/charmbracelet/catwalk.git
+soft repo import test https://github.com/charmbracelet/wizard-tutorial.git
 soft user create foo --key "$USER1_AUTHORIZED_KEY"
 
 # list collabs
diff --git a/testscript/testdata/repo-import.txtar b/testscript/testdata/repo-import.txtar
index e9bfa31..c90a3c5 100644
--- a/testscript/testdata/repo-import.txtar
+++ b/testscript/testdata/repo-import.txtar
@@ -9,17 +9,17 @@ exec soft serve &
 ensureserverrunning SSH_PORT
 
 # import private
-soft repo import --private repo1 https://github.com/charmbracelet/catwalk.git
+soft repo import --private repo1 https://github.com/charmbracelet/wizard-tutorial.git
 soft repo private repo1
 stdout 'true'
 
 # import hidden
-soft repo import --hidden repo2 https://github.com/charmbracelet/catwalk.git
+soft repo import --hidden repo2 https://github.com/charmbracelet/wizard-tutorial.git
 soft repo hidden repo2
 stdout 'true'
 
 # import with name and description
-soft repo import --name 'repo33' --description 'descriptive' repo3 https://github.com/charmbracelet/catwalk.git
+soft repo import --name 'repo33' --description 'descriptive' repo3 https://github.com/charmbracelet/wizard-tutorial.git
 soft repo info repo3
 cmp stdout repo3.txt
 
diff --git a/testscript/testdata/repo-push.txtar b/testscript/testdata/repo-push.txtar
index d9f9475..c4a5e24 100644
--- a/testscript/testdata/repo-push.txtar
+++ b/testscript/testdata/repo-push.txtar
@@ -11,8 +11,14 @@ soft repo create repo-empty -d 'description' -H -p -n 'repo-empty'
 # clone repo
 git clone ssh://localhost:$SSH_PORT/repo-empty repo-empty
 
-# push repo
+# push repo without any commits
 ! git -C repo-empty push origin HEAD
 
+# push repo with a commit
+mkfile ./repo-empty/README.md '# Hello\n\nwelcome'
+git -C repo-empty add README.md
+git -C repo-empty commit -m 'first'
+git -C repo-empty push origin HEAD
+
 # stop the server
 [windows] stopserver
diff --git a/testscript/testdata/soft-browse.txtar b/testscript/testdata/soft-browse.txtar
index 7514f77..9094717 100644
--- a/testscript/testdata/soft-browse.txtar
+++ b/testscript/testdata/soft-browse.txtar
@@ -3,16 +3,16 @@
 [windows] skip
 
 # clone repo
-#git clone https://github.com/charmbracelet/catwalk.git catwalk
+#git clone https://github.com/charmbracelet/wizard-tutorial.git wizard-tutorial
 
 # run soft browse
 # disable this temporarily
 #ttyin input.txt
-#exec soft browse ./catwalk
+#exec soft browse ./wizard-tutorial
 
 # cd and run soft
 # disable this temporarily
-#cd catwalk
+#cd wizard-tutorial
 #ttyin ../input.txt
 #exec soft
 
@@ -28,4 +28,3 @@ jjkkdduu
 jjkkdduu
 
 qqq
-
diff --git a/testscript/testdata/token.txtar b/testscript/testdata/token.txtar
index 7a88701..87aa1fd 100644
--- a/testscript/testdata/token.txtar
+++ b/testscript/testdata/token.txtar
@@ -22,9 +22,9 @@ stderr 'Access token created'
 # list tokens
 usoft token list
 cp stdout tokens.txt
-grep '1\s+test1.*-' tokens.txt
-grep '2\s+test2.*1 year from now' tokens.txt
-grep '3\s+test3.*expired' tokens.txt
+grep '1.*test1.*-' tokens.txt
+grep '2.*test2.*1 year from now' tokens.txt
+grep '3.*est3.*expired' tokens.txt
 
 # delete token
 usoft token delete 1