1package shell
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "os/exec"
8 "path/filepath"
9
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/log"
12 "github.com/charmbracelet/soft-serve/pkg/config"
13 "github.com/charmbracelet/soft-serve/pkg/ssh/cmd"
14 "github.com/charmbracelet/soft-serve/pkg/sshutils"
15 "github.com/charmbracelet/soft-serve/pkg/ui/common"
16 "github.com/charmbracelet/ssh"
17 "github.com/muesli/termenv"
18 "github.com/spf13/cobra"
19)
20
21// Command returns a new shell command.
22func Command(ctx context.Context, env termenv.Environ, isInteractive bool) *cobra.Command {
23 cfg := config.FromContext(ctx)
24 c := &cobra.Command{
25 Short: "Soft Serve is a self-hostable Git server for the command line.",
26 SilenceUsage: true,
27 SilenceErrors: true,
28 TraverseChildren: true,
29 RunE: func(cmd *cobra.Command, args []string) error {
30 in := cmd.InOrStdin()
31 out := cmd.OutOrStdout()
32 if isInteractive && len(args) == 0 {
33 // Run UI
34 output := termenv.NewOutput(out, termenv.WithColorCache(true), termenv.WithEnvironment(env))
35 c := common.NewCommon(ctx, output, 0, 0)
36 c.SetValue(common.ConfigKey, cfg)
37 m := NewUI(c, "")
38 p := tea.NewProgram(m,
39 tea.WithInput(in),
40 tea.WithOutput(out),
41 tea.WithAltScreen(),
42 tea.WithoutCatchPanics(),
43 tea.WithMouseCellMotion(),
44 tea.WithContext(ctx),
45 )
46
47 return startProgram(cmd.Context(), p)
48 } else if len(args) > 0 {
49 // Run custom command
50 return startCommand(cmd, args)
51 }
52
53 return fmt.Errorf("invalid command %v", args)
54 },
55 }
56 c.CompletionOptions.DisableDefaultCmd = true
57
58 c.SetUsageTemplate(cmd.UsageTemplate)
59 c.SetUsageFunc(cmd.UsageFunc)
60 c.AddCommand(
61 cmd.GitUploadPackCommand(),
62 cmd.GitUploadArchiveCommand(),
63 cmd.GitReceivePackCommand(),
64 // TODO: write shell commands for these
65 // cmd.RepoCommand(),
66 // cmd.SettingsCommand(),
67 // cmd.UserCommand(),
68 // cmd.InfoCommand(),
69 // cmd.PubkeyCommand(),
70 // cmd.SetUsernameCommand(),
71 // cmd.JWTCommand(),
72 // cmd.TokenCommand(),
73 )
74
75 if cfg.LFS.Enabled {
76 c.AddCommand(
77 cmd.GitLFSAuthenticateCommand(),
78 )
79
80 if cfg.LFS.SSHEnabled {
81 c.AddCommand(
82 cmd.GitLFSTransfer(),
83 )
84 }
85 }
86
87 c.SetContext(ctx)
88
89 return c
90}
91
92func startProgram(ctx context.Context, p *tea.Program) (err error) {
93 var windowChanges <-chan ssh.Window
94 if s := sshutils.SessionFromContext(ctx); s != nil {
95 _, windowChanges, _ = s.Pty()
96 }
97 ctx, cancel := context.WithCancel(ctx)
98 go func() {
99 for {
100 select {
101 case <-ctx.Done():
102 if p != nil {
103 p.Quit()
104 return
105 }
106 case w := <-windowChanges:
107 if p != nil {
108 p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
109 }
110 }
111 }
112 }()
113
114 _, err = p.Run()
115
116 // p.Kill() will force kill the program if it's still running,
117 // and restore the terminal to its original state in case of a
118 // tui crash
119 p.Kill()
120 cancel()
121
122 return
123}
124
125func startCommand(co *cobra.Command, args []string) error {
126 ctx := co.Context()
127 cfg := config.FromContext(ctx)
128 if len(args) == 0 {
129 return fmt.Errorf("no command specified")
130 }
131
132 cmdsDir := filepath.Join(cfg.DataPath, "commands")
133
134 var cmdArgs []string
135 if len(args) > 1 {
136 cmdArgs = args[1:]
137 }
138
139 cmdPath := filepath.Join(cmdsDir, args[0])
140
141 // if stat, err := os.Stat(cmdPath); errors.Is(err, fs.ErrNotExist) || stat.Mode()&0111 == 0 {
142 // log.Printf("command mode %s", stat.Mode().String())
143 // return fmt.Errorf("command not found: %s", args[0])
144 // }
145
146 cmdPath, err := filepath.Abs(cmdPath)
147 if err != nil {
148 return fmt.Errorf("could not get absolute path for command: %w", err)
149 }
150
151 cmd := exec.CommandContext(ctx, cmdPath, cmdArgs...)
152
153 cmd.Dir = cmdsDir
154 stdin, err := cmd.StdinPipe()
155 if err != nil {
156 return fmt.Errorf("could not get stdin pipe: %w", err)
157 }
158
159 stdout, err := cmd.StdoutPipe()
160 if err != nil {
161 return fmt.Errorf("could not get stdout pipe: %w", err)
162 }
163
164 stderr, err := cmd.StderrPipe()
165 if err != nil {
166 return fmt.Errorf("could not get stderr pipe: %w", err)
167 }
168
169 if err := cmd.Start(); err != nil {
170 return fmt.Errorf("could not start command: %w", err)
171 }
172
173 go io.Copy(stdin, co.InOrStdin()) // nolint: errcheck
174 go io.Copy(co.OutOrStdout(), stdout) // nolint: errcheck
175 go io.Copy(co.ErrOrStderr(), stderr) // nolint: errcheck
176
177 log.Infof("waiting for command to finish: %s", cmdPath)
178 if err := cmd.Wait(); err != nil {
179 return err
180 }
181
182 return nil
183}