feat: add soft-serve middleware commands

Ayman Bagabas created

* list files
* cat files
* reload config
* git command

Change summary

go.mod                    |   3 
go.sum                    |   8 +
server/cmd/cat.go         | 123 ++++++++++++++++++++++++
server/cmd/cmd.go         |  70 ++++++++++++++
server/cmd/git.go         |  54 ++++++++++
server/cmd/list.go        |  81 ++++++++++++++++
server/cmd/reload.go      |  23 ++++
server/middleware.go      | 202 ++++++----------------------------------
server/middleware_test.go |  16 ++
server/server.go          |   2 
10 files changed, 408 insertions(+), 174 deletions(-)

Detailed changes

go.mod 🔗

@@ -29,6 +29,7 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
 	github.com/muesli/mango v0.1.0
 	github.com/muesli/roff v0.1.0
+	github.com/spf13/cobra v1.4.0
 )
 
 require (
@@ -45,6 +46,7 @@ require (
 	github.com/go-git/gcfg v1.5.0 // indirect
 	github.com/gorilla/css v1.0.0 // indirect
 	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/kevinburke/ssh_config v1.1.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
@@ -57,6 +59,7 @@ require (
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/sahilm/fuzzy v0.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/xanzy/ssh-agent v0.3.1 // indirect
 	github.com/yuin/goldmark v1.4.4 // indirect
 	github.com/yuin/goldmark-emoji v1.0.1 // indirect

go.sum 🔗

@@ -40,6 +40,7 @@ github.com/charmbracelet/wish v0.3.1-0.20220405152319-bea68c3da3b1/go.mod h1:+Eg
 github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -76,6 +77,8 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
 github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
@@ -135,12 +138,17 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
 github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
+github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

server/cmd/cat.go 🔗

@@ -0,0 +1,123 @@
+package cmd
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/alecthomas/chroma/lexers"
+	gansi "github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/soft-serve/internal/git"
+	"github.com/charmbracelet/soft-serve/tui/common"
+	gitwish "github.com/charmbracelet/wish/git"
+	"github.com/muesli/termenv"
+	"github.com/spf13/cobra"
+)
+
+var (
+	lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
+	lineBarStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
+	dirnameStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF"))
+	filenameStyle  = lipgloss.NewStyle()
+	filemodeStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
+)
+
+// CatCommand returns a command that prints the contents of a file.
+func CatCommand() *cobra.Command {
+	var linenumber bool
+	var color bool
+
+	catCmd := &cobra.Command{
+		Use:   "cat PATH",
+		Short: "Outputs the contents of the file at path.",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ac, s := fromContext(cmd)
+			ps := strings.Split(args[0], "/")
+			rn := ps[0]
+			fp := strings.Join(ps[1:], "/")
+			auth := ac.AuthRepo(rn, s.PublicKey())
+			if auth < gitwish.ReadOnlyAccess {
+				return ErrUnauthorized
+			}
+			var repo *git.Repo
+			repoExists := false
+			for _, rp := range ac.Source.AllRepos() {
+				if rp.Name() == rn {
+					repoExists = true
+					repo = rp
+					break
+				}
+			}
+			if !repoExists {
+				return ErrRepoNotFound
+			}
+			c, _, err := repo.LatestFile(fp)
+			if err != nil {
+				return err
+			}
+			if color {
+				c, err = withFormatting(fp, c)
+				if err != nil {
+					return err
+				}
+			}
+			if linenumber {
+				c = withLineNumber(c, color)
+			}
+			fmt.Fprint(s, c)
+			return nil
+		},
+	}
+	catCmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
+	catCmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
+
+	return catCmd
+}
+
+func withLineNumber(s string, color bool) string {
+	lines := strings.Split(s, "\n")
+	// NB: len() is not a particularly safe way to count string width (because
+	// it's counting bytes instead of runes) but in this case it's okay
+	// because we're only dealing with digits, which are one byte each.
+	mll := len(fmt.Sprintf("%d", len(lines)))
+	for i, l := range lines {
+		digit := fmt.Sprintf("%*d", mll, i+1)
+		bar := "│"
+		if color {
+			digit = lineDigitStyle.Render(digit)
+			bar = lineBarStyle.Render(bar)
+		}
+		if i < len(lines)-1 || len(l) != 0 {
+			// If the final line was a newline we'll get an empty string for
+			// the final line, so drop the newline altogether.
+			lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
+		}
+	}
+	return strings.Join(lines, "\n")
+}
+
+func withFormatting(p, c string) (string, error) {
+	zero := uint(0)
+	lang := ""
+	lexer := lexers.Match(p)
+	if lexer != nil && lexer.Config() != nil {
+		lang = lexer.Config().Name
+	}
+	formatter := &gansi.CodeBlockElement{
+		Code:     c,
+		Language: lang,
+	}
+	r := strings.Builder{}
+	styles := common.DefaultStyles()
+	styles.CodeBlock.Margin = &zero
+	rctx := gansi.NewRenderContext(gansi.Options{
+		Styles:       styles,
+		ColorProfile: termenv.TrueColor,
+	})
+	err := formatter.Render(&r, rctx)
+	if err != nil {
+		return "", err
+	}
+	return r.String(), nil
+}

server/cmd/cmd.go 🔗

@@ -0,0 +1,70 @@
+package cmd
+
+import (
+	"fmt"
+
+	appCfg "github.com/charmbracelet/soft-serve/internal/config"
+	"github.com/gliderlabs/ssh"
+	"github.com/spf13/cobra"
+)
+
+var (
+	// ErrUnauthorized is returned when the user is not authorized to perform action.
+	ErrUnauthorized = fmt.Errorf("Unauthorized")
+	// ErrRepoNotFound is returned when the repo is not found.
+	ErrRepoNotFound = fmt.Errorf("Repository not found")
+	// ErrFileNotFound is returned when the file is not found.
+	ErrFileNotFound = fmt.Errorf("File not found")
+
+	usageTemplate = `Usage:{{if .Runnable}}
+  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
+  {{.UseLine}} [command]{{end}}{{if gt (len .Aliases) 0}}
+
+Aliases:
+  {{.NameAndAliases}}{{end}}{{if .HasExample}}
+
+Examples:
+{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
+
+Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
+  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
+
+Flags:
+{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
+
+Global Flags:
+{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
+
+Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
+  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
+
+Use "{{.UseLine}} [command] --help" for more information about a command.{{end}}
+`
+)
+
+// RootCommand is the root command for the server.
+func RootCommand() *cobra.Command {
+	rootCmd := &cobra.Command{
+		Use:                   "ssh [-p PORT] HOST",
+		Long:                  "Soft Serve is a self-hostable Git server for the command line.",
+		Args:                  cobra.MinimumNArgs(1),
+		DisableFlagsInUseLine: true,
+	}
+	rootCmd.SetUsageTemplate(usageTemplate)
+	rootCmd.CompletionOptions.DisableDefaultCmd = true
+	rootCmd.AddCommand(
+		ReloadCommand(),
+		CatCommand(),
+		ListCommand(),
+		GitCommand(),
+	)
+
+	return rootCmd
+}
+
+func fromContext(cmd *cobra.Command) (*appCfg.Config, ssh.Session) {
+	ctx := cmd.Context()
+	ac := ctx.Value("config").(*appCfg.Config)
+	s := ctx.Value("session").(ssh.Session)
+	return ac, s
+}

server/cmd/git.go 🔗

@@ -0,0 +1,54 @@
+package cmd
+
+import (
+	"io"
+	"os/exec"
+
+	"github.com/charmbracelet/soft-serve/internal/git"
+	gitwish "github.com/charmbracelet/wish/git"
+	"github.com/spf13/cobra"
+)
+
+// GitCommand returns a command that handles Git operations.
+func GitCommand() *cobra.Command {
+	gitCmd := &cobra.Command{
+		Use:   "git REPO COMMAND",
+		Short: "Perform Git operations on a repository.",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ac, s := fromContext(cmd)
+			auth := ac.AuthRepo("config", s.PublicKey())
+			if auth < gitwish.AdminAccess {
+				return ErrUnauthorized
+			}
+			if len(args) < 1 {
+				return runGit(nil, s, s, "")
+			}
+			var repo *git.Repo
+			rn := args[0]
+			repoExists := false
+			for _, rp := range ac.Source.AllRepos() {
+				if rp.Name() == rn {
+					repoExists = true
+					repo = rp
+					break
+				}
+			}
+			if !repoExists {
+				return ErrRepoNotFound
+			}
+			return runGit(nil, s, s, repo.Path(), args[1:]...)
+		},
+	}
+	gitCmd.Flags().SetInterspersed(false)
+
+	return gitCmd
+}
+
+func runGit(in io.Reader, out, err io.Writer, dir string, args ...string) error {
+	cmd := exec.Command("git", args...)
+	cmd.Stdin = in
+	cmd.Stdout = out
+	cmd.Stderr = err
+	cmd.Dir = dir
+	return cmd.Run()
+}

