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 tlsStatus := ""
75 if r.InsecureTLS {
76 tlsStatus = styles.Theme.Warning.Render("insecure")
77 }
78
79 rows = append(rows, []string{name, r.URL, tokenStatus, tlsStatus, status})
80 }
81
82 // Render table
83 table := components.NewTable(
84 components.WithColumns([]components.TableColumn{
85 {Title: "Name", Width: 15},
86 {Title: "URL", Width: 35},
87 {Title: "Token", Width: 15},
88 {Title: "TLS", Width: 10},
89 {Title: "Status", Width: 10},
90 }),
91 components.WithRows(rows),
92 )
93
94 fmt.Println(table.View())
95
96 return nil
97 },
98 }
99}
100
101// newRemotesAddCmd creates the remotes add command.
102func newRemotesAddCmd() *cobra.Command {
103 var token string
104 var tokenEnv string
105 var insecureTLS bool
106
107 cmd := &cobra.Command{
108 Use: "add <name> <url>",
109 Short: "Add a new remote",
110 Long: `Add a new saved remote.
111
112Examples:
113 gordon remotes add prod https://gordon.mydomain.com
114 gordon remotes add prod https://gordon.mydomain.com --token eyJ...
115 gordon remotes add staging https://staging.mydomain.com --token-env STAGING_TOKEN
116 gordon remotes add dev https://dev.internal --insecure`,
117 Args: cobra.ExactArgs(2),
118 RunE: func(cmd *cobra.Command, args []string) error {
119 name := args[0]
120 url := normalizeURL(args[1])
121
122 // If token-env is provided, use that instead of token
123 finalToken := token
124 if tokenEnv != "" {
125 // Store token_env reference instead of actual token
126 if err := addRemoteWithEnv(name, url, tokenEnv, insecureTLS); err != nil {
127 return fmt.Errorf("failed to add remote: %w", err)
128 }
129 } else {
130 if err := remote.AddRemote(name, url, finalToken, insecureTLS); err != nil {
131 return fmt.Errorf("failed to add remote: %w", err)
132 }
133 }
134
135 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote added: %s -> %s", name, url)))
136
137 if token == "" && tokenEnv == "" {
138 fmt.Println(styles.Theme.Muted.Render("Tip: Add a token with --token or --token-env for authenticated access"))
139 }
140
141 return nil
142 },
143 }
144
145 cmd.Flags().StringVar(&token, "token", "", "Authentication token")
146 cmd.Flags().StringVar(&tokenEnv, "token-env", "", "Environment variable containing token")
147 cmd.Flags().BoolVar(&insecureTLS, "insecure", false, "Skip TLS certificate verification")
148
149 return cmd
150}
151
152// addRemoteWithEnv adds a remote with a token environment variable reference.
153func addRemoteWithEnv(name, url, tokenEnv string, insecureTLS bool) error {
154 config, err := remote.LoadRemotes("")
155 if err != nil {
156 return err
157 }
158
159 config.Remotes[name] = remote.RemoteEntry{
160 URL: url,
161 TokenEnv: tokenEnv,
162 InsecureTLS: insecureTLS,
163 }
164
165 return remote.SaveRemotes("", config)
166}
167
168// newRemotesRemoveCmd creates the remotes remove command.
169func newRemotesRemoveCmd() *cobra.Command {
170 var force bool
171
172 cmd := &cobra.Command{
173 Use: "remove <name>",
174 Aliases: []string{"rm"},
175 Short: "Remove a remote",
176 Args: cobra.ExactArgs(1),
177 RunE: func(cmd *cobra.Command, args []string) error {
178 name := args[0]
179
180 // Check if remote exists
181 remotes, active, err := remote.ListRemotes()
182 if err != nil {
183 return fmt.Errorf("failed to list remotes: %w", err)
184 }
185
186 if _, exists := remotes[name]; !exists {
187 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
188 return nil
189 }
190
191 // Confirm unless --force
192 if !force {
193 message := fmt.Sprintf("Remove remote '%s'?", name)
194 if name == active {
195 message = fmt.Sprintf("Remove remote '%s'? (currently active)", name)
196 }
197
198 confirmed, err := components.RunConfirm(message)
199 if err != nil {
200 return err
201 }
202 if !confirmed {
203 fmt.Println(styles.Theme.Muted.Render("Cancelled"))
204 return nil
205 }
206 }
207
208 if err := remote.RemoveRemote(name); err != nil {
209 return fmt.Errorf("failed to remove remote: %w", err)
210 }
211
212 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote removed: %s", name)))
213 return nil
214 },
215 }
216
217 cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation")
218
219 return cmd
220}
221
222// newRemotesUseCmd creates the remotes use command.
223func newRemotesUseCmd() *cobra.Command {
224 return &cobra.Command{
225 Use: "use <name>",
226 Short: "Set the active remote",
227 Long: `Set a remote as the active default.
228
229When a remote is active, it will be used automatically for remote commands
230without needing to specify --remote.
231
232Examples:
233 gordon remotes use prod
234 gordon routes list # Uses prod remote automatically`,
235 Args: cobra.ExactArgs(1),
236 RunE: func(cmd *cobra.Command, args []string) error {
237 name := args[0]
238
239 if err := remote.SetActiveRemote(name); err != nil {
240 if err.Error() == fmt.Sprintf("remote '%s' not found", name) {
241 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
242 fmt.Println()
243 fmt.Println("Available remotes:")
244
245 remotes, _, _ := remote.ListRemotes()
246 for remoteName := range remotes {
247 fmt.Printf(" %s\n", remoteName)
248 }
249 return nil
250 }
251 return fmt.Errorf("failed to set active remote: %w", err)
252 }
253
254 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Active remote set to '%s'", name)))
255 return nil
256 },
257 }
258}
259
260// newRemotesSetTokenCmd creates the remotes set-token command.
261func newRemotesSetTokenCmd() *cobra.Command {
262 return &cobra.Command{
263 Use: "set-token <name> <token>",
264 Short: "Set the token for a remote",
265 Long: `Set or update the authentication token for a saved remote.
266
267This is useful when:
268- The server uses token-based authentication (not password auth)
269- You have a pre-generated token from 'gordon auth token generate'
270- You want to update an expired token
271
272For servers with password authentication, use 'gordon auth login' instead.
273
274Examples:
275 gordon remotes set-token prod eyJhbGciOiJIUzI1NiIs...
276 gordon remotes set-token staging $(cat token.txt)`,
277 Args: cobra.ExactArgs(2),
278 RunE: func(cmd *cobra.Command, args []string) error {
279 name := args[0]
280 token := args[1]
281
282 // Verify remote exists
283 remotes, _, err := remote.ListRemotes()
284 if err != nil {
285 return fmt.Errorf("failed to list remotes: %w", err)
286 }
287
288 if _, exists := remotes[name]; !exists {
289 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
290 return nil
291 }
292
293 if err := remote.UpdateRemoteToken(name, token); err != nil {
294 return fmt.Errorf("failed to update token: %w", err)
295 }
296
297 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Token updated for remote '%s'", name)))
298 return nil
299 },
300 }
301}
302
303// normalizeURL ensures the URL has a protocol scheme, defaulting to https.
304func normalizeURL(url string) string {
305 if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
306 return url
307 }
308 return "https://" + url
309}