1package cli
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
8 "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
9 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
10
11 "github.com/spf13/cobra"
12)
13
14// newRemotesCmd creates the remotes command group.
15func newRemotesCmd() *cobra.Command {
16 cmd := &cobra.Command{
17 Use: "remotes",
18 Short: "Manage saved remote Gordon instances",
19 Long: `Manage saved remote Gordon instances.
20
21Remotes allow you to save frequently used remote Gordon instances
22and quickly switch between them.
23
24Configuration is stored in ~/.config/gordon/remotes.toml`,
25 }
26
27 cmd.AddCommand(newRemotesListCmd())
28 cmd.AddCommand(newRemotesAddCmd())
29 cmd.AddCommand(newRemotesRemoveCmd())
30 cmd.AddCommand(newRemotesUseCmd())
31 cmd.AddCommand(newRemotesSetTokenCmd())
32
33 return cmd
34}
35
36// newRemotesListCmd creates the remotes list command.
37func newRemotesListCmd() *cobra.Command {
38 return &cobra.Command{
39 Use: "list",
40 Short: "List saved remotes",
41 RunE: func(cmd *cobra.Command, args []string) error {
42 remotes, active, err := remote.ListRemotes()
43 if err != nil {
44 return fmt.Errorf("failed to list remotes: %w", err)
45 }
46
47 if len(remotes) == 0 {
48 fmt.Println(styles.Theme.Muted.Render("No remotes configured"))
49 fmt.Println()
50 fmt.Println("Add a remote with:")
51 fmt.Println(styles.Theme.Bold.Render(" gordon remotes add <name> <url>"))
52 return nil
53 }
54
55 fmt.Println(styles.Theme.Title.Render("Saved Remotes"))
56 fmt.Println()
57
58 // Build table rows
59 rows := make([][]string, 0, len(remotes))
60 for name, r := range remotes {
61 status := ""
62 if name == active {
63 status = styles.Theme.Success.Render("active")
64 }
65
66 // Mask token if present
67 tokenStatus := styles.Theme.Muted.Render("none")
68 if r.Token != "" {
69 tokenStatus = styles.Theme.Success.Render("set")
70 } else if r.TokenEnv != "" {
71 tokenStatus = fmt.Sprintf("$%s", r.TokenEnv)
72 }
73
74 rows = append(rows, []string{name, r.URL, tokenStatus, status})
75 }
76
77 // Render table
78 table := components.NewTable(
79 components.WithColumns([]components.TableColumn{
80 {Title: "Name", Width: 15},
81 {Title: "URL", Width: 35},
82 {Title: "Token", Width: 15},
83 {Title: "Status", Width: 10},
84 }),
85 components.WithRows(rows),
86 )
87
88 fmt.Println(table.View())
89
90 return nil
91 },
92 }
93}
94
95// newRemotesAddCmd creates the remotes add command.
96func newRemotesAddCmd() *cobra.Command {
97 var token string
98 var tokenEnv string
99
100 cmd := &cobra.Command{
101 Use: "add <name> <url>",
102 Short: "Add a new remote",
103 Long: `Add a new saved remote.
104
105Examples:
106 gordon remotes add prod https://gordon.mydomain.com
107 gordon remotes add prod https://gordon.mydomain.com --token eyJ...
108 gordon remotes add staging https://staging.mydomain.com --token-env STAGING_TOKEN`,
109 Args: cobra.ExactArgs(2),
110 RunE: func(cmd *cobra.Command, args []string) error {
111 name := args[0]
112 url := normalizeURL(args[1])
113
114 // If token-env is provided, use that instead of token
115 finalToken := token
116 if tokenEnv != "" {
117 // Store token_env reference instead of actual token
118 if err := addRemoteWithEnv(name, url, tokenEnv); err != nil {
119 return fmt.Errorf("failed to add remote: %w", err)
120 }
121 } else {
122 if err := remote.AddRemote(name, url, finalToken); err != nil {
123 return fmt.Errorf("failed to add remote: %w", err)
124 }
125 }
126
127 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote added: %s -> %s", name, url)))
128
129 if token == "" && tokenEnv == "" {
130 fmt.Println(styles.Theme.Muted.Render("Tip: Add a token with --token or --token-env for authenticated access"))
131 }
132
133 return nil
134 },
135 }
136
137 cmd.Flags().StringVar(&token, "token", "", "Authentication token")
138 cmd.Flags().StringVar(&tokenEnv, "token-env", "", "Environment variable containing token")
139
140 return cmd
141}
142
143// addRemoteWithEnv adds a remote with a token environment variable reference.
144func addRemoteWithEnv(name, url, tokenEnv string) error {
145 config, err := remote.LoadRemotes("")
146 if err != nil {
147 return err
148 }
149
150 config.Remotes[name] = remote.RemoteEntry{
151 URL: url,
152 TokenEnv: tokenEnv,
153 }
154
155 return remote.SaveRemotes("", config)
156}
157
158// newRemotesRemoveCmd creates the remotes remove command.
159func newRemotesRemoveCmd() *cobra.Command {
160 var force bool
161
162 cmd := &cobra.Command{
163 Use: "remove <name>",
164 Aliases: []string{"rm"},
165 Short: "Remove a remote",
166 Args: cobra.ExactArgs(1),
167 RunE: func(cmd *cobra.Command, args []string) error {
168 name := args[0]
169
170 // Check if remote exists
171 remotes, active, err := remote.ListRemotes()
172 if err != nil {
173 return fmt.Errorf("failed to list remotes: %w", err)
174 }
175
176 if _, exists := remotes[name]; !exists {
177 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
178 return nil
179 }
180
181 // Confirm unless --force
182 if !force {
183 message := fmt.Sprintf("Remove remote '%s'?", name)
184 if name == active {
185 message = fmt.Sprintf("Remove remote '%s'? (currently active)", name)
186 }
187
188 confirmed, err := components.RunConfirm(message)
189 if err != nil {
190 return err
191 }
192 if !confirmed {
193 fmt.Println(styles.Theme.Muted.Render("Cancelled"))
194 return nil
195 }
196 }
197
198 if err := remote.RemoveRemote(name); err != nil {
199 return fmt.Errorf("failed to remove remote: %w", err)
200 }
201
202 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote removed: %s", name)))
203 return nil
204 },
205 }
206
207 cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation")
208
209 return cmd
210}
211
212// newRemotesUseCmd creates the remotes use command.
213func newRemotesUseCmd() *cobra.Command {
214 return &cobra.Command{
215 Use: "use <name>",
216 Short: "Set the active remote",
217 Long: `Set a remote as the active default.
218
219When a remote is active, it will be used automatically for remote commands
220without needing to specify --remote.
221
222Examples:
223 gordon remotes use prod
224 gordon routes list # Uses prod remote automatically`,
225 Args: cobra.ExactArgs(1),
226 RunE: func(cmd *cobra.Command, args []string) error {
227 name := args[0]
228
229 if err := remote.SetActiveRemote(name); err != nil {
230 if err.Error() == fmt.Sprintf("remote '%s' not found", name) {
231 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
232 fmt.Println()
233 fmt.Println("Available remotes:")
234
235 remotes, _, _ := remote.ListRemotes()
236 for remoteName := range remotes {
237 fmt.Printf(" %s\n", remoteName)
238 }
239 return nil
240 }
241 return fmt.Errorf("failed to set active remote: %w", err)
242 }
243
244 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Active remote set to '%s'", name)))
245 return nil
246 },
247 }
248}
249
250// newRemotesSetTokenCmd creates the remotes set-token command.
251func newRemotesSetTokenCmd() *cobra.Command {
252 return &cobra.Command{
253 Use: "set-token <name> <token>",
254 Short: "Set the token for a remote",
255 Long: `Set or update the authentication token for a saved remote.
256
257This is useful when:
258- The server uses token-based authentication (not password auth)
259- You have a pre-generated token from 'gordon auth token generate'
260- You want to update an expired token
261
262For servers with password authentication, use 'gordon auth login' instead.
263
264Examples:
265 gordon remotes set-token prod eyJhbGciOiJIUzI1NiIs...
266 gordon remotes set-token staging $(cat token.txt)`,
267 Args: cobra.ExactArgs(2),
268 RunE: func(cmd *cobra.Command, args []string) error {
269 name := args[0]
270 token := args[1]
271
272 // Verify remote exists
273 remotes, _, err := remote.ListRemotes()
274 if err != nil {
275 return fmt.Errorf("failed to list remotes: %w", err)
276 }
277
278 if _, exists := remotes[name]; !exists {
279 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
280 return nil
281 }
282
283 if err := remote.UpdateRemoteToken(name, token); err != nil {
284 return fmt.Errorf("failed to update token: %w", err)
285 }
286
287 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Token updated for remote '%s'", name)))
288 return nil
289 },
290 }
291}
292
293// normalizeURL ensures the URL has a protocol scheme, defaulting to https.
294func normalizeURL(url string) string {
295 if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
296 return url
297 }
298 return "https://" + url
299}