Detailed changes
@@ -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
@@ -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=
@@ -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
+}
@@ -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
+}
@@ -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()
+}
@@ -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
+}
@@ -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
+}
@@ -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
}
@@ -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")
}
@@ -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(),