server/cmd/list.go 🔗

@@ -0,0 +1,81 @@
+package cmd
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/charmbracelet/soft-serve/git"
+	gitwish "github.com/charmbracelet/wish/git"
+	"github.com/spf13/cobra"
+)
+
+// ListCommand returns a command that list file or directory at path.
+func ListCommand() *cobra.Command {
+	lsCmd := &cobra.Command{
+		Use:     "ls PATH",
+		Aliases: []string{"list"},
+		Short:   "List file or directory at path.",
+		Args:    cobra.RangeArgs(0, 1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ac, s := fromContext(cmd)
+			rn := ""
+			path := ""
+			ps := []string{}
+			if len(args) > 0 {
+				path = filepath.Clean(args[0])
+				ps = strings.Split(path, "/")
+				rn = ps[0]
+				auth := ac.AuthRepo(rn, s.PublicKey())
+				if auth < gitwish.ReadOnlyAccess {
+					return ErrUnauthorized
+				}
+			}
+			if path == "" || path == "." || path == "/" {
+				for _, r := range ac.Source.AllRepos() {
+					fmt.Fprintln(s, r.Name())
+				}
+				return nil
+			}
+			r, err := ac.Source.GetRepo(rn)
+			if err != nil {
+				return err
+			}
+			head, err := r.HEAD()
+			if err != nil {
+				return err
+			}
+			tree, err := r.Tree(head, "")
+			if err != nil {
+				return err
+			}
+			subpath := strings.Join(ps[1:], "/")
+			ents := git.Entries{}
+			te, err := tree.TreeEntry(subpath)
+			if err == git.ErrRevisionNotExist {
+				return ErrFileNotFound
+			}
+			if err != nil {
+				return err
+			}
+			if te.Type() == "tree" {
+				tree, err = tree.SubTree(subpath)
+				if err != nil {
+					return err
+				}
+				ents, err = tree.Entries()
+				if err != nil {
+					return err
+				}
+			} else {
+				ents = append(ents, te)
+			}
+			ents.Sort()
+			for _, ent := range ents {
+				fmt.Fprintf(s, "%s\t%d\t %s\n", ent.Mode(), ent.Size(), ent.Name())
+			}
+			return nil
+		},
+	}
+	return lsCmd
+}

