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