1package cli
2
3import (
4 "fmt"
5 "strings"
6
7 "gordon/internal/adapters/in/cli/remote"
8 "gordon/internal/adapters/in/cli/ui/components"
9 "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
32 return cmd
33}
34
35// newRemotesListCmd creates the remotes list command.
36func newRemotesListCmd() *cobra.Command {
37 return &cobra.Command{
38 Use: "list",
39 Short: "List saved remotes",
40 RunE: func(cmd *cobra.Command, args []string) error {
41 remotes, active, err := remote.ListRemotes()
42 if err != nil {
43 return fmt.Errorf("failed to list remotes: %w", err)
44 }
45
46 if len(remotes) == 0 {
47 fmt.Println(styles.Theme.Muted.Render("No remotes configured"))
48 fmt.Println()
49 fmt.Println("Add a remote with:")
50 fmt.Println(styles.Theme.Bold.Render(" gordon remotes add <name> <url>"))
51 return nil
52 }
53
54 fmt.Println(styles.Theme.Title.Render("Saved Remotes"))
55 fmt.Println()
56
57 // Build table rows
58 rows := make([][]string, 0, len(remotes))
59 for name, r := range remotes {
60 status := ""
61 if name == active {
62 status = styles.Theme.Success.Render("active")
63 }
64
65 // Mask token if present
66 tokenStatus := styles.Theme.Muted.Render("none")
67 if r.Token != "" {
68 tokenStatus = styles.Theme.Success.Render("set")
69 } else if r.TokenEnv != "" {
70 tokenStatus = fmt.Sprintf("$%s", r.TokenEnv)
71 }
72
73 rows = append(rows, []string{name, r.URL, tokenStatus, status})
74 }
75
76 // Render table
77 table := components.NewTable(
78 components.WithColumns([]components.TableColumn{
79 {Title: "Name", Width: 15},
80 {Title: "URL", Width: 35},
81 {Title: "Token", Width: 15},
82 {Title: "Status", Width: 10},
83 }),
84 components.WithRows(rows),
85 )
86
87 fmt.Println(table.View())
88
89 return nil
90 },
91 }
92}
93
94// newRemotesAddCmd creates the remotes add command.
95func newRemotesAddCmd() *cobra.Command {
96 var token string
97 var tokenEnv string
98
99 cmd := &cobra.Command{
100 Use: "add <name> <url>",
101 Short: "Add a new remote",
102 Long: `Add a new saved remote.
103
104Examples:
105 gordon remotes add prod https://gordon.mydomain.com
106 gordon remotes add prod https://gordon.mydomain.com --token eyJ...
107 gordon remotes add staging https://staging.mydomain.com --token-env STAGING_TOKEN`,
108 Args: cobra.ExactArgs(2),
109 RunE: func(cmd *cobra.Command, args []string) error {
110 name := args[0]
111 url := normalizeURL(args[1])
112
113 // If token-env is provided, use that instead of token
114 finalToken := token
115 if tokenEnv != "" {
116 // Store token_env reference instead of actual token
117 if err := addRemoteWithEnv(name, url, tokenEnv); err != nil {
118 return fmt.Errorf("failed to add remote: %w", err)
119 }
120 } else {
121 if err := remote.AddRemote(name, url, finalToken); err != nil {
122 return fmt.Errorf("failed to add remote: %w", err)
123 }
124 }
125
126 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote added: %s -> %s", name, url)))
127
128 if token == "" && tokenEnv == "" {
129 fmt.Println(styles.Theme.Muted.Render("Tip: Add a token with --token or --token-env for authenticated access"))
130 }
131
132 return nil
133 },
134 }
135
136 cmd.Flags().StringVar(&token, "token", "", "Authentication token")
137 cmd.Flags().StringVar(&tokenEnv, "token-env", "", "Environment variable containing token")
138
139 return cmd
140}
141
142// addRemoteWithEnv adds a remote with a token environment variable reference.
143func addRemoteWithEnv(name, url, tokenEnv string) error {
144 config, err := remote.LoadRemotes("")
145 if err != nil {
146 return err
147 }
148
149 config.Remotes[name] = remote.RemoteEntry{
150 URL: url,
151 TokenEnv: tokenEnv,
152 }
153
154 return remote.SaveRemotes("", config)
155}
156
157// newRemotesRemoveCmd creates the remotes remove command.
158func newRemotesRemoveCmd() *cobra.Command {
159 var force bool
160
161 cmd := &cobra.Command{
162 Use: "remove <name>",
163 Aliases: []string{"rm"},
164 Short: "Remove a remote",
165 Args: cobra.ExactArgs(1),
166 RunE: func(cmd *cobra.Command, args []string) error {
167 name := args[0]
168
169 // Check if remote exists
170 remotes, active, err := remote.ListRemotes()
171 if err != nil {
172 return fmt.Errorf("failed to list remotes: %w", err)
173 }
174
175 if _, exists := remotes[name]; !exists {
176 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
177 return nil
178 }
179
180 // Confirm unless --force
181 if !force {
182 message := fmt.Sprintf("Remove remote '%s'?", name)
183 if name == active {
184 message = fmt.Sprintf("Remove remote '%s'? (currently active)", name)
185 }
186
187 confirmed, err := components.RunConfirm(message)
188 if err != nil {
189 return err
190 }
191 if !confirmed {
192 fmt.Println(styles.Theme.Muted.Render("Cancelled"))
193 return nil
194 }
195 }
196
197 if err := remote.RemoveRemote(name); err != nil {
198 return fmt.Errorf("failed to remove remote: %w", err)
199 }
200
201 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote removed: %s", name)))
202 return nil
203 },
204 }
205
206 cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation")
207
208 return cmd
209}
210
211// newRemotesUseCmd creates the remotes use command.
212func newRemotesUseCmd() *cobra.Command {
213 return &cobra.Command{
214 Use: "use <name>",
215 Short: "Set the active remote",
216 Long: `Set a remote as the active default.
217
218When a remote is active, it will be used automatically for remote commands
219without needing to specify --remote.
220
221Examples:
222 gordon remotes use prod
223 gordon routes list # Uses prod remote automatically`,
224 Args: cobra.ExactArgs(1),
225 RunE: func(cmd *cobra.Command, args []string) error {
226 name := args[0]
227
228 if err := remote.SetActiveRemote(name); err != nil {
229 if err.Error() == fmt.Sprintf("remote '%s' not found", name) {
230 fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
231 fmt.Println()
232 fmt.Println("Available remotes:")
233
234 remotes, _, _ := remote.ListRemotes()
235 for remoteName := range remotes {
236 fmt.Printf(" %s\n", remoteName)
237 }
238 return nil
239 }
240 return fmt.Errorf("failed to set active remote: %w", err)
241 }
242
243 fmt.Println(styles.RenderSuccess(fmt.Sprintf("Active remote set to '%s'", name)))
244 return nil
245 },
246 }
247}
248
249// normalizeURL ensures the URL has a protocol scheme, defaulting to https.
250func normalizeURL(url string) string {
251 if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
252 return url
253 }
254 return "https://" + url
255}