server/cmd/reload.go 🔗

@@ -0,0 +1,23 @@
+package cmd
+
+import (
+	gitwish "github.com/charmbracelet/wish/git"
+	"github.com/spf13/cobra"
+)
+
+// ReloadCommand returns a command that reloads the server configuration.
+func ReloadCommand() *cobra.Command {
+	reloadCmd := &cobra.Command{
+		Use:   "reload",
+		Short: "Reloads the configuration",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ac, s := fromContext(cmd)
+			auth := ac.AuthRepo("config", s.PublicKey())
+			if auth < gitwish.AdminAccess {
+				return ErrUnauthorized
+			}
+			return ac.Reload()
+		},
+	}
+	return reloadCmd
+}

server/middleware.go 🔗

@@ -1,186 +1,48 @@
 package server
 
 import (
+	"context"
 	"fmt"
-	"path/filepath"
-	"strings"
 
-	"github.com/alecthomas/chroma/lexers"
-	gansi "github.com/charmbracelet/glamour/ansi"
-	"github.com/charmbracelet/lipgloss"
-	"github.com/charmbracelet/soft-serve/git"
 	appCfg "github.com/charmbracelet/soft-serve/internal/config"
-	"github.com/charmbracelet/soft-serve/tui/common"
+	"github.com/charmbracelet/soft-serve/server/cmd"
 	"github.com/charmbracelet/wish"
-	gitwish "github.com/charmbracelet/wish/git"
 	"github.com/gliderlabs/ssh"
-	ggit "github.com/gogs/git-module"
-	"github.com/muesli/termenv"
 )
 
