Detailed changes
@@ -7,16 +7,22 @@ import (
"io/fs"
"os"
+ "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/db"
"github.com/charmbracelet/soft-serve/pkg/hooks"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/sshutils"
"github.com/charmbracelet/soft-serve/pkg/store"
"github.com/charmbracelet/soft-serve/pkg/store/database"
"github.com/spf13/cobra"
)
// InitBackendContext initializes the backend context.
+// When a public-key is provided via the "SOFT_SERVE_PUBLIC_KEY" environment
+// variable, it will be used to try to find the corresponding user in the
+// database and set the user in the context.
func InitBackendContext(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
cfg := config.FromContext(ctx)
@@ -36,6 +42,18 @@ func InitBackendContext(cmd *cobra.Command, _ []string) error {
be := backend.New(ctx, cfg, dbx)
ctx = backend.WithContext(ctx, be)
+ // Store user in context if public key is provided
+ // via environment variable.
+ if ak, ok := os.LookupEnv("SOFT_SERVE_PUBLIC_KEY"); ok {
+ pk, _, err := sshutils.ParseAuthorizedKey(ak)
+ if err == nil && pk != nil {
+ user, err := be.UserByPublicKey(ctx, pk)
+ if err == nil && user != nil {
+ ctx = proto.WithUserContext(ctx, user)
+ }
+ }
+ }
+
cmd.SetContext(ctx)
return nil
@@ -69,3 +87,19 @@ func InitializeHooks(ctx context.Context, cfg *config.Config, be *backend.Backen
return nil
}
+
+// CheckUserHasAccess checks if the user in context has access to the repository.
+// If there is no user in context, it will skip the check and return true.
+// It won't skip this check if the "strict" flag is set to true.
+func CheckUserHasAccess(cmd *cobra.Command, repo string, level access.AccessLevel) bool {
+ ctx := cmd.Context()
+ isStrict := cmd.Flag("strict").Value.String() == "true"
+ if _, ok := os.LookupEnv("SOFT_SERVE_PUBLIC_KEY"); !ok && isStrict {
+ return false
+ }
+
+ user := proto.UserFromContext(ctx)
+ be := backend.FromContext(ctx)
+ al := be.AccessLevelForUser(ctx, repo, user)
+ return al >= level
+}
@@ -10,7 +10,11 @@ import (
"github.com/charmbracelet/soft-serve/cmd/soft/admin"
"github.com/charmbracelet/soft-serve/cmd/soft/browse"
"github.com/charmbracelet/soft-serve/cmd/soft/hook"
+ "github.com/charmbracelet/soft-serve/cmd/soft/repo"
"github.com/charmbracelet/soft-serve/cmd/soft/serve"
+ "github.com/charmbracelet/soft-serve/cmd/soft/settings"
+ "github.com/charmbracelet/soft-serve/cmd/soft/shell"
+ "github.com/charmbracelet/soft-serve/cmd/soft/user"
"github.com/charmbracelet/soft-serve/pkg/config"
logr "github.com/charmbracelet/soft-serve/pkg/log"
"github.com/charmbracelet/soft-serve/pkg/version"
@@ -33,6 +37,10 @@ var (
// built against. It's set via ldflags when building.
CommitDate = ""
+ // When this flag is set, the user will be checked for access to the
+ // repository before the command is run during cmd.CheckUserHasAccess.
+ strict bool
+
rootCmd = &cobra.Command{
Use: "soft",
Short: "A self-hostable Git server for the command line",
@@ -49,7 +57,7 @@ var (
Args: cobra.NoArgs,
Hidden: true,
RunE: func(_ *cobra.Command, _ []string) error {
- manPage, err := mcobra.NewManPage(1, rootCmd) //.
+ manPage, err := mcobra.NewManPage(1, rootCmd)
if err != nil {
return err
}
@@ -64,13 +72,18 @@ var (
func init() {
rootCmd.AddCommand(
- manCmd,
- serve.Command,
- hook.Command,
admin.Command,
browse.Command,
+ hook.Command,
+ manCmd,
+ repo.Command,
+ serve.Command,
+ settings.Command,
+ shell.Command,
+ user.Command,
)
rootCmd.CompletionOptions.HiddenDefaultCmd = true
+ rootCmd.PersistentFlags().BoolVarP(&strict, "strict", "", false, "Check if the user has access to the command")
if len(CommitSHA) >= 7 {
vt := rootCmd.VersionTemplate()
@@ -0,0 +1,114 @@
+package repo
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/ui/common"
+ "github.com/charmbracelet/soft-serve/pkg/ui/styles"
+ "github.com/spf13/cobra"
+)
+
+// blobCommand returns a command that prints the contents of a file.
+func blobCommand() *cobra.Command {
+ var linenumber bool
+ var color bool
+ var raw bool
+
+ styles := styles.DefaultStyles()
+ cmd := &cobra.Command{
+ Use: "blob REPOSITORY [REFERENCE] [PATH]",
+ Aliases: []string{"cat", "show"},
+ Short: "Print out the contents of file at path",
+ Args: cobra.RangeArgs(1, 3),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := args[0]
+ ref := ""
+ fp := ""
+ switch len(args) {
+ case 2:
+ fp = args[1]
+ case 3:
+ ref = args[1]
+ fp = args[2]
+ }
+
+ repo, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := repo.Open()
+ if err != nil {
+ return err
+ }
+
+ if ref == "" {
+ head, err := r.HEAD()
+ if err != nil {
+ return err
+ }
+ ref = head.ID
+ }
+
+ tree, err := r.LsTree(ref)
+ if err != nil {
+ return err
+ }
+
+ te, err := tree.TreeEntry(fp)
+ if err != nil {
+ return err
+ }
+
+ if te.Type() != "blob" {
+ return git.ErrFileNotFound
+ }
+
+ bts, err := te.Contents()
+ if err != nil {
+ return err
+ }
+
+ c := string(bts)
+ isBin, _ := te.File().IsBinary()
+ if isBin {
+ if raw {
+ co.Println(c)
+ } else {
+ return fmt.Errorf("binary file: use --raw to print")
+ }
+ } else {
+ if color {
+ c, err = common.FormatHighlight(fp, c)
+ if err != nil {
+ return err
+ }
+ }
+
+ if linenumber {
+ c, _ = common.FormatLineNumber(styles, c, color)
+ }
+
+ co.Println(c)
+ }
+ return nil
+ },
+ }
+
+ cmd.Flags().BoolVarP(&raw, "raw", "r", false, "Print raw contents")
+ cmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
+ cmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
+
+ return cmd
+}
@@ -0,0 +1,212 @@
+package repo
+
+import (
+ "fmt"
+ "strings"
+
+ gitm "github.com/aymanbagabas/git-module"
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/webhook"
+ "github.com/spf13/cobra"
+)
+
+func branchCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "branch",
+ Short: "Manage repository branches",
+ }
+
+ cmd.AddCommand(
+ branchListCommand(),
+ branchDefaultCommand(),
+ branchDeleteCommand(),
+ )
+
+ return cmd
+}
+
+func branchListCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list REPOSITORY",
+ Short: "List repository branches",
+ Args: cobra.ExactArgs(1),
+ RunE: func(c *cobra.Command, args []string) error {
+ ctx := c.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(c, rr.Name(), access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ branches, _ := r.Branches()
+ for _, b := range branches {
+ c.Println(b)
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
+
+func branchDefaultCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "default REPOSITORY [BRANCH]",
+ Short: "Set or get the default branch",
+ Args: cobra.RangeArgs(1, 2),
+ RunE: func(c *cobra.Command, args []string) error {
+ ctx := c.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ switch len(args) {
+ case 1:
+ if !cmd.CheckUserHasAccess(c, rr.Name(), access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ head, err := r.HEAD()
+ if err != nil {
+ return err
+ }
+
+ c.Println(head.Name().Short())
+ case 2:
+ if !cmd.CheckUserHasAccess(c, rr.Name(), access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ branch := args[1]
+ branches, _ := r.Branches()
+ var exists bool
+ for _, b := range branches {
+ if branch == b {
+ exists = true
+ break
+ }
+ }
+
+ if !exists {
+ return git.ErrReferenceNotExist
+ }
+
+ if _, err := r.SymbolicRef(git.HEAD, gitm.RefsHeads+branch, gitm.SymbolicRefOptions{
+ CommandOptions: gitm.CommandOptions{
+ Context: ctx,
+ },
+ }); err != nil {
+ return err
+ }
+
+ // TODO: move this to backend?
+ user := proto.UserFromContext(ctx)
+ wh, err := webhook.NewRepositoryEvent(ctx, user, rr, webhook.RepositoryEventActionDefaultBranchChange)
+ if err != nil {
+ return err
+ }
+
+ return webhook.SendEvent(ctx, wh)
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
+
+func branchDeleteCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete REPOSITORY BRANCH",
+ Aliases: []string{"remove", "rm", "del"},
+ Short: "Delete a branch",
+ RunE: func(c *cobra.Command, args []string) error {
+ ctx := c.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(c, rr.Name(), access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ branch := args[1]
+ branches, _ := r.Branches()
+ var exists bool
+ for _, b := range branches {
+ if branch == b {
+ exists = true
+ break
+ }
+ }
+
+ if !exists {
+ return git.ErrReferenceNotExist
+ }
+
+ head, err := r.HEAD()
+ if err != nil {
+ return err
+ }
+
+ if head.Name().Short() == branch {
+ return fmt.Errorf("cannot delete the default branch")
+ }
+
+ branchCommit, err := r.BranchCommit(branch)
+ if err != nil {
+ return err
+ }
+
+ if err := r.DeleteBranch(branch, gitm.DeleteBranchOptions{Force: true}); err != nil {
+ return err
+ }
+
+ wh, err := webhook.NewBranchTagEvent(ctx, proto.UserFromContext(ctx), rr, git.RefsHeads+branch, branchCommit.ID.String(), git.ZeroID)
+ if err != nil {
+ return err
+ }
+
+ return webhook.SendEvent(ctx, wh)
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,121 @@
+package repo
+
+import (
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func collabCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "collab",
+ Aliases: []string{"collabs", "collaborator", "collaborators"},
+ Short: "Manage collaborators",
+ }
+
+ cmd.AddCommand(
+ collabAddCommand(),
+ collabRemoveCommand(),
+ collabListCommand(),
+ )
+
+ return cmd
+}
+
+func collabAddCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "add REPOSITORY USERNAME [LEVEL]",
+ Short: "Add a collaborator to a repo",
+ Long: "Add a collaborator to a repo. LEVEL can be one of: no-access, read-only, read-write, or admin-access. Defaults to read-write.",
+ Args: cobra.RangeArgs(2, 3),
+ RunE: func(c *cobra.Command, args []string) error {
+ ctx := c.Context()
+ be := backend.FromContext(ctx)
+ repo := args[0]
+ rr, err := be.Repository(ctx, repo)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(c, rr.Name(), access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ username := args[1]
+ level := access.ReadWriteAccess
+ if len(args) > 2 {
+ level = access.ParseAccessLevel(args[2])
+ if level < 0 {
+ return access.ErrInvalidAccessLevel
+ }
+ }
+
+ return be.AddCollaborator(ctx, repo, username, level)
+ },
+ }
+
+ return cmd
+}
+
+func collabRemoveCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "remove REPOSITORY USERNAME",
+ Args: cobra.ExactArgs(2),
+ Short: "Remove a collaborator from a repo",
+ RunE: func(c *cobra.Command, args []string) error {
+ ctx := c.Context()
+ be := backend.FromContext(ctx)
+ repo := args[0]
+ rr, err := be.Repository(ctx, repo)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(c, rr.Name(), access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ username := args[1]
+
+ return be.RemoveCollaborator(ctx, repo, username)
+ },
+ }
+
+ return cmd
+}
+
+func collabListCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list REPOSITORY",
+ Short: "List collaborators for a repo",
+ Args: cobra.ExactArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo := args[0]
+ rr, err := be.Repository(ctx, repo)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, rr.Name(), access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ collabs, err := be.Collaborators(ctx, repo)
+ if err != nil {
+ return err
+ }
+
+ for _, c := range collabs {
+ co.Println(c)
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,171 @@
+package repo
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ gansi "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/ui/common"
+ "github.com/charmbracelet/soft-serve/pkg/ui/styles"
+ "github.com/muesli/termenv"
+ "github.com/spf13/cobra"
+)
+
+// commitCommand returns a command that prints the contents of a commit.
+func commitCommand() *cobra.Command {
+ var color bool
+ var patchOnly bool
+
+ cmd := &cobra.Command{
+ Use: "commit SHA",
+ Short: "Print out the contents of a diff",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repoName := args[0]
+ commitSHA := args[1]
+
+ rr, err := be.Repository(ctx, repoName)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, rr.Name(), access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ commit, err := r.CommitByRevision(commitSHA)
+ if err != nil {
+ return err
+ }
+
+ patch, err := r.Patch(commit)
+ if err != nil {
+ return err
+ }
+
+ diff, err := r.Diff(commit)
+ if err != nil {
+ return err
+ }
+
+ commonStyle := styles.DefaultStyles()
+ style := commonStyle.Log
+
+ s := strings.Builder{}
+ commitLine := "commit " + commitSHA
+ authorLine := "Author: " + commit.Author.Name
+ dateLine := "Date: " + commit.Committer.When.UTC().Format(time.UnixDate)
+ msgLine := strings.ReplaceAll(commit.Message, "\r\n", "\n")
+ statsLine := renderStats(diff, commonStyle, color)
+ diffLine := renderDiff(patch, color)
+
+ if patchOnly {
+ co.Println(
+ diffLine,
+ )
+ return nil
+ }
+
+ if color {
+ s.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n",
+ style.CommitHash.Render(commitLine),
+ style.CommitAuthor.Render(authorLine),
+ style.CommitDate.Render(dateLine),
+ style.CommitBody.Render(msgLine),
+ ))
+ } else {
+ s.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n",
+ commitLine,
+ authorLine,
+ dateLine,
+ msgLine,
+ ))
+ }
+
+ s.WriteString(fmt.Sprintf("\n%s\n%s",
+ statsLine,
+ diffLine,
+ ))
+
+ co.Println(
+ s.String(),
+ )
+
+ return nil
+ },
+ }
+
+ cmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
+ cmd.Flags().BoolVarP(&patchOnly, "patch", "p", false, "Output patch only")
+
+ return cmd
+}
+
+func renderCtx() gansi.RenderContext {
+ return gansi.NewRenderContext(gansi.Options{
+ ColorProfile: termenv.TrueColor,
+ Styles: common.StyleConfig(),
+ })
+}
+
+func renderDiff(patch string, color bool) string {
+ c := patch
+
+ if color {
+ var s strings.Builder
+ var pr strings.Builder
+
+ diffChroma := &gansi.CodeBlockElement{
+ Code: patch,
+ Language: "diff",
+ }
+
+ err := diffChroma.Render(&pr, renderCtx())
+
+ if err != nil {
+ s.WriteString(fmt.Sprintf("\n%s", err.Error()))
+ } else {
+ s.WriteString(fmt.Sprintf("\n%s", pr.String()))
+ }
+
+ c = s.String()
+ }
+
+ return c
+}
+
+func renderStats(diff *git.Diff, commonStyle *styles.Styles, color bool) string {
+ style := commonStyle.Log
+ c := diff.Stats().String()
+
+ if color {
+ s := strings.Split(c, "\n")
+
+ for i, line := range s {
+ ch := strings.Split(line, "|")
+ if len(ch) > 1 {
+ adddel := ch[len(ch)-1]
+ adddel = strings.ReplaceAll(adddel, "+", style.CommitStatsAdd.Render("+"))
+ adddel = strings.ReplaceAll(adddel, "-", style.CommitStatsDel.Render("-"))
+ s[i] = strings.Join(ch[:len(ch)-1], "|") + "|" + adddel
+ }
+ }
+
+ return strings.Join(s, "\n")
+ }
+
+ return c
+}
@@ -0,0 +1,59 @@
+package repo
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "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/spf13/cobra"
+)
+
+// createCommand is the command for creating a new repository.
+func createCommand() *cobra.Command {
+ var private bool
+ var description string
+ var projectName string
+ var hidden bool
+
+ cmd := &cobra.Command{
+ Use: "create REPOSITORY",
+ Short: "Create a new repository",
+ Args: cobra.ExactArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ cfg := config.FromContext(ctx)
+ be := backend.FromContext(ctx)
+ user := proto.UserFromContext(ctx)
+ name := args[0]
+ if !cmd.CheckUserHasAccess(co, name, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{
+ Private: private,
+ Description: description,
+ ProjectName: projectName,
+ Hidden: hidden,
+ })
+ if err != nil {
+ return err
+ }
+
+ cloneurl := fmt.Sprintf("%s/%s.git", cfg.SSH.PublicURL, r.Name())
+ co.PrintErrf("Created repository %s\n", r.Name())
+ co.Println(cloneurl)
+
+ return nil
+ },
+ }
+
+ cmd.Flags().BoolVarP(&private, "private", "p", false, "make the repository private")
+ cmd.Flags().StringVarP(&description, "description", "d", "", "set the repository description")
+ cmd.Flags().StringVarP(&projectName, "name", "n", "", "set the project name")
+ cmd.Flags().BoolVarP(&hidden, "hidden", "H", false, "hide the repository from the UI")
+
+ return cmd
+}
@@ -0,0 +1,30 @@
+package repo
+
+import (
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func deleteCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete REPOSITORY",
+ Aliases: []string{"del", "remove", "rm"},
+ Short: "Delete a repository",
+ Args: cobra.ExactArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ name := args[0]
+ if !cmd.CheckUserHasAccess(co, name, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ return be.DeleteRepository(ctx, name)
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,50 @@
+package repo
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func descriptionCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "description REPOSITORY [DESCRIPTION]",
+ Aliases: []string{"desc"},
+ Short: "Set or get the description for a repository",
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+ switch len(args) {
+ case 1:
+ if !cmd.CheckUserHasAccess(co, rn, access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ desc, err := be.Description(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ co.Println(desc)
+ default:
+ if !cmd.CheckUserHasAccess(co, rn, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ if err := be.SetDescription(ctx, rn, strings.Join(args[1:], " ")); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,49 @@
+package repo
+
+import (
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func hiddenCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "hidden REPOSITORY [TRUE|FALSE]",
+ Short: "Hide or unhide a repository",
+ Aliases: []string{"hide"},
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo := args[0]
+ switch len(args) {
+ case 1:
+ if !cmd.CheckUserHasAccess(co, repo, access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ hidden, err := be.IsHidden(ctx, repo)
+ if err != nil {
+ return err
+ }
+
+ co.Println(hidden)
+ case 2:
+ if !cmd.CheckUserHasAccess(co, repo, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ hidden := args[1] == "true"
+ if err := be.SetHidden(ctx, repo, hidden); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,67 @@
+package repo
+
+import (
+ "errors"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/task"
+ "github.com/spf13/cobra"
+)
+
+// importCommand is the command for creating a new repository.
+func importCommand() *cobra.Command {
+ var private bool
+ var description string
+ var projectName string
+ var mirror bool
+ var hidden bool
+ var lfs bool
+ var lfsEndpoint string
+
+ cmd := &cobra.Command{
+ Use: "import REPOSITORY REMOTE",
+ Short: "Import a new repository from remote",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ user := proto.UserFromContext(ctx)
+ name := args[0]
+ if !cmd.CheckUserHasAccess(co, name, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ remote := args[1]
+ if _, err := be.ImportRepository(ctx, name, user, remote, proto.RepositoryOptions{
+ Private: private,
+ Description: description,
+ ProjectName: projectName,
+ Mirror: mirror,
+ Hidden: hidden,
+ LFS: lfs,
+ LFSEndpoint: lfsEndpoint,
+ }); err != nil {
+ if errors.Is(err, task.ErrAlreadyStarted) {
+ return errors.New("import already in progress")
+ }
+
+ return err
+ }
+
+ return nil
+ },
+ }
+
+ cmd.Flags().BoolVarP(&lfs, "lfs", "", false, "pull Git LFS objects")
+ cmd.Flags().StringVarP(&lfsEndpoint, "lfs-endpoint", "", "", "set the Git LFS endpoint")
+ cmd.Flags().BoolVarP(&mirror, "mirror", "m", false, "mirror the repository")
+ cmd.Flags().BoolVarP(&private, "private", "p", false, "make the repository private")
+ cmd.Flags().StringVarP(&description, "description", "d", "", "set the repository description")
+ cmd.Flags().StringVarP(&projectName, "name", "n", "", "set the project name")
+ cmd.Flags().BoolVarP(&hidden, "hidden", "H", false, "hide the repository from the UI")
+
+ return cmd
+}
@@ -0,0 +1,43 @@
+package repo
+
+import (
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/spf13/cobra"
+)
+
+// listCommand returns a command that list file or directory at path.
+func listCommand() *cobra.Command {
+ var all bool
+
+ listCmd := &cobra.Command{
+ Use: "list",
+ Aliases: []string{"ls"},
+ Short: "List repositories",
+ Args: cobra.NoArgs,
+ RunE: func(co *cobra.Command, _ []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repos, err := be.Repositories(ctx)
+ if err != nil {
+ return err
+ }
+
+ for _, r := range repos {
+ if !cmd.CheckUserHasAccess(co, r.Name(), access.ReadOnlyAccess) {
+ continue
+ }
+
+ if !r.IsHidden() || all {
+ co.Println(r.Name())
+ }
+ }
+ return nil
+ },
+ }
+
+ listCmd.Flags().BoolVarP(&all, "all", "a", false, "List all repositories")
+
+ return listCmd
+}
@@ -0,0 +1,36 @@
+package repo
+
+import (
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func mirrorCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "is-mirror REPOSITORY",
+ Short: "Whether a repository is a mirror",
+ Args: cobra.ExactArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := args[0]
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, rr.Name(), access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ isMirror := rr.IsMirror()
+ co.Println(isMirror)
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,54 @@
+package repo
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func privateCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "private REPOSITORY [true|false]",
+ Short: "Set or get a repository private property",
+ Args: cobra.RangeArgs(1, 2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+
+ switch len(args) {
+ case 1:
+ if !cmd.CheckUserHasAccess(co, rn, access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ isPrivate, err := be.IsPrivate(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ co.Println(isPrivate)
+ case 2:
+ if !cmd.CheckUserHasAccess(co, rn, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ isPrivate, err := strconv.ParseBool(args[1])
+ if err != nil {
+ return err
+ }
+ if err := be.SetPrivate(ctx, rn, isPrivate); err != nil {
+ return err
+ }
+ }
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,50 @@
+package repo
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func projectName() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "project-name REPOSITORY [NAME]",
+ Aliases: []string{"project"},
+ Short: "Set or get the project name for a repository",
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+ switch len(args) {
+ case 1:
+ if !cmd.CheckUserHasAccess(co, rn, access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ pn, err := be.ProjectName(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ co.Println(pn)
+ default:
+ if !cmd.CheckUserHasAccess(co, rn, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ if err := be.SetProjectName(ctx, rn, strings.Join(args[1:], " ")); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,32 @@
+package repo
+
+import (
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+func renameCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "rename REPOSITORY NEW_NAME",
+ Aliases: []string{"mv", "move"},
+ Short: "Rename an existing repository",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ oldName := args[0]
+ newName := args[1]
+
+ if !cmd.CheckUserHasAccess(co, oldName, access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ return be.RenameRepository(ctx, oldName, newName)
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,117 @@
+package repo
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/spf13/cobra"
+)
+
+var (
+ // Command returns a command for managing repositories.
+ Command = &cobra.Command{
+ Use: "repo",
+ Aliases: []string{"repos", "repository", "repositories"},
+ Short: "Manage repositories",
+ PersistentPreRunE: cmd.InitBackendContext,
+ PersistentPostRunE: cmd.CloseDBContext,
+ }
+)
+
+func init() {
+ Command.AddCommand(
+ blobCommand(),
+ branchCommand(),
+ collabCommand(),
+ commitCommand(),
+ createCommand(),
+ deleteCommand(),
+ descriptionCommand(),
+ hiddenCommand(),
+ importCommand(),
+ infoCommand(),
+ listCommand(),
+ mirrorCommand(),
+ privateCommand(),
+ projectName(),
+ renameCommand(),
+ tagCommand(),
+ treeCommand(),
+ webhookCommand(),
+ )
+}
+
+func infoCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "info REPOSITORY",
+ Short: "Get information about a repository",
+ Args: cobra.ExactArgs(1),
+ RunE: func(c *cobra.Command, args []string) error {
+ ctx := c.Context()
+ be := backend.FromContext(ctx)
+ rn := args[0]
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(c, rr.Name(), access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ head, err := r.HEAD()
+ if err != nil {
+ return err
+ }
+
+ var owner proto.User
+ if rr.UserID() > 0 {
+ owner, err = be.UserByID(ctx, rr.UserID())
+ if err != nil {
+ return err
+ }
+ }
+
+ branches, _ := r.Branches()
+ tags, _ := r.Tags()
+
+ // project name and description are optional, handle trailing
+ // whitespace to avoid breaking tests.
+ c.Println(strings.TrimSpace(fmt.Sprint("Project Name: ", rr.ProjectName())))
+ c.Println("Repository:", rr.Name())
+ c.Println(strings.TrimSpace(fmt.Sprint("Description: ", rr.Description())))
+ c.Println("Private:", rr.IsPrivate())
+ c.Println("Hidden:", rr.IsHidden())
+ c.Println("Mirror:", rr.IsMirror())
+ if owner != nil {
+ c.Println(strings.TrimSpace(fmt.Sprint("Owner: ", owner.Username())))
+ }
+ c.Println("Default Branch:", head.Name().Short())
+ if len(branches) > 0 {
+ c.Println("Branches:")
+ for _, b := range branches {
+ c.Println(" -", b)
+ }
+ }
+ if len(tags) > 0 {
+ c.Println("Tags:")
+ for _, t := range tags {
+ c.Println(" -", t)
+ }
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,128 @@
+package repo
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/log"
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/webhook"
+ "github.com/spf13/cobra"
+)
+
+func tagCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "tag",
+ Short: "Manage repository tags",
+ }
+
+ cmd.AddCommand(
+ tagListCommand(),
+ tagDeleteCommand(),
+ )
+
+ return cmd
+}
+
+func tagListCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list REPOSITORY",
+ Aliases: []string{"ls"},
+ Short: "List repository tags",
+ Args: cobra.ExactArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, rr.Name(), access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ tags, _ := r.Tags()
+ for _, t := range tags {
+ co.Println(t)
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
+
+func tagDeleteCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete REPOSITORY TAG",
+ Aliases: []string{"remove", "rm", "del"},
+ Short: "Delete a tag",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := strings.TrimSuffix(args[0], ".git")
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, rr.Name(), access.ReadWriteAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ log.Errorf("failed to open repo: %s", err)
+ return err
+ }
+
+ tag := args[1]
+ tags, _ := r.Tags()
+ var exists bool
+ for _, t := range tags {
+ if tag == t {
+ exists = true
+ break
+ }
+ }
+
+ if !exists {
+ log.Errorf("failed to get tag: tag %s does not exist", tag)
+ return git.ErrReferenceNotExist
+ }
+
+ tagCommit, err := r.TagCommit(tag)
+ if err != nil {
+ log.Errorf("failed to get tag commit: %s", err)
+ return err
+ }
+
+ if err := r.DeleteTag(tag); err != nil {
+ log.Errorf("failed to delete tag: %s", err)
+ return err
+ }
+
+ wh, err := webhook.NewBranchTagEvent(ctx, proto.UserFromContext(ctx), rr, git.RefsTags+tag, tagCommit.ID.String(), git.ZeroID)
+ if err != nil {
+ log.Error("failed to create branch_tag webhook", "err", err)
+ return err
+ }
+
+ return webhook.SendEvent(ctx, wh)
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,108 @@
+package repo
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/dustin/go-humanize"
+ "github.com/spf13/cobra"
+)
+
+// treeCommand returns a command that list file or directory at path.
+func treeCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "tree REPOSITORY [REFERENCE] [PATH]",
+ Short: "Print repository tree at path",
+ Args: cobra.RangeArgs(1, 3),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ rn := args[0]
+ path := ""
+ ref := ""
+ switch len(args) {
+ case 2:
+ path = args[1]
+ case 3:
+ ref = args[1]
+ path = args[2]
+ }
+
+ rr, err := be.Repository(ctx, rn)
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, rr.Name(), access.ReadOnlyAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ return err
+ }
+
+ if ref == "" {
+ head, err := r.HEAD()
+ if err != nil {
+ if bs, err := r.Branches(); err != nil && len(bs) == 0 {
+ return fmt.Errorf("repository is empty")
+ }
+ return err
+ }
+
+ ref = head.ID
+ }
+
+ tree, err := r.LsTree(ref)
+ if err != nil {
+ return err
+ }
+
+ ents := git.Entries{}
+ if path != "" && path != "/" {
+ te, err := tree.TreeEntry(path)
+ if err == git.ErrRevisionNotExist {
+ return proto.ErrFileNotFound
+ }
+ if err != nil {
+ return err
+ }
+ if te.Type() == "tree" {
+ tree, err = tree.SubTree(path)
+ if err != nil {
+ return err
+ }
+ ents, err = tree.Entries()
+ if err != nil {
+ return err
+ }
+ } else {
+ ents = append(ents, te)
+ }
+ } else {
+ ents, err = tree.Entries()
+ if err != nil {
+ return err
+ }
+ }
+ ents.Sort()
+ for _, ent := range ents {
+ size := ent.Size()
+ ssize := ""
+ if size == 0 {
+ ssize = "-"
+ } else {
+ ssize = humanize.Bytes(uint64(size))
+ }
+ co.Printf("%s\t%s\t %s\n", ent.Mode(), ssize, ent.Name())
+ }
+ return nil
+ },
+ }
+ return cmd
+}
@@ -0,0 +1,442 @@
+package repo
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/caarlos0/tablewriter"
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/webhook"
+ "github.com/dustin/go-humanize"
+ "github.com/google/uuid"
+ "github.com/spf13/cobra"
+)
+
+func webhookCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "webhook",
+ Aliases: []string{"webhooks"},
+ Short: "Manage repository webhooks",
+ }
+
+ cmd.AddCommand(
+ webhookListCommand(),
+ webhookCreateCommand(),
+ webhookDeleteCommand(),
+ webhookUpdateCommand(),
+ webhookDeliveriesCommand(),
+ )
+
+ return cmd
+}
+
+var webhookEvents []string
+
+func init() {
+ events := webhook.Events()
+ webhookEvents = make([]string, len(events))
+ for i, e := range events {
+ webhookEvents[i] = e.String()
+ }
+}
+
+func webhookListCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list REPOSITORY",
+ Short: "List repository webhooks",
+ Args: cobra.ExactArgs(1),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo, err := be.Repository(ctx, args[0])
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.AdminAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ webhooks, err := be.ListWebhooks(ctx, repo)
+ if err != nil {
+ return err
+ }
+
+ return tablewriter.Render(
+ co.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),
+ }
+
+ return row, nil
+ },
+ )
+ },
+ }
+
+ return cmd
+}
+
+func webhookCreateCommand() *cobra.Command {
+ var events []string
+ var secret string
+ var active bool
+ var contentType string
+
+ cmd := &cobra.Command{
+ Use: "create REPOSITORY URL",
+ Short: "Create a repository webhook",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo, err := be.Repository(ctx, args[0])
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.AdminAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ var evs []webhook.Event
+ for _, e := range events {
+ ev, err := webhook.ParseEvent(e)
+ if err != nil {
+ return fmt.Errorf("invalid event: %w", err)
+ }
+
+ evs = append(evs, ev)
+ }
+
+ var ct webhook.ContentType
+ switch strings.ToLower(strings.TrimSpace(contentType)) {
+ case "json":
+ ct = webhook.ContentTypeJSON
+ case "form":
+ ct = webhook.ContentTypeForm
+ default:
+ return webhook.ErrInvalidContentType
+ }
+
+ return be.CreateWebhook(ctx, repo, strings.TrimSpace(args[1]), ct, secret, evs, active)
+ },
+ }
+
+ cmd.Flags().StringSliceVarP(&events, "events", "e", nil, fmt.Sprintf("events to trigger the webhook, available events are (%s)", strings.Join(webhookEvents, ", ")))
+ cmd.Flags().StringVarP(&secret, "secret", "s", "", "secret to sign the webhook payload")
+ cmd.Flags().BoolVarP(&active, "active", "a", true, "whether the webhook is active")
+ cmd.Flags().StringVarP(&contentType, "content-type", "c", "json", "content type of the webhook payload, can be either `json` or `form`")
+
+ return cmd
+}
+
+func webhookDeleteCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete REPOSITORY WEBHOOK_ID",
+ Short: "Delete a repository webhook",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo, err := be.Repository(ctx, args[0])
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.AdminAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ id, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return fmt.Errorf("invalid webhook ID: %w", err)
+ }
+
+ return be.DeleteWebhook(ctx, repo, id)
+ },
+ }
+
+ return cmd
+}
+
+func webhookUpdateCommand() *cobra.Command {
+ var events []string
+ var secret string
+ var active string
+ var contentType string
+ var url string
+
+ cmd := &cobra.Command{
+ Use: "update REPOSITORY WEBHOOK_ID",
+ Short: "Update a repository webhook",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo, err := be.Repository(ctx, args[0])
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.AdminAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ id, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return fmt.Errorf("invalid webhook ID: %w", err)
+ }
+
+ wh, err := be.Webhook(ctx, repo, id)
+ if err != nil {
+ return err
+ }
+
+ newURL := wh.URL
+ if url != "" {
+ newURL = url
+ }
+
+ newSecret := wh.Secret
+ if secret != "" {
+ newSecret = secret
+ }
+
+ newActive := wh.Active
+ if active != "" {
+ active, err := strconv.ParseBool(active)
+ if err != nil {
+ return fmt.Errorf("invalid active value: %w", err)
+ }
+
+ newActive = active
+ }
+
+ newContentType := wh.ContentType
+ if contentType != "" {
+ var ct webhook.ContentType
+ switch strings.ToLower(strings.TrimSpace(contentType)) {
+ case "json":
+ ct = webhook.ContentTypeJSON
+ case "form":
+ ct = webhook.ContentTypeForm
+ default:
+ return webhook.ErrInvalidContentType
+ }
+ newContentType = ct
+ }
+
+ newEvents := wh.Events
+ if len(events) > 0 {
+ var evs []webhook.Event
+ for _, e := range events {
+ ev, err := webhook.ParseEvent(e)
+ if err != nil {
+ return fmt.Errorf("invalid event: %w", err)
+ }
+
+ evs = append(evs, ev)
+ }
+
+ newEvents = evs
+ }
+
+ return be.UpdateWebhook(ctx, repo, id, newURL, newContentType, newSecret, newEvents, newActive)
+ },
+ }
+
+ cmd.Flags().StringSliceVarP(&events, "events", "e", nil, fmt.Sprintf("events to trigger the webhook, available events are (%s)", strings.Join(webhookEvents, ", ")))
+ cmd.Flags().StringVarP(&secret, "secret", "s", "", "secret to sign the webhook payload")
+ cmd.Flags().StringVarP(&active, "active", "a", "", "whether the webhook is active")
+ cmd.Flags().StringVarP(&contentType, "content-type", "c", "", "content type of the webhook payload, can be either `json` or `form`")
+ cmd.Flags().StringVarP(&url, "url", "u", "", "webhook URL")
+
+ return cmd
+}
+
+func webhookDeliveriesCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "deliveries",
+ Short: "Manage webhook deliveries",
+ Aliases: []string{"delivery", "deliver"},
+ }
+
+ cmd.AddCommand(
+ webhookDeliveriesListCommand(),
+ webhookDeliveriesRedeliverCommand(),
+ webhookDeliveriesGetCommand(),
+ )
+
+ return cmd
+}
+
+func webhookDeliveriesListCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list REPOSITORY WEBHOOK_ID",
+ Short: "List webhook deliveries",
+ Args: cobra.ExactArgs(2),
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo, err := be.Repository(ctx, args[0])
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.AdminAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ id, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return fmt.Errorf("invalid webhook ID: %w", err)
+ }
+
+ dels, err := be.ListWebhookDeliveries(ctx, id)
+ if err != nil {
+ return err
+ }
+
+ return tablewriter.Render(
+ co.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
+ },
+ )
+ },
+ }
+
+ return cmd
+}
+
+func webhookDeliveriesRedeliverCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "redeliver REPOSITORY WEBHOOK_ID DELIVERY_ID",
+ Short: "Redeliver a webhook delivery",
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo, err := be.Repository(ctx, args[0])
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.AdminAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ id, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return fmt.Errorf("invalid webhook ID: %w", err)
+ }
+
+ delID, err := uuid.Parse(args[2])
+ if err != nil {
+ return fmt.Errorf("invalid delivery ID: %w", err)
+ }
+
+ return be.RedeliverWebhookDelivery(ctx, repo, id, delID)
+ },
+ }
+
+ return cmd
+}
+
+func webhookDeliveriesGetCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "get REPOSITORY WEBHOOK_ID DELIVERY_ID",
+ Short: "Get a webhook delivery",
+ RunE: func(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ be := backend.FromContext(ctx)
+ repo, err := be.Repository(ctx, args[0])
+ if err != nil {
+ return err
+ }
+
+ if !cmd.CheckUserHasAccess(co, repo.Name(), access.AdminAccess) {
+ return proto.ErrUnauthorized
+ }
+
+ id, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return fmt.Errorf("invalid webhook ID: %w", err)
+ }
+
+ delID, err := uuid.Parse(args[2])
+ if err != nil {
+ return fmt.Errorf("invalid delivery ID: %w", err)
+ }
+
+ del, err := be.WebhookDelivery(ctx, id, delID)
+ if err != nil {
+ return err
+ }
+
+ out := co.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")
+ reqHeaders := strings.Split(del.RequestHeaders, "\n")
+ for _, h := range reqHeaders {
+ fmt.Fprintf(out, " %s\n", h)
+ }
+
+ fmt.Fprintf(out, "Request Body:\n")
+ reqBody := strings.Split(del.RequestBody, "\n")
+ for _, b := range reqBody {
+ fmt.Fprintf(out, " %s\n", b)
+ }
+
+ fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus)
+ fmt.Fprintf(out, "Response Headers:\n")
+ resHeaders := strings.Split(del.ResponseHeaders, "\n")
+ for _, h := range resHeaders {
+ fmt.Fprintf(out, " %s\n", h)
+ }
+
+ fmt.Fprintf(out, "Response Body:\n")
+ resBody := strings.Split(del.ResponseBody, "\n")
+ for _, b := range resBody {
+ fmt.Fprintf(out, " %s\n", b)
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,79 @@
+package settings
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/access"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/spf13/cobra"
+)
+
+var (
+ // Command returns a command that manages server settings.
+ Command = &cobra.Command{
+ Use: "settings",
+ Short: "Manage server settings",
+ PersistentPreRunE: cmd.InitBackendContext,
+ PersistentPostRunE: cmd.CloseDBContext,
+ }
+)
+
+func init() {
+ Command.AddCommand(
+ &cobra.Command{
+ Use: "allow-keyless [true|false]",
+ Short: "Set or get allow keyless access to repositories",
+ Args: cobra.RangeArgs(0, 1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ switch len(args) {
+ case 0:
+ cmd.Println(be.AllowKeyless(ctx))
+ case 1:
+ v, _ := strconv.ParseBool(args[0])
+ if err := be.SetAllowKeyless(ctx, v); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ },
+ },
+ )
+
+ als := []string{
+ access.NoAccess.String(),
+ access.ReadOnlyAccess.String(),
+ access.ReadWriteAccess.String(),
+ access.AdminAccess.String(),
+ }
+ Command.AddCommand(
+ &cobra.Command{
+ Use: "anon-access [ACCESS_LEVEL]",
+ Short: "Set or get the default access level for anonymous users",
+ Args: cobra.RangeArgs(0, 1),
+ ValidArgs: als,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ switch len(args) {
+ case 0:
+ cmd.Println(be.AnonAccess(ctx))
+ case 1:
+ al := access.ParseAccessLevel(args[0])
+ if al < 0 {
+ return fmt.Errorf("invalid access level: %s. Please choose one of the following: %s", args[0], als)
+ }
+ if err := be.SetAnonAccess(ctx, al); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ },
+ },
+ )
+}
@@ -0,0 +1,151 @@
+package shell
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "runtime"
+ "strings"
+
+ "github.com/anmitsu/go-shlex"
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/shell"
+ sshcmd "github.com/charmbracelet/soft-serve/pkg/ssh/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/sshutils"
+ "github.com/charmbracelet/ssh"
+ "github.com/mattn/go-tty"
+ "github.com/muesli/termenv"
+ "github.com/spf13/cobra"
+)
+
+var (
+ commandString string
+
+ // Command is a login shell command.
+ Command = &cobra.Command{
+ Use: "shell",
+ SilenceUsage: true,
+ PersistentPreRunE: cmd.InitBackendContext,
+ PersistentPostRunE: cmd.CloseDBContext,
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ args, err := shlex.Split(commandString, true)
+ if err != nil {
+ return err
+ }
+
+ return runShell(cmd, args)
+ },
+ }
+)
+
+func init() {
+ Command.CompletionOptions.DisableDefaultCmd = true
+ Command.SetUsageTemplate(sshcmd.UsageTemplate)
+ Command.SetUsageFunc(sshcmd.UsageFunc)
+ Command.Flags().StringVarP(&commandString, "", "c", "", "Command to run")
+}
+
+func runShell(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ sshTty, isInteractive := os.LookupEnv("SSH_TTY")
+ sshUserAuth := os.Getenv("SSH_USER_AUTH")
+
+ var ak string
+ if sshUserAuth != "" {
+ f, err := os.Open(sshUserAuth)
+ if err != nil {
+ return fmt.Errorf("could not open SSH_USER_AUTH file: %w", err)
+ }
+
+ ak = parseSSHUserAuth(f)
+ f.Close() // nolint: errcheck
+ }
+
+ if err := os.Setenv("SOFT_SERVE_PUBLIC_KEY", ak); err != nil {
+ return fmt.Errorf("could not set SOFT_SERVE_PUBLIC_KEY: %w", err)
+ }
+
+ var pk ssh.PublicKey
+ var err error
+ if ak != "" {
+ pk, _, err = sshutils.ParseAuthorizedKey(ak)
+ if err != nil {
+ return fmt.Errorf("could not parse authorized key: %w", err)
+ }
+ }
+
+ // We need a public key in the context even if it's nil
+ ctx = context.WithValue(ctx, ssh.ContextKeyPublicKey, pk)
+
+ in, out, er := os.Stdin, os.Stdout, os.Stderr
+ if isInteractive {
+ switch runtime.GOOS {
+ case "windows":
+ tty, err := tty.Open()
+ if err != nil {
+ return fmt.Errorf("could not open tty: %w", err)
+ }
+
+ in = tty.Input()
+ out = tty.Output()
+ er = tty.Output()
+ default:
+ var err error
+ in, err = os.Open(sshTty)
+ if err != nil {
+ return fmt.Errorf("could not open input tty: %w", err)
+ }
+
+ out, err = os.OpenFile(sshTty, os.O_WRONLY, 0)
+ if err != nil {
+ return fmt.Errorf("could not open output tty: %w", err)
+ }
+ er = out
+ }
+ }
+
+ c := shell.Command(ctx, osEnv, isInteractive)
+
+ c.SetArgs(args)
+ c.SetIn(in)
+ c.SetOut(out)
+ c.SetErr(er)
+ c.SetContext(ctx)
+
+ return c.ExecuteContext(ctx)
+}
+
+func parseSSHUserAuth(r io.Reader) string {
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if line == "" {
+ continue
+ }
+
+ if strings.HasPrefix(line, "publickey ") {
+ return strings.TrimPrefix(line, "publickey ")
+ }
+ }
+
+ return ""
+}
+
+var osEnv = &osEnviron{}
+
+type osEnviron struct{}
+
+var _ termenv.Environ = &osEnviron{}
+
+// Environ implements termenv.Environ.
+func (*osEnviron) Environ() []string {
+ return os.Environ()
+}
+
+// Getenv implements termenv.Environ.
+func (*osEnviron) Getenv(key string) string {
+ return os.Getenv(key)
+}
@@ -0,0 +1,57 @@
+package user
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/charmbracelet/soft-serve/pkg/config"
+ "github.com/charmbracelet/soft-serve/pkg/jwk"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/spf13/cobra"
+)
+
+func init() {
+ // cmd is a command that generates a JSON Web Token.
+ cmd := &cobra.Command{
+ Use: "jwt [repository1 repository2...]",
+ Short: "Generate a JSON Web Token",
+ Args: cobra.MinimumNArgs(0),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ cfg := config.FromContext(ctx)
+ kp, err := jwk.NewPair(cfg)
+ if err != nil {
+ return err
+ }
+
+ user := proto.UserFromContext(ctx)
+ if user == nil {
+ return proto.ErrUserNotFound
+ }
+
+ now := time.Now()
+ expiresAt := now.Add(time.Hour)
+ claims := jwt.RegisteredClaims{
+ Subject: fmt.Sprintf("%s#%d", user.Username(), user.ID()),
+ ExpiresAt: jwt.NewNumericDate(expiresAt), // expire in an hour
+ NotBefore: jwt.NewNumericDate(now),
+ IssuedAt: jwt.NewNumericDate(now),
+ Issuer: cfg.HTTP.PublicURL,
+ Audience: args,
+ }
+
+ token := jwt.NewWithClaims(jwk.SigningMethod, claims)
+ token.Header["kid"] = kp.JWK().KeyID
+ j, err := token.SignedString(kp.PrivateKey())
+ if err != nil {
+ return err
+ }
+
+ cmd.Println(j)
+ return nil
+ },
+ }
+
+ Command.AddCommand(cmd)
+}
@@ -0,0 +1,154 @@
+package user
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/caarlos0/duration"
+ "github.com/caarlos0/tablewriter"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/dustin/go-humanize"
+ "github.com/spf13/cobra"
+)
+
+func init() {
+ cmd := &cobra.Command{
+ Use: "token",
+ Aliases: []string{"access-token"},
+ Short: "Manage access tokens",
+ }
+
+ var createExpiresIn string
+ createCmd := &cobra.Command{
+ Use: "create NAME",
+ Short: "Create a new access token",
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ name := strings.Join(args, " ")
+
+ user := proto.UserFromContext(ctx)
+ if user == nil {
+ return proto.ErrUserNotFound
+ }
+
+ var expiresAt time.Time
+ var expiresIn time.Duration
+ if createExpiresIn != "" {
+ d, err := duration.Parse(createExpiresIn)
+ if err != nil {
+ return err
+ }
+
+ expiresIn = d
+ expiresAt = time.Now().Add(d)
+ }
+
+ token, err := be.CreateAccessToken(ctx, user, name, expiresAt)
+ if err != nil {
+ return err
+ }
+
+ notice := "Access token created"
+ if expiresIn != 0 {
+ notice += " (expires in " + humanize.Time(expiresAt) + ")"
+ }
+
+ cmd.PrintErrln(notice)
+ cmd.Println(token)
+
+ return nil
+ },
+ }
+
+ createCmd.Flags().StringVar(&createExpiresIn, "expires-in", "", "Token expiration time (e.g. 1y, 3mo, 2w, 5d4h, 1h30m)")
+
+ listCmd := &cobra.Command{
+ Use: "list",
+ Aliases: []string{"ls"},
+ Short: "List access tokens",
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+
+ user := proto.UserFromContext(ctx)
+ if user == nil {
+ return proto.ErrUserNotFound
+ }
+
+ tokens, err := be.ListAccessTokens(ctx, user)
+ if err != nil {
+ return err
+ }
+
+ if len(tokens) == 0 {
+ cmd.Println("No tokens found")
+ return nil
+ }
+
+ 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)
+ }
+ }
+
+ return []string{
+ strconv.FormatInt(t.ID, 10),
+ t.Name,
+ humanize.Time(t.CreatedAt),
+ expiresAt,
+ }, nil
+ },
+ )
+ },
+ }
+
+ deleteCmd := &cobra.Command{
+ Use: "delete ID",
+ Aliases: []string{"rm", "remove"},
+ Short: "Delete an access token",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+
+ user := proto.UserFromContext(ctx)
+ if user == nil {
+ return proto.ErrUserNotFound
+ }
+
+ id, err := strconv.ParseInt(args[0], 10, 64)
+ if err != nil {
+ return err
+ }
+
+ if err := be.DeleteAccessToken(ctx, user, id); err != nil {
+ return err
+ }
+
+ cmd.PrintErrln("Access token deleted")
+ return nil
+ },
+ }
+
+ cmd.AddCommand(
+ createCmd,
+ listCmd,
+ deleteCmd,
+ )
+
+ Command.AddCommand(cmd)
+}
@@ -0,0 +1,195 @@
+package user
+
+import (
+ "sort"
+ "strings"
+
+ "github.com/charmbracelet/soft-serve/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/backend"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/sshutils"
+ "github.com/spf13/cobra"
+ "golang.org/x/crypto/ssh"
+)
+
+var (
+ // Command returns the user subcommand.
+ Command = &cobra.Command{
+ Use: "user",
+ Aliases: []string{"users"},
+ Short: "Manage users",
+ PersistentPreRunE: cmd.InitBackendContext,
+ PersistentPostRunE: cmd.CloseDBContext,
+ }
+)
+
+func init() {
+ var admin bool
+ var key string
+ userCreateCommand := &cobra.Command{
+ Use: "create USERNAME",
+ Short: "Create a new user",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ var pubkeys []ssh.PublicKey
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ username := args[0]
+ if key != "" {
+ pk, _, err := sshutils.ParseAuthorizedKey(key)
+ if err != nil {
+ return err
+ }
+
+ pubkeys = []ssh.PublicKey{pk}
+ }
+
+ opts := proto.UserOptions{
+ Admin: admin,
+ PublicKeys: pubkeys,
+ }
+
+ _, err := be.CreateUser(ctx, username, opts)
+ return err
+ },
+ }
+
+ userCreateCommand.Flags().BoolVarP(&admin, "admin", "a", false, "make the user an admin")
+ userCreateCommand.Flags().StringVarP(&key, "key", "k", "", "add a public key to the user")
+
+ userDeleteCommand := &cobra.Command{
+ Use: "delete USERNAME",
+ Short: "Delete a user",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ username := args[0]
+
+ return be.DeleteUser(ctx, username)
+ },
+ }
+
+ userListCommand := &cobra.Command{
+ Use: "list",
+ Aliases: []string{"ls"},
+ Short: "List users",
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ users, err := be.Users(ctx)
+ if err != nil {
+ return err
+ }
+
+ sort.Strings(users)
+ for _, u := range users {
+ cmd.Println(u)
+ }
+
+ return nil
+ },
+ }
+
+ userAddPubkeyCommand := &cobra.Command{
+ Use: "add-pubkey USERNAME AUTHORIZED_KEY",
+ Short: "Add a public key to a user",
+ Args: cobra.MinimumNArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ username := args[0]
+ pubkey := strings.Join(args[1:], " ")
+ pk, _, err := sshutils.ParseAuthorizedKey(pubkey)
+ if err != nil {
+ return err
+ }
+
+ return be.AddPublicKey(ctx, username, pk)
+ },
+ }
+
+ userRemovePubkeyCommand := &cobra.Command{
+ Use: "remove-pubkey USERNAME AUTHORIZED_KEY",
+ Short: "Remove a public key from a user",
+ Args: cobra.MinimumNArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ username := args[0]
+ pubkey := strings.Join(args[1:], " ")
+ pk, _, err := sshutils.ParseAuthorizedKey(pubkey)
+ if err != nil {
+ return err
+ }
+
+ return be.RemovePublicKey(ctx, username, pk)
+ },
+ }
+
+ userSetAdminCommand := &cobra.Command{
+ Use: "set-admin USERNAME [true|false]",
+ Short: "Make a user an admin",
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ username := args[0]
+
+ return be.SetAdmin(ctx, username, args[1] == "true")
+ },
+ }
+
+ userInfoCommand := &cobra.Command{
+ Use: "info USERNAME",
+ Short: "Show information about a user",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ username := args[0]
+
+ user, err := be.User(ctx, username)
+ if err != nil {
+ return err
+ }
+
+ isAdmin := user.IsAdmin()
+
+ cmd.Printf("Username: %s\n", user.Username())
+ cmd.Printf("Admin: %t\n", isAdmin)
+ cmd.Printf("Public keys:\n")
+ for _, pk := range user.PublicKeys() {
+ cmd.Printf(" %s\n", sshutils.MarshalAuthorizedKey(pk))
+ }
+
+ return nil
+ },
+ }
+
+ userSetUsernameCommand := &cobra.Command{
+ Use: "set-username USERNAME NEW_USERNAME",
+ Short: "Change a user's username",
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ be := backend.FromContext(ctx)
+ username := args[0]
+ newUsername := args[1]
+
+ return be.SetUsername(ctx, username, newUsername)
+ },
+ }
+
+ Command.AddCommand(
+ userCreateCommand,
+ userAddPubkeyCommand,
+ userInfoCommand,
+ userListCommand,
+ userDeleteCommand,
+ userRemovePubkeyCommand,
+ userSetAdminCommand,
+ userSetUsernameCommand,
+ )
+}
@@ -18,14 +18,16 @@ require (
)
require (
+ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2
+ github.com/aymanbagabas/go-pty v0.2.0
github.com/caarlos0/duration v0.0.0-20220103233809-8df7c22fe305
github.com/caarlos0/env/v10 v10.0.0
github.com/caarlos0/tablewriter v0.1.0
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20231027181609-f7ff6baf2ed0
github.com/charmbracelet/keygen v0.5.0
github.com/charmbracelet/log v0.3.1
- github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09
+ github.com/charmbracelet/ssh v0.0.0-20231129225614-669b249ca6ee
github.com/go-jose/go-jose/v3 v3.0.1
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v5 v5.1.0
@@ -37,6 +39,7 @@ require (
github.com/jmoiron/sqlx v1.3.5
github.com/lib/pq v1.10.9
github.com/lrstanley/bubblezone v0.0.0-20220716194435-3cb8c52f6a8f
+ github.com/mattn/go-tty v0.0.3
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/roff v0.1.0
github.com/prometheus/client_golang v1.17.0
@@ -52,13 +55,13 @@ require (
)
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.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
+ github.com/creack/pty v1.1.15 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
@@ -87,6 +90,7 @@ require (
github.com/rivo/uniseg v0.4.3 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
+ github.com/u-root/u-root v0.11.0 // indirect
github.com/yuin/goldmark v1.5.2 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
@@ -1,7 +1,14 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
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=
@@ -9,16 +16,25 @@ github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2/go.mod h
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-pty v0.2.0 h1:fC2ATGqFG+zH0Hr0Ks46h0lrRU2ExmQxR7iHUjBG1Do=
+github.com/aymanbagabas/go-pty v0.2.0/go.mod h1:Ul2Zd4Z3Cby9ByGdurtjlAafh6pjgrjiHNAsx4LC5yE=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/caarlos0/duration v0.0.0-20220103233809-8df7c22fe305 h1:vJpZ14MU1/YhqsAyMst/70MHqRgCkPsIwZNoSgTm2Dc=
github.com/caarlos0/duration v0.0.0-20220103233809-8df7c22fe305/go.mod h1:mSkwb/eZEwOJJJ4tqAKiuhLIPe0e9+FKhlU0oMCpbf8=
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
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/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
@@ -35,70 +51,161 @@ github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=
github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
-github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09 h1:ZDIQmTtohv0S/AAYE//w8mYTxCzqphhF1+4ACPDMiLU=
-github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09/go.mod h1:F1vgddWsb/Yr/OZilFeRZEh5sE/qU0Dt1mKkmke6Zvg=
+github.com/charmbracelet/ssh v0.0.0-20231129225614-669b249ca6ee h1:r0yd6qZE8iLT2smnxiYre41b9/atj4kFxsfuIXW0Oqs=
+github.com/charmbracelet/ssh v0.0.0-20231129225614-669b249ca6ee/go.mod h1:Lq6b5f587hn2S27yNPgSB4Nei+iqOkghe9Ep7syGuQY=
github.com/charmbracelet/wish v1.2.0 h1:h5Wj9pr97IQz/l4gM5Xep2lXcY/YM+6O2RC2o3x0JIQ=
github.com/charmbracelet/wish v1.2.0/go.mod h1:JX3fC+178xadJYAhPu6qWtVDpJTwpnFvpdjz9RKJlUE=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
+github.com/creack/pty v1.1.15/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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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/gliderlabs/ssh v0.1.2-0.20181113160402-cbabf5414432/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
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.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk=
github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
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.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gojuno/minimock/v3 v3.0.4/go.mod h1:HqeqnwV8mAABn3pO5hqF+RE7gjA0jsN8cbbSogoGrzI=
+github.com/gojuno/minimock/v3 v3.0.8/go.mod h1:TPKxc8tiB8O83YH2//pOzxvEjaI3TMhd6ev/GmlMiYA=
github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU=
github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
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/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI=
+github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw=
+github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4=
+github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
+github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
+github.com/google/goexpect v0.0.0-20191001010744-5b6988669ffa/go.mod h1:qtE5aAEkt0vOSA84DBh8aJsz6riL8ONfqfULY7lBjqc=
+github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hexdigest/gowrap v1.1.7/go.mod h1:Z+nBFUDLa01iaNM+/jzoOA1JJ7sm51rnYFauKFUB5fs=
+github.com/hexdigest/gowrap v1.1.8/go.mod h1:H/JiFmQMp//tedlV8qt2xBdGzmne6bpbaSuiHmygnMw=
+github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
+github.com/intel-go/cpuid v0.0.0-20200819041909-2aa72927c3e2/go.mod h1:RmeVYf9XrPRbRc3XIx0gLYA8qOFvNoPOfaEZduRlEp4=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
+github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
+github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
+github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kaey/framebuffer v0.0.0-20140402104929-7b385489a1ff/go.mod h1:tS4qtlcKqtt3tCIHUflVSqeP3CLH5Qtv2szX9X2SyhU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.6/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -107,13 +214,21 @@ github.com/lrstanley/bubblezone v0.0.0-20220716194435-3cb8c52f6a8f h1:FjWlbnOxKS
github.com/lrstanley/bubblezone v0.0.0-20220716194435-3cb8c52f6a8f/go.mod h1:CxaUrg7Y6DmnquTpb1Rgxib+u+NcRxrDi8m/mR1poTM=
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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
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-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -121,12 +236,26 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
+github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
+github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk=
github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
+github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
+github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
+github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
+github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
+github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
+github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
+github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -144,21 +273,47 @@ github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nanmu42/limitio v1.0.0/go.mod h1:8H40zQ7pqxzbwZ9jxsK2hDoE06TH5ziybtApt1io8So=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/orangecms/go-framebuffer v0.0.0-20200613202404-a0700d90c330/go.mod h1:3Myb/UszJY32F2G7yGkUtcW/ejHpjlGfYLim7cv2uKA=
+github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rck/unit v0.0.3/go.mod h1:jTOnzP4s1OjIP1vdxb4n76b23QPKS4EurYg7sYMr2DM=
+github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc/go.mod h1:scrOqOnnHVKCHENvFw8k9ajCb88uqLQDA4BvuJNJ2ew=
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=
@@ -167,23 +322,45 @@ github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/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/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 h1:mncRSDOqYCng7jOD+Y6+IivdRI6Kzv2BLWYkWkdQfu0=
github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086/go.mod h1:YpdgDXpumPB/+EGmGTYHeiW/0QVFRzBYTNFaxWfPDk4=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -191,71 +368,228 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/twitchtv/twirp v5.8.0+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
+github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
+github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90/go.mod h1:lYt+LVfZBBwDZ3+PHk4k/c/TnKOkjJXiJO73E32Mmpc=
+github.com/u-root/iscsinl v0.1.1-0.20210528121423-84c32645822a/go.mod h1:RWIgJWqm9/0gjBZ0Hl8iR6MVGzZ+yAda2uqqLmetE2I=
+github.com/u-root/prompt v0.0.0-20221110083427-a2ad3c8339a8/go.mod h1:LyU/Wj6OFmFnGUAR/8mzI5PjxxvbcIfDmZqKupSplG0=
+github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
+github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
+github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
+github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
+github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810/go.mod h1:dF0BBJ2YrV1+2eAIyEI+KeSidgA6HqoIP1u5XTlMq/o=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
+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.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/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.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.11-0.20220322213029-87a8611856c1/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
+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.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@@ -280,3 +614,7 @@ modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
+mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0=
+mvdan.cc/sh/v3 v3.4.1/go.mod h1:p/tqPPI4Epfk2rICAe2RoaNd8HBSJ8t9Y2DA9yQlbzY=
+pack.ag/tftp v1.0.1-0.20181129014014-07909dfbde3c/go.mod h1:N1Pyo5YG+K90XHoR2vfLPhpRuE8ziqbgMn/r/SghZas=
+src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f/go.mod h1:kPbhv5+fBeUh85nET3wWhHGUaUQ64nZMJ8FwA5v5Olg=
@@ -0,0 +1,96 @@
+package shell
+
+import (
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "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"
+ "github.com/muesli/termenv"
+)
+
+// var tuiSessionCounter = promauto.NewCounterVec(prometheus.CounterOpts{
+// Namespace: "soft_serve",
+// Subsystem: "ssh",
+// Name: "tui_session_total",
+// Help: "The total number of TUI sessions",
+// }, []string{"repo", "term"})
+//
+// var tuiSessionDuration = promauto.NewCounterVec(prometheus.CounterOpts{
+// Namespace: "soft_serve",
+// Subsystem: "ssh",
+// Name: "tui_session_seconds_total",
+// Help: "The total number of TUI sessions",
+// }, []string{"repo", "term"})
+
+// SessionHandler is the soft-serve bubbletea ssh session handler.
+// This middleware must be run after the ContextMiddleware.
+func SessionHandler(s ssh.Session) *tea.Program {
+ pty, _, active := s.Pty()
+ if !active {
+ return nil
+ }
+
+ ctx := s.Context()
+ be := backend.FromContext(ctx)
+ cfg := config.FromContext(ctx)
+ cmd := s.Command()
+ initialRepo := ""
+ if len(cmd) == 1 {
+ initialRepo = cmd[0]
+ auth := be.AccessLevelByPublicKey(ctx, initialRepo, s.PublicKey())
+ if auth < access.ReadOnlyAccess {
+ wish.Fatalln(s, proto.ErrUnauthorized)
+ return nil
+ }
+ }
+
+ envs := &sessionEnv{s}
+ output := termenv.NewOutput(s, termenv.WithColorCache(true), termenv.WithEnvironment(envs))
+ c := common.NewCommon(ctx, output, pty.Window.Width, pty.Window.Height)
+ c.SetValue(common.ConfigKey, cfg)
+ m := NewUI(c, initialRepo)
+ p := tea.NewProgram(m,
+ tea.WithInput(s),
+ tea.WithOutput(s),
+ tea.WithAltScreen(),
+ tea.WithoutCatchPanics(),
+ tea.WithMouseCellMotion(),
+ tea.WithContext(ctx),
+ )
+
+ // tuiSessionCounter.WithLabelValues(initialRepo, pty.Term).Inc()
+ //
+ // start := time.Now()
+ // go func() {
+ // <-ctx.Done()
+ // tuiSessionDuration.WithLabelValues(initialRepo, pty.Term).Add(time.Since(start).Seconds())
+ // }()
+
+ return p
+}
+
+var _ termenv.Environ = &sessionEnv{}
+
+type sessionEnv struct {
+ ssh.Session
+}
+
+func (s *sessionEnv) Environ() []string {
+ pty, _, _ := s.Pty()
+ return append(s.Session.Environ(), "TERM="+pty.Term)
+}
+
+func (s *sessionEnv) Getenv(key string) string {
+ for _, env := range s.Environ() {
+ if strings.HasPrefix(env, key+"=") {
+ return strings.TrimPrefix(env, key+"=")
+ }
+ }
+ return ""
+}
@@ -0,0 +1,185 @@
+package shell
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/log"
+ "github.com/charmbracelet/soft-serve/pkg/config"
+ "github.com/charmbracelet/soft-serve/pkg/ssh/cmd"
+ "github.com/charmbracelet/soft-serve/pkg/sshutils"
+ "github.com/charmbracelet/soft-serve/pkg/ui/common"
+ "github.com/charmbracelet/ssh"
+ "github.com/muesli/termenv"
+ "github.com/spf13/cobra"
+)
+
+// Command returns a new shell command.
+func Command(ctx context.Context, env termenv.Environ, isInteractive bool) *cobra.Command {
+ cfg := config.FromContext(ctx)
+ c := &cobra.Command{
+ Short: "Soft Serve is a self-hostable Git server for the command line.",
+ SilenceUsage: true,
+ SilenceErrors: true,
+ TraverseChildren: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ in := cmd.InOrStdin()
+ out := cmd.OutOrStdout()
+ if isInteractive && len(args) == 0 {
+ // Run UI
+ output := termenv.NewOutput(out, termenv.WithColorCache(true), termenv.WithEnvironment(env))
+ c := common.NewCommon(ctx, output, 0, 0)
+ c.SetValue(common.ConfigKey, cfg)
+ m := NewUI(c, "")
+ p := tea.NewProgram(m,
+ tea.WithInput(in),
+ tea.WithOutput(out),
+ tea.WithAltScreen(),
+ tea.WithoutCatchPanics(),
+ tea.WithMouseCellMotion(),
+ tea.WithContext(ctx),
+ )
+
+ return startProgram(cmd.Context(), p)
+ } else if len(args) > 0 {
+ // Run custom command
+ return startCommand(cmd, args)
+ }
+
+ return fmt.Errorf("invalid command %v", args)
+ },
+ }
+ c.CompletionOptions.DisableDefaultCmd = true
+
+ c.SetUsageTemplate(cmd.UsageTemplate)
+ c.SetUsageFunc(cmd.UsageFunc)
+ c.AddCommand(
+ cmd.GitUploadPackCommand(),
+ cmd.GitUploadArchiveCommand(),
+ cmd.GitReceivePackCommand(),
+ // TODO: write shell commands for these
+ // cmd.RepoCommand(),
+ // cmd.SettingsCommand(),
+ // cmd.UserCommand(),
+ // cmd.InfoCommand(),
+ // cmd.PubkeyCommand(),
+ // cmd.SetUsernameCommand(),
+ // cmd.JWTCommand(),
+ // cmd.TokenCommand(),
+ )
+
+ if cfg.LFS.Enabled {
+ c.AddCommand(
+ cmd.GitLFSAuthenticateCommand(),
+ )
+
+ if cfg.LFS.SSHEnabled {
+ c.AddCommand(
+ cmd.GitLFSTransfer(),
+ )
+ }
+ }
+
+ c.SetContext(ctx)
+
+ return c
+}
+
+func startProgram(ctx context.Context, p *tea.Program) (err error) {
+ var windowChanges <-chan ssh.Window
+ if s := sshutils.SessionFromContext(ctx); s != nil {
+ _, windowChanges, _ = s.Pty()
+ }
+ ctx, cancel := context.WithCancel(ctx)
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ if p != nil {
+ p.Quit()
+ return
+ }
+ case w := <-windowChanges:
+ if p != nil {
+ p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
+ }
+ }
+ }
+ }()
+
+ _, err = p.Run()
+
+ // p.Kill() will force kill the program if it's still running,
+ // and restore the terminal to its original state in case of a
+ // tui crash
+ p.Kill()
+ cancel()
+
+ return
+}
+
+func startCommand(co *cobra.Command, args []string) error {
+ ctx := co.Context()
+ cfg := config.FromContext(ctx)
+ if len(args) == 0 {
+ return fmt.Errorf("no command specified")
+ }
+
+ cmdsDir := filepath.Join(cfg.DataPath, "commands")
+
+ var cmdArgs []string
+ if len(args) > 1 {
+ cmdArgs = args[1:]
+ }
+
+ cmdPath := filepath.Join(cmdsDir, args[0])
+
+ if stat, err := os.Stat(cmdPath); errors.Is(err, fs.ErrNotExist) || stat.Mode()&0111 == 0 {
+ return fmt.Errorf("command not found: %s", args[0])
+ }
+
+ cmdPath, err := filepath.Abs(cmdPath)
+ if err != nil {
+ return fmt.Errorf("could not get absolute path for command: %w", err)
+ }
+
+ cmd := exec.CommandContext(ctx, cmdPath, cmdArgs...)
+
+ cmd.Dir = cmdsDir
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return fmt.Errorf("could not get stdin pipe: %w", err)
+ }
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return fmt.Errorf("could not get stdout pipe: %w", err)
+ }
+
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return fmt.Errorf("could not get stderr pipe: %w", err)
+ }
+
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("could not start command: %w", err)
+ }
+
+ go io.Copy(stdin, co.InOrStdin()) // nolint: errcheck
+ go io.Copy(co.OutOrStdout(), stdout) // nolint: errcheck
+ go io.Copy(co.ErrOrStderr(), stderr) // nolint: errcheck
+
+ log.Infof("waiting for command to finish: %s", cmdPath)
+ if err := cmd.Wait(); err != nil {
+ return err
+ }
+
+ return nil
+}
@@ -0,0 +1,332 @@
+package shell
+
+import (
+ "errors"
+
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/list"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/charmbracelet/soft-serve/pkg/ui/common"
+ "github.com/charmbracelet/soft-serve/pkg/ui/components/footer"
+ "github.com/charmbracelet/soft-serve/pkg/ui/components/header"
+ "github.com/charmbracelet/soft-serve/pkg/ui/components/selector"
+ "github.com/charmbracelet/soft-serve/pkg/ui/pages/repo"
+ "github.com/charmbracelet/soft-serve/pkg/ui/pages/selection"
+)
+
+type page int
+
+const (
+ selectionPage page = iota
+ repoPage
+)
+
+type sessionState int
+
+const (
+ loadingState sessionState = iota
+ errorState
+ readyState
+)
+
+// UI is the main UI model.
+type UI struct {
+ serverName string
+ initialRepo string
+ common common.Common
+ pages []common.Component
+ activePage page
+ state sessionState
+ header *header.Header
+ footer *footer.Footer
+ showFooter bool
+ error error
+}
+
+// NewUI returns a new UI model.
+func NewUI(c common.Common, initialRepo string) *UI {
+ serverName := c.Config().Name
+ h := header.New(c, serverName)
+ ui := &UI{
+ serverName: serverName,
+ common: c,
+ pages: make([]common.Component, 2), // selection & repo
+ activePage: selectionPage,
+ state: loadingState,
+ header: h,
+ initialRepo: initialRepo,
+ showFooter: true,
+ }
+ ui.footer = footer.New(c, ui)
+ return ui
+}
+
+func (ui *UI) getMargins() (wm, hm int) {
+ style := ui.common.Styles.App.Copy()
+ switch ui.activePage {
+ case selectionPage:
+ hm += ui.common.Styles.ServerName.GetHeight() +
+ ui.common.Styles.ServerName.GetVerticalFrameSize()
+ case repoPage:
+ }
+ wm += style.GetHorizontalFrameSize()
+ hm += style.GetVerticalFrameSize()
+ if ui.showFooter {
+ // NOTE: we don't use the footer's style to determine the margins
+ // because footer.Height() is the height of the footer after applying
+ // the styles.
+ hm += ui.footer.Height()
+ }
+ return
+}
+
+// ShortHelp implements help.KeyMap.
+func (ui *UI) ShortHelp() []key.Binding {
+ b := make([]key.Binding, 0)
+ switch ui.state {
+ case errorState:
+ b = append(b, ui.common.KeyMap.Back)
+ case readyState:
+ b = append(b, ui.pages[ui.activePage].ShortHelp()...)
+ }
+ if !ui.IsFiltering() {
+ b = append(b, ui.common.KeyMap.Quit)
+ }
+ b = append(b, ui.common.KeyMap.Help)
+ return b
+}
+
+// FullHelp implements help.KeyMap.
+func (ui *UI) FullHelp() [][]key.Binding {
+ b := make([][]key.Binding, 0)
+ switch ui.state {
+ case errorState:
+ b = append(b, []key.Binding{ui.common.KeyMap.Back})
+ case readyState:
+ b = append(b, ui.pages[ui.activePage].FullHelp()...)
+ }
+ h := []key.Binding{
+ ui.common.KeyMap.Help,
+ }
+ if !ui.IsFiltering() {
+ h = append(h, ui.common.KeyMap.Quit)
+ }
+ b = append(b, h)
+ return b
+}
+
+// SetSize implements common.Component.
+func (ui *UI) SetSize(width, height int) {
+ ui.common.SetSize(width, height)
+ wm, hm := ui.getMargins()
+ ui.header.SetSize(width-wm, height-hm)
+ ui.footer.SetSize(width-wm, height-hm)
+ for _, p := range ui.pages {
+ if p != nil {
+ p.SetSize(width-wm, height-hm)
+ }
+ }
+}
+
+// Init implements tea.Model.
+func (ui *UI) Init() tea.Cmd {
+ ui.pages[selectionPage] = selection.New(ui.common)
+ ui.pages[repoPage] = repo.New(ui.common,
+ repo.NewReadme(ui.common),
+ repo.NewFiles(ui.common),
+ repo.NewLog(ui.common),
+ repo.NewRefs(ui.common, git.RefsHeads),
+ repo.NewRefs(ui.common, git.RefsTags),
+ )
+ ui.SetSize(ui.common.Width, ui.common.Height)
+ cmds := make([]tea.Cmd, 0)
+ cmds = append(cmds,
+ ui.pages[selectionPage].Init(),
+ ui.pages[repoPage].Init(),
+ )
+ if ui.initialRepo != "" {
+ cmds = append(cmds, ui.initialRepoCmd(ui.initialRepo))
+ }
+ ui.state = readyState
+ ui.SetSize(ui.common.Width, ui.common.Height)
+ return tea.Batch(cmds...)
+}
+
+// IsFiltering returns true if the selection page is filtering.
+func (ui *UI) IsFiltering() bool {
+ if ui.activePage == selectionPage {
+ if s, ok := ui.pages[selectionPage].(*selection.Selection); ok && s.FilterState() == list.Filtering {
+ return true
+ }
+ }
+ return false
+}
+
+// Update implements tea.Model.
+func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ ui.common.Logger.Debugf("msg received: %T", msg)
+ cmds := make([]tea.Cmd, 0)
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ ui.SetSize(msg.Width, msg.Height)
+ for i, p := range ui.pages {
+ m, cmd := p.Update(msg)
+ ui.pages[i] = m.(common.Component)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+ case tea.KeyMsg, tea.MouseMsg:
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ 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 && key.Matches(msg, ui.common.KeyMap.Back):
+ ui.activePage = selectionPage
+ // Always show the footer on selection page.
+ ui.showFooter = true
+ }
+ case tea.MouseMsg:
+ switch msg.Type {
+ case tea.MouseLeft:
+ switch {
+ case ui.common.Zone.Get("footer").InBounds(msg):
+ cmds = append(cmds, footer.ToggleFooterCmd)
+ }
+ }
+ }
+ case footer.ToggleFooterMsg:
+ ui.footer.SetShowAll(!ui.footer.ShowAll())
+ // Show the footer when on repo page and shot all help.
+ if ui.error == nil && ui.activePage == repoPage {
+ ui.showFooter = !ui.showFooter
+ }
+ case repo.RepoMsg:
+ ui.common.SetValue(common.RepoKey, msg)
+ ui.activePage = repoPage
+ // Show the footer on repo page if show all is set.
+ ui.showFooter = ui.footer.ShowAll()
+ cmds = append(cmds, repo.UpdateRefCmd(msg))
+ case common.ErrorMsg:
+ ui.error = msg
+ ui.state = errorState
+ ui.showFooter = true
+ case selector.SelectMsg:
+ switch msg.IdentifiableItem.(type) {
+ case selection.Item:
+ if ui.activePage == selectionPage {
+ cmds = append(cmds, ui.setRepoCmd(msg.ID()))
+ }
+ }
+ }
+ h, cmd := ui.header.Update(msg)
+ ui.header = h.(*header.Header)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ f, cmd := ui.footer.Update(msg)
+ ui.footer = f.(*footer.Footer)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ if ui.state != loadingState {
+ m, cmd := ui.pages[ui.activePage].Update(msg)
+ ui.pages[ui.activePage] = m.(common.Component)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+ // This fixes determining the height margin of the footer.
+ ui.SetSize(ui.common.Width, ui.common.Height)
+ return ui, tea.Batch(cmds...)
+}
+
+// View implements tea.Model.
+func (ui *UI) View() string {
+ var view string
+ wm, hm := ui.getMargins()
+ switch ui.state {
+ case loadingState:
+ view = "Loading..."
+ case errorState:
+ err := ui.common.Styles.ErrorTitle.Render("Bummer")
+ err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
+ view = ui.common.Styles.Error.Copy().
+ Width(ui.common.Width -
+ wm -
+ ui.common.Styles.ErrorBody.GetHorizontalFrameSize()).
+ Height(ui.common.Height -
+ hm -
+ ui.common.Styles.Error.GetVerticalFrameSize()).
+ Render(err)
+ case readyState:
+ view = ui.pages[ui.activePage].View()
+ default:
+ view = "Unknown state :/ this is a bug!"
+ }
+ if ui.activePage == selectionPage {
+ view = lipgloss.JoinVertical(lipgloss.Top, ui.header.View(), view)
+ }
+ if ui.showFooter {
+ view = lipgloss.JoinVertical(lipgloss.Top, view, ui.footer.View())
+ }
+ return ui.common.Zone.Scan(
+ ui.common.Styles.App.Render(view),
+ )
+}
+
+func (ui *UI) openRepo(rn string) (proto.Repository, error) {
+ cfg := ui.common.Config()
+ if cfg == nil {
+ return nil, errors.New("config is nil")
+ }
+
+ ctx := ui.common.Context()
+ be := ui.common.Backend()
+ repos, err := be.Repositories(ctx)
+ if err != nil {
+ ui.common.Logger.Debugf("ui: failed to list repos: %v", err)
+ return nil, err
+ }
+ for _, r := range repos {
+ if r.Name() == rn {
+ return r, nil
+ }
+ }
+ return nil, common.ErrMissingRepo
+}
+
+func (ui *UI) setRepoCmd(rn string) tea.Cmd {
+ return func() tea.Msg {
+ r, err := ui.openRepo(rn)
+ if err != nil {
+ return common.ErrorMsg(err)
+ }
+ return repo.RepoMsg(r)
+ }
+}
+
+func (ui *UI) initialRepoCmd(rn string) tea.Cmd {
+ return func() tea.Msg {
+ r, err := ui.openRepo(rn)
+ if err != nil {
+ return nil
+ }
+ return repo.RepoMsg(r)
+ }
+}
@@ -203,8 +203,8 @@ func gitRunE(cmd *cobra.Command, args []string) error {
}
// Add ssh session & config environ
- s := sshutils.SessionFromContext(ctx)
- envs = append(envs, s.Environ()...)
+ // s := sshutils.SessionFromContext(ctx)
+ // envs = append(envs, s.Environ()...)
envs = append(envs, cfg.Environ()...)
repoPath := filepath.Join(reposDir, repoDir)
@@ -167,11 +167,11 @@ func LoggingMiddleware(sh ssh.Handler) ssh.Handler {
}
if isPty {
- logArgs = []interface{}{
+ logArgs = append([]interface{}{
"term", ptyReq.Term,
"width", ptyReq.Window.Width,
"height", ptyReq.Window.Height,
- }
+ }, logArgs...)
}
if config.IsVerbose() {
@@ -0,0 +1,14 @@
+//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !windows
+// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!windows
+
+package ssh
+
+import (
+ "os"
+
+ "github.com/aymanbagabas/go-pty"
+)
+
+func ptyNew(p pty.Pty) (in *os.File, out *os.File, er *os.File, err error) { // nolint
+ return nil, nil, nil, pty.ErrUnsupported
+}
@@ -0,0 +1,18 @@
+//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package ssh
+
+import (
+ "os"
+
+ "github.com/aymanbagabas/go-pty"
+)
+
+func ptyNew(p pty.Pty) (in *os.File, out *os.File, er *os.File, err error) { // nolint
+ tty := p.(pty.UnixPty)
+ in = tty.Slave()
+ out = tty.Slave()
+ er = tty.Slave()
+ return
+}
@@ -0,0 +1,18 @@
+//go:build windows
+// +build windows
+
+package ssh
+
+import (
+ "os"
+
+ "github.com/aymanbagabas/go-pty"
+)
+
+func ptyNew(p pty.Pty) (in *os.File, out *os.File, er *os.File, err error) { // nolint
+ tty := p.(pty.ConPty)
+ in = tty.InputPipe()
+ out = tty.OutputPipe()
+ er = tty.OutputPipe()
+ return
+}
@@ -0,0 +1,59 @@
+package ssh
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/charmbracelet/log"
+ "github.com/charmbracelet/soft-serve/pkg/shell"
+ "github.com/charmbracelet/ssh"
+ "github.com/charmbracelet/wish"
+)
+
+// ShellMiddleware is a middleware for the SSH shell.
+func ShellMiddleware(sh ssh.Handler) ssh.Handler {
+ return func(s ssh.Session) {
+ ctx := s.Context()
+ logger := log.FromContext(ctx).WithPrefix("ssh")
+ envs := &sessionEnv{s}
+
+ ppty, _, isInteractive := s.Pty()
+
+ var (
+ in io.Reader = s
+ out io.Writer = s
+ er io.Writer = s.Stderr()
+ err error
+ )
+
+ if isInteractive {
+ in, out, er, err = ptyNew(ppty.Pty)
+ if err != nil {
+ logger.Errorf("could not create pty: %v", err)
+ // TODO: replace this err with a declared error
+ wish.Fatalln(s, fmt.Errorf("internal server error"))
+ return
+ }
+ }
+
+ args := s.Command()
+ if len(args) == 0 {
+ // XXX: args cannot be nil, otherwise cobra will use os.Args[1:]
+ args = []string{}
+ }
+
+ cmd := shell.Command(ctx, envs, isInteractive)
+ cmd.SetArgs(args)
+ cmd.SetIn(in)
+ cmd.SetOut(out)
+ cmd.SetErr(er)
+ cmd.SetContext(ctx)
+
+ if err := cmd.ExecuteContext(ctx); err != nil {
+ wish.Fatalln(s, err)
+ return
+ }
+
+ sh(s)
+ }
+}
@@ -17,9 +17,7 @@ import (
"github.com/charmbracelet/soft-serve/pkg/store"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
- bm "github.com/charmbracelet/wish/bubbletea"
rm "github.com/charmbracelet/wish/recover"
- "github.com/muesli/termenv"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
gossh "golang.org/x/crypto/ssh"
@@ -70,9 +68,10 @@ func NewSSHServer(ctx context.Context) (*SSHServer, error) {
rm.MiddlewareWithLogger(
logger,
// BubbleTea middleware.
- bm.MiddlewareWithProgramHandler(SessionHandler, termenv.ANSI256),
+ // bm.MiddlewareWithProgramHandler(SessionHandler, termenv.ANSI256),
// CLI middleware.
- CommandMiddleware,
+ // CommandMiddleware,
+ ShellMiddleware,
// Logging middleware.
LoggingMiddleware,
// Context middleware.
@@ -86,6 +85,7 @@ func NewSSHServer(ctx context.Context) (*SSHServer, error) {
}
s.srv, err = wish.NewServer(
+ ssh.AllocatePty(),
ssh.PublicKeyAuth(s.PublicKeyHandler),
ssh.KeyboardInteractiveAuth(s.KeyboardInteractiveHandler),
wish.WithAddress(cfg.SSH.ListenAddr),