-var (
-	lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
-	lineBarStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
-	dirnameStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF"))
-	filenameStyle  = lipgloss.NewStyle()
-	filemodeStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
-)
-
-// softServeMiddleware is a middleware that handles displaying files with the
-// option of syntax highlighting and line numbers.
-func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
+// softMiddleware is the Soft Serve middleware that handles SSH commands.
+func softMiddleware(ac *appCfg.Config) wish.Middleware {
 	return func(sh ssh.Handler) ssh.Handler {
 		return func(s ssh.Session) {
-			_, _, active := s.Pty()
-			cmds := s.Command()
-			if !active && len(cmds) > 0 {
-				func() {
-					color := false
-					lineno := false
-					fp := filepath.Clean(cmds[0])
-					ps := strings.Split(fp, "/")
-					repo := ps[0]
-					if repo == "config" {
-						return
-					}
-					repoExists := false
-					for _, rp := range ac.Source.AllRepos() {
-						if rp.Name() == repo {
-							repoExists = true
-							break
-						}
-					}
-					if !repoExists {
-						s.Write([]byte("repository not found"))
-						s.Exit(1)
-						return
-					}
-					auth := ac.AuthRepo(repo, s.PublicKey())
-					if auth < gitwish.ReadOnlyAccess {
-						s.Write([]byte("unauthorized"))
-						s.Exit(1)
-						return
-					}
-					for _, op := range cmds[1:] {
-						if op == "-c" || op == "--color" {
-							color = true
-						} else if op == "-l" || op == "--lineno" || op == "--linenumber" {
-							lineno = true
-						}
-					}
-					rs, err := ac.Source.GetRepo(repo)
-					if err != nil {
-						_, _ = s.Write([]byte(err.Error()))
-						_ = s.Exit(1)
-						return
-					}
-					ref, err := rs.HEAD()
-					if err != nil {
-						_, _ = s.Write([]byte(err.Error()))
-						_ = s.Exit(1)
-						return
-					}
-					p := strings.Join(ps[1:], "/")
-					t, err := rs.Tree(ref, p)
-					if err != nil && err != ggit.ErrRevisionNotExist {
-						_, _ = s.Write([]byte(err.Error()))
-						_ = s.Exit(1)
-						return
-					}
-					if err == ggit.ErrRevisionNotExist {
-						_, _ = s.Write([]byte(git.ErrFileNotFound.Error()))
-						_ = s.Exit(1)
-						return
-					}
-					ents, err := t.Entries()
-					if err != nil {
-						fc, _, err := rs.LatestFile(p)
-						if err != nil {
-							_, _ = s.Write([]byte(err.Error()))
-							_ = s.Exit(1)
-							return
-						}
-						if color {
-							ffc, err := withFormatting(fp, fc)
-							if err != nil {
-								s.Write([]byte(err.Error()))
-								s.Exit(1)
-								return
-							}
-							fc = ffc
-						}
-						if lineno {
-							fc = withLineNumber(fc, color)
-						}
-						s.Write([]byte(fc))
-					} else {
-						ents.Sort()
-						for _, e := range ents {
-							m := e.Mode()
-							if m == 0 {
-								s.Write([]byte(strings.Repeat(" ", 10)))
-							} else {
-								s.Write([]byte(filemodeStyle.Render(m.String())))
-							}
-							s.Write([]byte(" "))
-							if !e.IsTree() {
-								s.Write([]byte(filenameStyle.Render(e.Name())))
-							} else {
-								s.Write([]byte(dirnameStyle.Render(e.Name())))
-							}
-							s.Write([]byte("\n"))
-						}
-					}
-				}()
-			}
-			sh(s)
-		}
-	}
-}
+			func() {
+				_, _, active := s.Pty()
+				if active {
+					return
+				}
+				ctx := s.Context()
+				ctx = context.WithValue(ctx, "config", ac) //nolint:revive
+				ctx = context.WithValue(ctx, "session", s) //nolint:revive
 
-func withLineNumber(s string, color bool) string {
-	lines := strings.Split(s, "\n")
-	// NB: len() is not a particularly safe way to count string width (because
-	// it's counting bytes instead of runes) but in this case it's okay
-	// because we're only dealing with digits, which are one byte each.
-	mll := len(fmt.Sprintf("%d", len(lines)))
-	for i, l := range lines {
-		digit := fmt.Sprintf("%*d", mll, i+1)
-		bar := "│"
-		if color {
-			digit = lineDigitStyle.Render(digit)
-			bar = lineBarStyle.Render(bar)
-		}
-		if i < len(lines)-1 || len(l) != 0 {
-			// If the final line was a newline we'll get an empty string for
-			// the final line, so drop the newline altogether.
-			lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
+				use := "ssh"
+				port := ac.Cfg.Port
+				if port != 22 {
+					use += fmt.Sprintf(" -p%d", port)
+				}
+				use += fmt.Sprintf(" %s", ac.Cfg.Host)
+				cmd := cmd.RootCommand()
+				cmd.Use = use
+				cmd.SetIn(s)
+				cmd.SetOut(s)
+				cmd.SetErr(s)
+				cmd.SetArgs(s.Command())
+				err := cmd.ExecuteContext(ctx)
+				if err != nil {
+					_, _ = s.Write([]byte(err.Error()))
+					_ = s.Exit(1)
+					return
+				}
+			}()
+			sh(s)
 		}
 	}
-	return strings.Join(lines, "\n")
-}
-
-func withFormatting(p, c string) (string, error) {
-	zero := uint(0)
-	lang := ""
-	lexer := lexers.Match(p)
-	if lexer != nil && lexer.Config() != nil {
-		lang = lexer.Config().Name
-	}
-	formatter := &gansi.CodeBlockElement{
-		Code:     c,
-		Language: lang,
-	}
-	r := strings.Builder{}
-	styles := common.DefaultStyles()
-	styles.CodeBlock.Margin = &zero
-	rctx := gansi.NewRenderContext(gansi.Options{
-		Styles:       styles,
-		ColorProfile: termenv.TrueColor,
-	})
-	err := formatter.Render(&r, rctx)
-	if err != nil {
-		return "", err
-	}
-	return r.String(), nil
 }

server/middleware_test.go 🔗

@@ -1,8 +1,10 @@
 package server
 
 import (
+	"os"
 	"testing"
 
+	sconfig "github.com/charmbracelet/soft-serve/config"
 	"github.com/charmbracelet/soft-serve/internal/config"
 	"github.com/charmbracelet/wish/testsession"
 	"github.com/gliderlabs/ssh"
@@ -12,13 +14,21 @@ import (
 var ()
 
 func TestMiddleware(t *testing.T) {
+	t.Cleanup(func() {
+		os.RemoveAll("testmiddleware")
+	})
 	is := is.New(t)
-	appCfg, err := config.NewConfig(cfg)
+	appCfg, err := config.NewConfig(&sconfig.Config{
+		Host:     "localhost",
+		Port:     22223,
+		RepoPath: "testmiddleware/repos",
+		KeyPath:  "testmiddleware/key",
+	})
 	is.NoErr(err)
 	_ = testsession.New(t, &ssh.Server{
-		Handler: softServeMiddleware(appCfg)(func(s ssh.Session) {
+		Handler: softMiddleware(appCfg)(func(s ssh.Session) {
 			t.Run("TestCatConfig", func(t *testing.T) {
-				_, err := s.Write([]byte("config/config.json"))
+				_, err := s.Write([]byte("cat config/config.json"))
 				if err == nil {
 					t.Errorf("Expected error, got nil")
 				}

server/server.go 🔗

@@ -36,7 +36,7 @@ func NewServer(cfg *config.Config) *Server {
 	mw := []wish.Middleware{
 		rm.MiddlewareWithLogger(
 			cfg.ErrorLog,
-			softServeMiddleware(ac),
+			softMiddleware(ac),
 			bm.Middleware(tui.SessionHandler(ac)),
 			gm.Middleware(cfg.RepoPath, ac),
 			lm.Middleware(),