detached 2000c43

refactor(cli): rename target(s) to remote(s) for clarity

commit
2000c4321c05ca29668af57b9941bc4b917b7d83
author
bnema · · 2026-01-16 20:57 UTC
parent
bfe59e6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +352 -352
M docs/cli/commands.md +24 -24
@@ -14,7 +14,7 @@ Gordon provides a command-line interface for server management, deployment, and
1414 | `gordon auth` | Manage registry authentication |
1515 | `gordon routes` | Manage routes (local or remote) |
1616 | `gordon secrets` | Manage secrets (remote only) |
17-| `gordon targets` | Manage remote Gordon targets |
17+| `gordon remotes` | Manage saved remote Gordon instances |
1818
1919 ## Quick Reference
2020
@@ -50,19 +50,19 @@ gordon auth internal
5050 | Option | Description |
5151 |--------|-------------|
5252 | `-c, --config` | Path to configuration file |
53-| `--target` | Remote Gordon URL (e.g., `https://gordon.mydomain.com`) |
54-| `--token` | Authentication token for remote target |
53+| `--remote` | Remote Gordon URL (e.g., `https://gordon.mydomain.com`) |
54+| `--token` | Authentication token for remote |
5555
5656 ### Remote Targeting
5757
58-The CLI can target remote Gordon instances using the `--target` flag or `GORDON_TARGET` environment variable:
58+The CLI can target remote Gordon instances using the `--remote` flag or `GORDON_REMOTE` environment variable:
5959
6060 ```bash
6161 # Using flags
62-gordon routes list --target https://gordon.mydomain.com --token $TOKEN
62+gordon routes list --remote https://gordon.mydomain.com --token $TOKEN
6363
6464 # Using environment variables
65-export GORDON_TARGET=https://gordon.mydomain.com
65+export GORDON_REMOTE=https://gordon.mydomain.com
6666 export GORDON_TOKEN=$TOKEN
6767 gordon routes list
6868 ```
@@ -567,7 +567,7 @@ List all configured routes.
567567
568568 ```bash
569569 gordon routes list
570-gordon routes list --target https://gordon.mydomain.com --token $TOKEN
570+gordon routes list --remote https://gordon.mydomain.com --token $TOKEN
571571 ```
572572
573573 ### gordon routes add
@@ -616,46 +616,46 @@ Manage secrets on remote Gordon instances. Requires remote targeting.
616616
617617 ```bash
618618 # List secrets
619-gordon secrets list --target https://gordon.mydomain.com --token $TOKEN
619+gordon secrets list --remote https://gordon.mydomain.com --token $TOKEN
620620
621621 # Set a secret
622-gordon secrets set DATABASE_URL "postgres://..." --target ... --token ...
622+gordon secrets set DATABASE_URL "postgres://..." --remote ... --token ...
623623
624624 # Get a secret
625-gordon secrets get DATABASE_URL --target ... --token ...
625+gordon secrets get DATABASE_URL --remote ... --token ...
626626
627627 # Delete a secret
628-gordon secrets delete DATABASE_URL --target ... --token ...
628+gordon secrets delete DATABASE_URL --remote ... --token ...
629629 ```
630630
631631 ---
632632
633-## gordon targets
633+## gordon remotes
634634
635-Manage saved remote Gordon targets for easier CLI usage.
635+Manage saved remote Gordon instances for easier CLI usage.
636636
637637 ### Subcommands
638638
639639 | Subcommand | Description |
640640 |------------|-------------|
641-| `list` | List saved targets |
642-| `add` | Add a new target |
643-| `remove` | Remove a saved target |
644-| `set-default` | Set default target |
641+| `list` | List saved remotes |
642+| `add` | Add a new remote |
643+| `remove` | Remove a saved remote |
644+| `use` | Set active remote |
645645
646646 ### Examples
647647
648648 ```bash
649-# Add a target
650-gordon targets add prod https://gordon.mydomain.com $TOKEN
649+# Add a remote
650+gordon remotes add prod https://gordon.mydomain.com --token $TOKEN
651651
652-# List saved targets
653-gordon targets list
652+# List saved remotes
653+gordon remotes list
654654
655-# Set default target
656-gordon targets set-default prod
655+# Set active remote
656+gordon remotes use prod
657657
658-# Now use without --target flag
658+# Now use without --remote flag
659659 gordon routes list
660660 ```
661661
@@ -14,7 +14,7 @@ Gordon provides a command-line interface for server management, deployment, and
14 | `gordon auth` | Manage registry authentication |14 | `gordon auth` | Manage registry authentication |
15 | `gordon routes` | Manage routes (local or remote) |15 | `gordon routes` | Manage routes (local or remote) |
16 | `gordon secrets` | Manage secrets (remote only) |16 | `gordon secrets` | Manage secrets (remote only) |
17-| `gordon targets` | Manage remote Gordon targets |17+| `gordon remotes` | Manage saved remote Gordon instances |
18 18
19 ## Quick Reference19 ## Quick Reference
20 20
@@ -50,19 +50,19 @@ gordon auth internal
50 | Option | Description |50 | Option | Description |
51 |--------|-------------|51 |--------|-------------|
52 | `-c, --config` | Path to configuration file |52 | `-c, --config` | Path to configuration file |
53-| `--target` | Remote Gordon URL (e.g., `https://gordon.mydomain.com`) |53+| `--remote` | Remote Gordon URL (e.g., `https://gordon.mydomain.com`) |
54-| `--token` | Authentication token for remote target |54+| `--token` | Authentication token for remote |
55 55
56 ### Remote Targeting56 ### Remote Targeting
57 57
58-The CLI can target remote Gordon instances using the `--target` flag or `GORDON_TARGET` environment variable:58+The CLI can target remote Gordon instances using the `--remote` flag or `GORDON_REMOTE` environment variable:
59 59
60 ```bash60 ```bash
61 # Using flags61 # Using flags
62-gordon routes list --target https://gordon.mydomain.com --token $TOKEN62+gordon routes list --remote https://gordon.mydomain.com --token $TOKEN
63 63
64 # Using environment variables64 # Using environment variables
65-export GORDON_TARGET=https://gordon.mydomain.com65+export GORDON_REMOTE=https://gordon.mydomain.com
66 export GORDON_TOKEN=$TOKEN66 export GORDON_TOKEN=$TOKEN
67 gordon routes list67 gordon routes list
68 ```68 ```
@@ -567,7 +567,7 @@ List all configured routes.
567 567
568 ```bash568 ```bash
569 gordon routes list569 gordon routes list
570-gordon routes list --target https://gordon.mydomain.com --token $TOKEN570+gordon routes list --remote https://gordon.mydomain.com --token $TOKEN
571 ```571 ```
572 572
573 ### gordon routes add573 ### gordon routes add
@@ -616,46 +616,46 @@ Manage secrets on remote Gordon instances. Requires remote targeting.
616 616
617 ```bash617 ```bash
618 # List secrets618 # List secrets
619-gordon secrets list --target https://gordon.mydomain.com --token $TOKEN619+gordon secrets list --remote https://gordon.mydomain.com --token $TOKEN
620 620
621 # Set a secret621 # Set a secret
622-gordon secrets set DATABASE_URL "postgres://..." --target ... --token ...622+gordon secrets set DATABASE_URL "postgres://..." --remote ... --token ...
623 623
624 # Get a secret624 # Get a secret
625-gordon secrets get DATABASE_URL --target ... --token ...625+gordon secrets get DATABASE_URL --remote ... --token ...
626 626
627 # Delete a secret627 # Delete a secret
628-gordon secrets delete DATABASE_URL --target ... --token ...628+gordon secrets delete DATABASE_URL --remote ... --token ...
629 ```629 ```
630 630
631 ---631 ---
632 632
633-## gordon targets633+## gordon remotes
634 634
635-Manage saved remote Gordon targets for easier CLI usage.635+Manage saved remote Gordon instances for easier CLI usage.
636 636
637 ### Subcommands637 ### Subcommands
638 638
639 | Subcommand | Description |639 | Subcommand | Description |
640 |------------|-------------|640 |------------|-------------|
641-| `list` | List saved targets |641+| `list` | List saved remotes |
642-| `add` | Add a new target |642+| `add` | Add a new remote |
643-| `remove` | Remove a saved target |643+| `remove` | Remove a saved remote |
644-| `set-default` | Set default target |644+| `use` | Set active remote |
645 645
646 ### Examples646 ### Examples
647 647
648 ```bash648 ```bash
649-# Add a target649+# Add a remote
650-gordon targets add prod https://gordon.mydomain.com $TOKEN650+gordon remotes add prod https://gordon.mydomain.com --token $TOKEN
651 651
652-# List saved targets652+# List saved remotes
653-gordon targets list653+gordon remotes list
654 654
655-# Set default target655+# Set active remote
656-gordon targets set-default prod656+gordon remotes use prod
657 657
658-# Now use without --target flag658+# Now use without --remote flag
659 gordon routes list659 gordon routes list
660 ```660 ```
661 661
M docs/config/auto-route.md +2 -2
@@ -249,12 +249,12 @@ Gordon will:
249249 ### Example: Complete Setup
250250
251251 ```dockerfile
252-FROM golang:1.21-alpine AS builder
252+FROM golang:1.25-alpine AS builder
253253 WORKDIR /app
254254 COPY . .
255255 RUN go build -o server
256256
257-FROM alpine:3.19
257+FROM alpine:3.21
258258 WORKDIR /app
259259 COPY --from=builder /app/server .
260260 COPY .env.example .
@@ -249,12 +249,12 @@ Gordon will:
249 ### Example: Complete Setup249 ### Example: Complete Setup
250 250
251 ```dockerfile251 ```dockerfile
252-FROM golang:1.21-alpine AS builder252+FROM golang:1.25-alpine AS builder
253 WORKDIR /app253 WORKDIR /app
254 COPY . .254 COPY . .
255 RUN go build -o server255 RUN go build -o server
256 256
257-FROM alpine:3.19257+FROM alpine:3.21
258 WORKDIR /app258 WORKDIR /app
259 COPY --from=builder /app/server .259 COPY --from=builder /app/server .
260 COPY .env.example .260 COPY .env.example .
M internal/adapters/in/cli/remote/config.go +72 -72
@@ -11,20 +11,20 @@ import (
1111 // ClientConfig represents the client-mode configuration.
1212 type ClientConfig struct {
1313 Client ClientSettings `toml:"client"`
14- Targets map[string]TargetEntry `toml:"targets"`
15- Active string `toml:"active"` // Active target name
14+ Remotes map[string]RemoteEntry `toml:"remotes"`
15+ Active string `toml:"active"` // Active remote name
1616 }
1717
1818 // ClientSettings represents the [client] section.
1919 type ClientSettings struct {
2020 Mode string `toml:"mode"` // "local" (default) or "remote"
21- Target string `toml:"target"` // Remote Gordon URL
21+ Remote string `toml:"remote"` // Remote Gordon URL
2222 Token string `toml:"token"` // Auth token
2323 TokenEnv string `toml:"token_env"` // Env var name for token
2424 }
2525
26-// TargetEntry represents a saved target in [targets.*].
27-type TargetEntry struct {
26+// RemoteEntry represents a saved remote in [remotes.*].
27+type RemoteEntry struct {
2828 URL string `toml:"url"`
2929 Token string `toml:"token,omitempty"`
3030 TokenEnv string `toml:"token_env,omitempty"`
@@ -39,13 +39,13 @@ func DefaultClientConfigPath() string {
3939 return filepath.Join(configDir, "gordon", "gordon.toml")
4040 }
4141
42-// DefaultTargetsPath returns the default targets config path.
43-func DefaultTargetsPath() string {
42+// DefaultRemotesPath returns the default remotes config path.
43+func DefaultRemotesPath() string {
4444 configDir, err := os.UserConfigDir()
4545 if err != nil {
4646 configDir = os.Getenv("HOME")
4747 }
48- return filepath.Join(configDir, "gordon", "targets.toml")
48+ return filepath.Join(configDir, "gordon", "remotes.toml")
4949 }
5050
5151 // LoadClientConfig loads the client configuration.
@@ -62,7 +62,7 @@ func LoadClientConfig(path string) (*ClientConfig, error) {
6262 Client: ClientSettings{
6363 Mode: "local",
6464 },
65- Targets: make(map[string]TargetEntry),
65+ Remotes: make(map[string]RemoteEntry),
6666 }, nil
6767 }
6868 return nil, fmt.Errorf("failed to read config: %w", err)
@@ -73,45 +73,45 @@ func LoadClientConfig(path string) (*ClientConfig, error) {
7373 return nil, fmt.Errorf("failed to parse config: %w", err)
7474 }
7575
76- if config.Targets == nil {
77- config.Targets = make(map[string]TargetEntry)
76+ if config.Remotes == nil {
77+ config.Remotes = make(map[string]RemoteEntry)
7878 }
7979
8080 return &config, nil
8181 }
8282
83-// LoadTargets loads the targets configuration.
84-func LoadTargets(path string) (*ClientConfig, error) {
83+// LoadRemotes loads the remotes configuration.
84+func LoadRemotes(path string) (*ClientConfig, error) {
8585 if path == "" {
86- path = DefaultTargetsPath()
86+ path = DefaultRemotesPath()
8787 }
8888
8989 data, err := os.ReadFile(path)
9090 if err != nil {
9191 if os.IsNotExist(err) {
9292 return &ClientConfig{
93- Targets: make(map[string]TargetEntry),
93+ Remotes: make(map[string]RemoteEntry),
9494 }, nil
9595 }
96- return nil, fmt.Errorf("failed to read targets: %w", err)
96+ return nil, fmt.Errorf("failed to read remotes: %w", err)
9797 }
9898
9999 var config ClientConfig
100100 if err := toml.Unmarshal(data, &config); err != nil {
101- return nil, fmt.Errorf("failed to parse targets: %w", err)
101+ return nil, fmt.Errorf("failed to parse remotes: %w", err)
102102 }
103103
104- if config.Targets == nil {
105- config.Targets = make(map[string]TargetEntry)
104+ if config.Remotes == nil {
105+ config.Remotes = make(map[string]RemoteEntry)
106106 }
107107
108108 return &config, nil
109109 }
110110
111-// SaveTargets saves the targets configuration.
112-func SaveTargets(path string, config *ClientConfig) error {
111+// SaveRemotes saves the remotes configuration.
112+func SaveRemotes(path string, config *ClientConfig) error {
113113 if path == "" {
114- path = DefaultTargetsPath()
114+ path = DefaultRemotesPath()
115115 }
116116
117117 // Ensure directory exists
@@ -122,51 +122,51 @@ func SaveTargets(path string, config *ClientConfig) error {
122122
123123 data, err := toml.Marshal(config)
124124 if err != nil {
125- return fmt.Errorf("failed to marshal targets: %w", err)
125+ return fmt.Errorf("failed to marshal remotes: %w", err)
126126 }
127127
128128 if err := os.WriteFile(path, data, 0600); err != nil {
129- return fmt.Errorf("failed to write targets: %w", err)
129+ return fmt.Errorf("failed to write remotes: %w", err)
130130 }
131131
132132 return nil
133133 }
134134
135-// ResolveTarget resolves the target URL and token from configuration.
136-// Precedence: flag > env > config > active target
137-func ResolveTarget(flagTarget, flagToken string) (url, token string, isRemote bool) {
135+// ResolveRemote resolves the remote URL and token from configuration.
136+// Precedence: flag > env > config > active remote
137+func ResolveRemote(flagRemote, flagToken string) (url, token string, isRemote bool) {
138138 config, _ := LoadClientConfig("")
139- targets, _ := LoadTargets("")
139+ remotes, _ := LoadRemotes("")
140140
141- url, isRemote = resolveTargetURL(flagTarget, config, targets)
141+ url, isRemote = resolveRemoteURL(flagRemote, config, remotes)
142142 if isRemote {
143- token = resolveToken(flagToken, config, targets)
143+ token = resolveToken(flagToken, config, remotes)
144144 }
145145
146146 return url, token, isRemote
147147 }
148148
149-// resolveTargetURL resolves the target URL from various sources.
150-func resolveTargetURL(flagTarget string, config *ClientConfig, targets *ClientConfig) (string, bool) {
149+// resolveRemoteURL resolves the remote URL from various sources.
150+func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (string, bool) {
151151 // 1. Check flag
152- if flagTarget != "" {
153- return flagTarget, true
152+ if flagRemote != "" {
153+ return flagRemote, true
154154 }
155155
156156 // 2. Check environment variable
157- if envTarget := os.Getenv("GORDON_TARGET"); envTarget != "" {
158- return envTarget, true
157+ if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
158+ return envRemote, true
159159 }
160160
161161 // 3. Check client config
162- if config != nil && config.Client.Mode == "remote" && config.Client.Target != "" {
163- return config.Client.Target, true
162+ if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
163+ return config.Client.Remote, true
164164 }
165165
166- // 4. Check active target
167- if targets != nil && targets.Active != "" {
168- if target, ok := targets.Targets[targets.Active]; ok {
169- return target.URL, true
166+ // 4. Check active remote
167+ if remotes != nil && remotes.Active != "" {
168+ if remote, ok := remotes.Remotes[remotes.Active]; ok {
169+ return remote.URL, true
170170 }
171171 }
172172
@@ -174,7 +174,7 @@ func resolveTargetURL(flagTarget string, config *ClientConfig, targets *ClientCo
174174 }
175175
176176 // resolveToken resolves the authentication token from various sources.
177-func resolveToken(flagToken string, config *ClientConfig, targets *ClientConfig) string {
177+func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
178178 // 1. Flag token takes precedence
179179 if flagToken != "" {
180180 return flagToken
@@ -195,14 +195,14 @@ func resolveToken(flagToken string, config *ClientConfig, targets *ClientConfig)
195195 }
196196 }
197197
198- // 4. Active target token
199- if targets != nil && targets.Active != "" {
200- if target, ok := targets.Targets[targets.Active]; ok {
201- if target.Token != "" {
202- return target.Token
198+ // 4. Active remote token
199+ if remotes != nil && remotes.Active != "" {
200+ if remote, ok := remotes.Remotes[remotes.Active]; ok {
201+ if remote.Token != "" {
202+ return remote.Token
203203 }
204- if target.TokenEnv != "" {
205- return os.Getenv(target.TokenEnv)
204+ if remote.TokenEnv != "" {
205+ return os.Getenv(remote.TokenEnv)
206206 }
207207 }
208208 }
@@ -210,61 +210,61 @@ func resolveToken(flagToken string, config *ClientConfig, targets *ClientConfig)
210210 return ""
211211 }
212212
213-// AddTarget adds a new target to the targets configuration.
214-func AddTarget(name, url, token string) error {
215- config, err := LoadTargets("")
213+// AddRemote adds a new remote to the remotes configuration.
214+func AddRemote(name, url, token string) error {
215+ config, err := LoadRemotes("")
216216 if err != nil {
217217 return err
218218 }
219219
220- config.Targets[name] = TargetEntry{
220+ config.Remotes[name] = RemoteEntry{
221221 URL: url,
222222 Token: token,
223223 }
224224
225- return SaveTargets("", config)
225+ return SaveRemotes("", config)
226226 }
227227
228-// RemoveTarget removes a target from the targets configuration.
229-func RemoveTarget(name string) error {
230- config, err := LoadTargets("")
228+// RemoveRemote removes a remote from the remotes configuration.
229+func RemoveRemote(name string) error {
230+ config, err := LoadRemotes("")
231231 if err != nil {
232232 return err
233233 }
234234
235- delete(config.Targets, name)
235+ delete(config.Remotes, name)
236236
237- // Clear active if it was the removed target
237+ // Clear active if it was the removed remote
238238 if config.Active == name {
239239 config.Active = ""
240240 }
241241
242- return SaveTargets("", config)
242+ return SaveRemotes("", config)
243243 }
244244
245-// SetActiveTarget sets the active target.
246-func SetActiveTarget(name string) error {
247- config, err := LoadTargets("")
245+// SetActiveRemote sets the active remote.
246+func SetActiveRemote(name string) error {
247+ config, err := LoadRemotes("")
248248 if err != nil {
249249 return err
250250 }
251251
252- // Verify target exists
253- if _, ok := config.Targets[name]; !ok {
254- return fmt.Errorf("target '%s' not found", name)
252+ // Verify remote exists
253+ if _, ok := config.Remotes[name]; !ok {
254+ return fmt.Errorf("remote '%s' not found", name)
255255 }
256256
257257 config.Active = name
258258
259- return SaveTargets("", config)
259+ return SaveRemotes("", config)
260260 }
261261
262-// ListTargets returns all saved targets.
263-func ListTargets() (map[string]TargetEntry, string, error) {
264- config, err := LoadTargets("")
262+// ListRemotes returns all saved remotes.
263+func ListRemotes() (map[string]RemoteEntry, string, error) {
264+ config, err := LoadRemotes("")
265265 if err != nil {
266266 return nil, "", err
267267 }
268268
269- return config.Targets, config.Active, nil
269+ return config.Remotes, config.Active, nil
270270 }
@@ -11,20 +11,20 @@ import (
11 // ClientConfig represents the client-mode configuration.11 // ClientConfig represents the client-mode configuration.
12 type ClientConfig struct {12 type ClientConfig struct {
13 Client ClientSettings `toml:"client"`13 Client ClientSettings `toml:"client"`
14- Targets map[string]TargetEntry `toml:"targets"`14+ Remotes map[string]RemoteEntry `toml:"remotes"`
15- Active string `toml:"active"` // Active target name15+ Active string `toml:"active"` // Active remote name
16 }16 }
17 17
18 // ClientSettings represents the [client] section.18 // ClientSettings represents the [client] section.
19 type ClientSettings struct {19 type ClientSettings struct {
20 Mode string `toml:"mode"` // "local" (default) or "remote"20 Mode string `toml:"mode"` // "local" (default) or "remote"
21- Target string `toml:"target"` // Remote Gordon URL21+ Remote string `toml:"remote"` // Remote Gordon URL
22 Token string `toml:"token"` // Auth token22 Token string `toml:"token"` // Auth token
23 TokenEnv string `toml:"token_env"` // Env var name for token23 TokenEnv string `toml:"token_env"` // Env var name for token
24 }24 }
25 25
26-// TargetEntry represents a saved target in [targets.*].26+// RemoteEntry represents a saved remote in [remotes.*].
27-type TargetEntry struct {27+type RemoteEntry struct {
28 URL string `toml:"url"`28 URL string `toml:"url"`
29 Token string `toml:"token,omitempty"`29 Token string `toml:"token,omitempty"`
30 TokenEnv string `toml:"token_env,omitempty"`30 TokenEnv string `toml:"token_env,omitempty"`
@@ -39,13 +39,13 @@ func DefaultClientConfigPath() string {
39 return filepath.Join(configDir, "gordon", "gordon.toml")39 return filepath.Join(configDir, "gordon", "gordon.toml")
40 }40 }
41 41
42-// DefaultTargetsPath returns the default targets config path.42+// DefaultRemotesPath returns the default remotes config path.
43-func DefaultTargetsPath() string {43+func DefaultRemotesPath() string {
44 configDir, err := os.UserConfigDir()44 configDir, err := os.UserConfigDir()
45 if err != nil {45 if err != nil {
46 configDir = os.Getenv("HOME")46 configDir = os.Getenv("HOME")
47 }47 }
48- return filepath.Join(configDir, "gordon", "targets.toml")48+ return filepath.Join(configDir, "gordon", "remotes.toml")
49 }49 }
50 50
51 // LoadClientConfig loads the client configuration.51 // LoadClientConfig loads the client configuration.
@@ -62,7 +62,7 @@ func LoadClientConfig(path string) (*ClientConfig, error) {
62 Client: ClientSettings{62 Client: ClientSettings{
63 Mode: "local",63 Mode: "local",
64 },64 },
65- Targets: make(map[string]TargetEntry),65+ Remotes: make(map[string]RemoteEntry),
66 }, nil66 }, nil
67 }67 }
68 return nil, fmt.Errorf("failed to read config: %w", err)68 return nil, fmt.Errorf("failed to read config: %w", err)
@@ -73,45 +73,45 @@ func LoadClientConfig(path string) (*ClientConfig, error) {
73 return nil, fmt.Errorf("failed to parse config: %w", err)73 return nil, fmt.Errorf("failed to parse config: %w", err)
74 }74 }
75 75
76- if config.Targets == nil {76+ if config.Remotes == nil {
77- config.Targets = make(map[string]TargetEntry)77+ config.Remotes = make(map[string]RemoteEntry)
78 }78 }
79 79
80 return &config, nil80 return &config, nil
81 }81 }
82 82
83-// LoadTargets loads the targets configuration.83+// LoadRemotes loads the remotes configuration.
84-func LoadTargets(path string) (*ClientConfig, error) {84+func LoadRemotes(path string) (*ClientConfig, error) {
85 if path == "" {85 if path == "" {
86- path = DefaultTargetsPath()86+ path = DefaultRemotesPath()
87 }87 }
88 88
89 data, err := os.ReadFile(path)89 data, err := os.ReadFile(path)
90 if err != nil {90 if err != nil {
91 if os.IsNotExist(err) {91 if os.IsNotExist(err) {
92 return &ClientConfig{92 return &ClientConfig{
93- Targets: make(map[string]TargetEntry),93+ Remotes: make(map[string]RemoteEntry),
94 }, nil94 }, nil
95 }95 }
96- return nil, fmt.Errorf("failed to read targets: %w", err)96+ return nil, fmt.Errorf("failed to read remotes: %w", err)
97 }97 }
98 98
99 var config ClientConfig99 var config ClientConfig
100 if err := toml.Unmarshal(data, &config); err != nil {100 if err := toml.Unmarshal(data, &config); err != nil {
101- return nil, fmt.Errorf("failed to parse targets: %w", err)101+ return nil, fmt.Errorf("failed to parse remotes: %w", err)
102 }102 }
103 103
104- if config.Targets == nil {104+ if config.Remotes == nil {
105- config.Targets = make(map[string]TargetEntry)105+ config.Remotes = make(map[string]RemoteEntry)
106 }106 }
107 107
108 return &config, nil108 return &config, nil
109 }109 }
110 110
111-// SaveTargets saves the targets configuration.111+// SaveRemotes saves the remotes configuration.
112-func SaveTargets(path string, config *ClientConfig) error {112+func SaveRemotes(path string, config *ClientConfig) error {
113 if path == "" {113 if path == "" {
114- path = DefaultTargetsPath()114+ path = DefaultRemotesPath()
115 }115 }
116 116
117 // Ensure directory exists117 // Ensure directory exists
@@ -122,51 +122,51 @@ func SaveTargets(path string, config *ClientConfig) error {
122 122
123 data, err := toml.Marshal(config)123 data, err := toml.Marshal(config)
124 if err != nil {124 if err != nil {
125- return fmt.Errorf("failed to marshal targets: %w", err)125+ return fmt.Errorf("failed to marshal remotes: %w", err)
126 }126 }
127 127
128 if err := os.WriteFile(path, data, 0600); err != nil {128 if err := os.WriteFile(path, data, 0600); err != nil {
129- return fmt.Errorf("failed to write targets: %w", err)129+ return fmt.Errorf("failed to write remotes: %w", err)
130 }130 }
131 131
132 return nil132 return nil
133 }133 }
134 134
135-// ResolveTarget resolves the target URL and token from configuration.135+// ResolveRemote resolves the remote URL and token from configuration.
136-// Precedence: flag > env > config > active target136+// Precedence: flag > env > config > active remote
137-func ResolveTarget(flagTarget, flagToken string) (url, token string, isRemote bool) {137+func ResolveRemote(flagRemote, flagToken string) (url, token string, isRemote bool) {
138 config, _ := LoadClientConfig("")138 config, _ := LoadClientConfig("")
139- targets, _ := LoadTargets("")139+ remotes, _ := LoadRemotes("")
140 140
141- url, isRemote = resolveTargetURL(flagTarget, config, targets)141+ url, isRemote = resolveRemoteURL(flagRemote, config, remotes)
142 if isRemote {142 if isRemote {
143- token = resolveToken(flagToken, config, targets)143+ token = resolveToken(flagToken, config, remotes)
144 }144 }
145 145
146 return url, token, isRemote146 return url, token, isRemote
147 }147 }
148 148
149-// resolveTargetURL resolves the target URL from various sources.149+// resolveRemoteURL resolves the remote URL from various sources.
150-func resolveTargetURL(flagTarget string, config *ClientConfig, targets *ClientConfig) (string, bool) {150+func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (string, bool) {
151 // 1. Check flag151 // 1. Check flag
152- if flagTarget != "" {152+ if flagRemote != "" {
153- return flagTarget, true153+ return flagRemote, true
154 }154 }
155 155
156 // 2. Check environment variable156 // 2. Check environment variable
157- if envTarget := os.Getenv("GORDON_TARGET"); envTarget != "" {157+ if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
158- return envTarget, true158+ return envRemote, true
159 }159 }
160 160
161 // 3. Check client config161 // 3. Check client config
162- if config != nil && config.Client.Mode == "remote" && config.Client.Target != "" {162+ if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
163- return config.Client.Target, true163+ return config.Client.Remote, true
164 }164 }
165 165
166- // 4. Check active target166+ // 4. Check active remote
167- if targets != nil && targets.Active != "" {167+ if remotes != nil && remotes.Active != "" {
168- if target, ok := targets.Targets[targets.Active]; ok {168+ if remote, ok := remotes.Remotes[remotes.Active]; ok {
169- return target.URL, true169+ return remote.URL, true
170 }170 }
171 }171 }
172 172
@@ -174,7 +174,7 @@ func resolveTargetURL(flagTarget string, config *ClientConfig, targets *ClientCo
174 }174 }
175 175
176 // resolveToken resolves the authentication token from various sources.176 // resolveToken resolves the authentication token from various sources.
177-func resolveToken(flagToken string, config *ClientConfig, targets *ClientConfig) string {177+func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
178 // 1. Flag token takes precedence178 // 1. Flag token takes precedence
179 if flagToken != "" {179 if flagToken != "" {
180 return flagToken180 return flagToken
@@ -195,14 +195,14 @@ func resolveToken(flagToken string, config *ClientConfig, targets *ClientConfig)
195 }195 }
196 }196 }
197 197
198- // 4. Active target token198+ // 4. Active remote token
199- if targets != nil && targets.Active != "" {199+ if remotes != nil && remotes.Active != "" {
200- if target, ok := targets.Targets[targets.Active]; ok {200+ if remote, ok := remotes.Remotes[remotes.Active]; ok {
201- if target.Token != "" {201+ if remote.Token != "" {
202- return target.Token202+ return remote.Token
203 }203 }
204- if target.TokenEnv != "" {204+ if remote.TokenEnv != "" {
205- return os.Getenv(target.TokenEnv)205+ return os.Getenv(remote.TokenEnv)
206 }206 }
207 }207 }
208 }208 }
@@ -210,61 +210,61 @@ func resolveToken(flagToken string, config *ClientConfig, targets *ClientConfig)
210 return ""210 return ""
211 }211 }
212 212
213-// AddTarget adds a new target to the targets configuration.213+// AddRemote adds a new remote to the remotes configuration.
214-func AddTarget(name, url, token string) error {214+func AddRemote(name, url, token string) error {
215- config, err := LoadTargets("")215+ config, err := LoadRemotes("")
216 if err != nil {216 if err != nil {
217 return err217 return err
218 }218 }
219 219
220- config.Targets[name] = TargetEntry{220+ config.Remotes[name] = RemoteEntry{
221 URL: url,221 URL: url,
222 Token: token,222 Token: token,
223 }223 }
224 224
225- return SaveTargets("", config)225+ return SaveRemotes("", config)
226 }226 }
227 227
228-// RemoveTarget removes a target from the targets configuration.228+// RemoveRemote removes a remote from the remotes configuration.
229-func RemoveTarget(name string) error {229+func RemoveRemote(name string) error {
230- config, err := LoadTargets("")230+ config, err := LoadRemotes("")
231 if err != nil {231 if err != nil {
232 return err232 return err
233 }233 }
234 234
235- delete(config.Targets, name)235+ delete(config.Remotes, name)
236 236
237- // Clear active if it was the removed target237+ // Clear active if it was the removed remote
238 if config.Active == name {238 if config.Active == name {
239 config.Active = ""239 config.Active = ""
240 }240 }
241 241
242- return SaveTargets("", config)242+ return SaveRemotes("", config)
243 }243 }
244 244
245-// SetActiveTarget sets the active target.245+// SetActiveRemote sets the active remote.
246-func SetActiveTarget(name string) error {246+func SetActiveRemote(name string) error {
247- config, err := LoadTargets("")247+ config, err := LoadRemotes("")
248 if err != nil {248 if err != nil {
249 return err249 return err
250 }250 }
251 251
252- // Verify target exists252+ // Verify remote exists
253- if _, ok := config.Targets[name]; !ok {253+ if _, ok := config.Remotes[name]; !ok {
254- return fmt.Errorf("target '%s' not found", name)254+ return fmt.Errorf("remote '%s' not found", name)
255 }255 }
256 256
257 config.Active = name257 config.Active = name
258 258
259- return SaveTargets("", config)259+ return SaveRemotes("", config)
260 }260 }
261 261
262-// ListTargets returns all saved targets.262+// ListRemotes returns all saved remotes.
263-func ListTargets() (map[string]TargetEntry, string, error) {263+func ListRemotes() (map[string]RemoteEntry, string, error) {
264- config, err := LoadTargets("")264+ config, err := LoadRemotes("")
265 if err != nil {265 if err != nil {
266 return nil, "", err266 return nil, "", err
267 }267 }
268 268
269- return config.Targets, config.Active, nil269+ return config.Remotes, config.Active, nil
270 }270 }
A internal/adapters/in/cli/remotes.go +246 -0
@@ -0,0 +1,246 @@
1+package cli
2+
3+import (
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.
14+func 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+
20+Remotes allow you to save frequently used remote Gordon instances
21+and quickly switch between them.
22+
23+Configuration 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.
35+func 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.
94+func 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+
103+Examples:
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.
142+func 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.
157+func 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.
211+func 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+
217+When a remote is active, it will be used automatically for remote commands
218+without needing to specify --remote.
219+
220+Examples:
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+}
@@ -0,0 +1,246 @@
1+package cli
2+
3+import (
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.
14+func 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+
20+Remotes allow you to save frequently used remote Gordon instances
21+and quickly switch between them.
22+
23+Configuration 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.
35+func 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.
94+func 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+
103+Examples:
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.
142+func 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.
157+func 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.
211+func 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+
217+When a remote is active, it will be used automatically for remote commands
218+without needing to specify --remote.
219+
220+Examples:
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+}
M internal/adapters/in/cli/root.go +8 -8
@@ -16,7 +16,7 @@ var (
1616 BuildDate = "unknown"
1717
1818 // Global flags for remote targeting
19- targetFlag string
19+ remoteFlag string
2020 tokenFlag string
2121 )
2222
@@ -31,13 +31,13 @@ a Docker registry with automatic container deployment capabilities.
3131 It listens for image pushes and automatically deploys containers based on
3232 configuration rules, making it ideal for single-server deployments.
3333
34-The CLI can target remote Gordon instances using the --target flag or
35-GORDON_TARGET environment variable.`,
34+The CLI can target remote Gordon instances using the --remote flag or
35+GORDON_REMOTE environment variable.`,
3636 }
3737
3838 // Add persistent flags for remote targeting
39- rootCmd.PersistentFlags().StringVar(&targetFlag, "target", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")
40- rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote target")
39+ rootCmd.PersistentFlags().StringVar(&remoteFlag, "remote", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")
40+ rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote")
4141
4242 // Server commands
4343 rootCmd.AddCommand(newServeCmd())
@@ -51,7 +51,7 @@ GORDON_TARGET environment variable.`,
5151 // Remote management commands
5252 rootCmd.AddCommand(newRoutesCmd())
5353 rootCmd.AddCommand(newSecretsCmd())
54- rootCmd.AddCommand(newTargetsCmd())
54+ rootCmd.AddCommand(newRemotesCmd())
5555 rootCmd.AddCommand(newStatusCmd())
5656
5757 return rootCmd
@@ -60,7 +60,7 @@ GORDON_TARGET environment variable.`,
6060 // GetRemoteClient returns a remote client if targeting a remote instance,
6161 // or nil if running locally.
6262 func GetRemoteClient() (*remote.Client, bool) {
63- url, token, isRemote := remote.ResolveTarget(targetFlag, tokenFlag)
63+ url, token, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)
6464 if !isRemote {
6565 return nil, false
6666 }
@@ -71,7 +71,7 @@ func GetRemoteClient() (*remote.Client, bool) {
7171
7272 // IsRemoteMode returns true if CLI is targeting a remote Gordon instance.
7373 func IsRemoteMode() bool {
74- _, _, isRemote := remote.ResolveTarget(targetFlag, tokenFlag)
74+ _, _, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)
7575 return isRemote
7676 }
7777
@@ -16,7 +16,7 @@ var (
16 BuildDate = "unknown"16 BuildDate = "unknown"
17 17
18 // Global flags for remote targeting18 // Global flags for remote targeting
19- targetFlag string19+ remoteFlag string
20 tokenFlag string20 tokenFlag string
21 )21 )
22 22
@@ -31,13 +31,13 @@ a Docker registry with automatic container deployment capabilities.
31 It listens for image pushes and automatically deploys containers based on31 It listens for image pushes and automatically deploys containers based on
32 configuration rules, making it ideal for single-server deployments.32 configuration rules, making it ideal for single-server deployments.
33 33
34-The CLI can target remote Gordon instances using the --target flag or34+The CLI can target remote Gordon instances using the --remote flag or
35-GORDON_TARGET environment variable.`,35+GORDON_REMOTE environment variable.`,
36 }36 }
37 37
38 // Add persistent flags for remote targeting38 // Add persistent flags for remote targeting
39- rootCmd.PersistentFlags().StringVar(&targetFlag, "target", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")39+ rootCmd.PersistentFlags().StringVar(&remoteFlag, "remote", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")
40- rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote target")40+ rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote")
41 41
42 // Server commands42 // Server commands
43 rootCmd.AddCommand(newServeCmd())43 rootCmd.AddCommand(newServeCmd())
@@ -51,7 +51,7 @@ GORDON_TARGET environment variable.`,
51 // Remote management commands51 // Remote management commands
52 rootCmd.AddCommand(newRoutesCmd())52 rootCmd.AddCommand(newRoutesCmd())
53 rootCmd.AddCommand(newSecretsCmd())53 rootCmd.AddCommand(newSecretsCmd())
54- rootCmd.AddCommand(newTargetsCmd())54+ rootCmd.AddCommand(newRemotesCmd())
55 rootCmd.AddCommand(newStatusCmd())55 rootCmd.AddCommand(newStatusCmd())
56 56
57 return rootCmd57 return rootCmd
@@ -60,7 +60,7 @@ GORDON_TARGET environment variable.`,
60 // GetRemoteClient returns a remote client if targeting a remote instance,60 // GetRemoteClient returns a remote client if targeting a remote instance,
61 // or nil if running locally.61 // or nil if running locally.
62 func GetRemoteClient() (*remote.Client, bool) {62 func GetRemoteClient() (*remote.Client, bool) {
63- url, token, isRemote := remote.ResolveTarget(targetFlag, tokenFlag)63+ url, token, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)
64 if !isRemote {64 if !isRemote {
65 return nil, false65 return nil, false
66 }66 }
@@ -71,7 +71,7 @@ func GetRemoteClient() (*remote.Client, bool) {
71 71
72 // IsRemoteMode returns true if CLI is targeting a remote Gordon instance.72 // IsRemoteMode returns true if CLI is targeting a remote Gordon instance.
73 func IsRemoteMode() bool {73 func IsRemoteMode() bool {
74- _, _, isRemote := remote.ResolveTarget(targetFlag, tokenFlag)74+ _, _, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)
75 return isRemote75 return isRemote
76 }76 }
77 77
D internal/adapters/in/cli/targets.go +0 -246
@@ -1,246 +0,0 @@
1-package cli
2-
3-import (
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-// newTargetsCmd creates the targets command group.
14-func newTargetsCmd() *cobra.Command {
15- cmd := &cobra.Command{
16- Use: "targets",
17- Short: "Manage saved remote targets",
18- Long: `Manage saved remote Gordon targets.
19-
20-Targets allow you to save frequently used remote Gordon instances
21-and quickly switch between them.
22-
23-Configuration is stored in ~/.config/gordon/targets.toml`,
24- }
25-
26- cmd.AddCommand(newTargetsListCmd())
27- cmd.AddCommand(newTargetsAddCmd())
28- cmd.AddCommand(newTargetsRemoveCmd())
29- cmd.AddCommand(newTargetsUseCmd())
30-
31- return cmd
32-}
33-
34-// newTargetsListCmd creates the targets list command.
35-func newTargetsListCmd() *cobra.Command {
36- return &cobra.Command{
37- Use: "list",
38- Short: "List saved targets",
39- RunE: func(cmd *cobra.Command, args []string) error {
40- targets, active, err := remote.ListTargets()
41- if err != nil {
42- return fmt.Errorf("failed to list targets: %w", err)
43- }
44-
45- if len(targets) == 0 {
46- fmt.Println(styles.Theme.Muted.Render("No targets configured"))
47- fmt.Println()
48- fmt.Println("Add a target with:")
49- fmt.Println(styles.Theme.Bold.Render(" gordon targets add <name> <url>"))
50- return nil
51- }
52-
53- fmt.Println(styles.Theme.Title.Render("Saved Targets"))
54- fmt.Println()
55-
56- // Build table rows
57- rows := make([][]string, 0, len(targets))
58- for name, target := range targets {
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 target.Token != "" {
67- tokenStatus = styles.Theme.Success.Render("set")
68- } else if target.TokenEnv != "" {
69- tokenStatus = fmt.Sprintf("$%s", target.TokenEnv)
70- }
71-
72- rows = append(rows, []string{name, target.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-// newTargetsAddCmd creates the targets add command.
94-func newTargetsAddCmd() *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 target",
101- Long: `Add a new saved target.
102-
103-Examples:
104- gordon targets add prod https://gordon.mydomain.com
105- gordon targets add prod https://gordon.mydomain.com --token eyJ...
106- gordon targets 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 := addTargetWithEnv(name, url, tokenEnv); err != nil {
117- return fmt.Errorf("failed to add target: %w", err)
118- }
119- } else {
120- if err := remote.AddTarget(name, url, finalToken); err != nil {
121- return fmt.Errorf("failed to add target: %w", err)
122- }
123- }
124-
125- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Target 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-// addTargetWithEnv adds a target with a token environment variable reference.
142-func addTargetWithEnv(name, url, tokenEnv string) error {
143- config, err := remote.LoadTargets("")
144- if err != nil {
145- return err
146- }
147-
148- config.Targets[name] = remote.TargetEntry{
149- URL: url,
150- TokenEnv: tokenEnv,
151- }
152-
153- return remote.SaveTargets("", config)
154-}
155-
156-// newTargetsRemoveCmd creates the targets remove command.
157-func newTargetsRemoveCmd() *cobra.Command {
158- var force bool
159-
160- cmd := &cobra.Command{
161- Use: "remove <name>",
162- Aliases: []string{"rm"},
163- Short: "Remove a target",
164- Args: cobra.ExactArgs(1),
165- RunE: func(cmd *cobra.Command, args []string) error {
166- name := args[0]
167-
168- // Check if target exists
169- targets, active, err := remote.ListTargets()
170- if err != nil {
171- return fmt.Errorf("failed to list targets: %w", err)
172- }
173-
174- if _, exists := targets[name]; !exists {
175- fmt.Println(styles.RenderError(fmt.Sprintf("Target '%s' not found", name)))
176- return nil
177- }
178-
179- // Confirm unless --force
180- if !force {
181- message := fmt.Sprintf("Remove target '%s'?", name)
182- if name == active {
183- message = fmt.Sprintf("Remove target '%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.RemoveTarget(name); err != nil {
197- return fmt.Errorf("failed to remove target: %w", err)
198- }
199-
200- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Target 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-// newTargetsUseCmd creates the targets use command.
211-func newTargetsUseCmd() *cobra.Command {
212- return &cobra.Command{
213- Use: "use <name>",
214- Short: "Set the active target",
215- Long: `Set a target as the active default.
216-
217-When a target is active, it will be used automatically for remote commands
218-without needing to specify --target.
219-
220-Examples:
221- gordon targets use prod
222- gordon routes list # Uses prod target automatically`,
223- Args: cobra.ExactArgs(1),
224- RunE: func(cmd *cobra.Command, args []string) error {
225- name := args[0]
226-
227- if err := remote.SetActiveTarget(name); err != nil {
228- if err.Error() == fmt.Sprintf("target '%s' not found", name) {
229- fmt.Println(styles.RenderError(fmt.Sprintf("Target '%s' not found", name)))
230- fmt.Println()
231- fmt.Println("Available targets:")
232-
233- targets, _, _ := remote.ListTargets()
234- for targetName := range targets {
235- fmt.Printf(" %s\n", targetName)
236- }
237- return nil
238- }
239- return fmt.Errorf("failed to set active target: %w", err)
240- }
241-
242- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Active target set to '%s'", name)))
243- return nil
244- },
245- }
246-}
@@ -1,246 +0,0 @@
1-package cli
2-
3-import (
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-// newTargetsCmd creates the targets command group.
14-func newTargetsCmd() *cobra.Command {
15- cmd := &cobra.Command{
16- Use: "targets",
17- Short: "Manage saved remote targets",
18- Long: `Manage saved remote Gordon targets.
19-
20-Targets allow you to save frequently used remote Gordon instances
21-and quickly switch between them.
22-
23-Configuration is stored in ~/.config/gordon/targets.toml`,
24- }
25-
26- cmd.AddCommand(newTargetsListCmd())
27- cmd.AddCommand(newTargetsAddCmd())
28- cmd.AddCommand(newTargetsRemoveCmd())
29- cmd.AddCommand(newTargetsUseCmd())
30-
31- return cmd
32-}
33-
34-// newTargetsListCmd creates the targets list command.
35-func newTargetsListCmd() *cobra.Command {
36- return &cobra.Command{
37- Use: "list",
38- Short: "List saved targets",
39- RunE: func(cmd *cobra.Command, args []string) error {
40- targets, active, err := remote.ListTargets()
41- if err != nil {
42- return fmt.Errorf("failed to list targets: %w", err)
43- }
44-
45- if len(targets) == 0 {
46- fmt.Println(styles.Theme.Muted.Render("No targets configured"))
47- fmt.Println()
48- fmt.Println("Add a target with:")
49- fmt.Println(styles.Theme.Bold.Render(" gordon targets add <name> <url>"))
50- return nil
51- }
52-
53- fmt.Println(styles.Theme.Title.Render("Saved Targets"))
54- fmt.Println()
55-
56- // Build table rows
57- rows := make([][]string, 0, len(targets))
58- for name, target := range targets {
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 target.Token != "" {
67- tokenStatus = styles.Theme.Success.Render("set")
68- } else if target.TokenEnv != "" {
69- tokenStatus = fmt.Sprintf("$%s", target.TokenEnv)
70- }
71-
72- rows = append(rows, []string{name, target.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-// newTargetsAddCmd creates the targets add command.
94-func newTargetsAddCmd() *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 target",
101- Long: `Add a new saved target.
102-
103-Examples:
104- gordon targets add prod https://gordon.mydomain.com
105- gordon targets add prod https://gordon.mydomain.com --token eyJ...
106- gordon targets 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 := addTargetWithEnv(name, url, tokenEnv); err != nil {
117- return fmt.Errorf("failed to add target: %w", err)
118- }
119- } else {
120- if err := remote.AddTarget(name, url, finalToken); err != nil {
121- return fmt.Errorf("failed to add target: %w", err)
122- }
123- }
124-
125- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Target 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-// addTargetWithEnv adds a target with a token environment variable reference.
142-func addTargetWithEnv(name, url, tokenEnv string) error {
143- config, err := remote.LoadTargets("")
144- if err != nil {
145- return err
146- }
147-
148- config.Targets[name] = remote.TargetEntry{
149- URL: url,
150- TokenEnv: tokenEnv,
151- }
152-
153- return remote.SaveTargets("", config)
154-}
155-
156-// newTargetsRemoveCmd creates the targets remove command.
157-func newTargetsRemoveCmd() *cobra.Command {
158- var force bool
159-
160- cmd := &cobra.Command{
161- Use: "remove <name>",
162- Aliases: []string{"rm"},
163- Short: "Remove a target",
164- Args: cobra.ExactArgs(1),
165- RunE: func(cmd *cobra.Command, args []string) error {
166- name := args[0]
167-
168- // Check if target exists
169- targets, active, err := remote.ListTargets()
170- if err != nil {
171- return fmt.Errorf("failed to list targets: %w", err)
172- }
173-
174- if _, exists := targets[name]; !exists {
175- fmt.Println(styles.RenderError(fmt.Sprintf("Target '%s' not found", name)))
176- return nil
177- }
178-
179- // Confirm unless --force
180- if !force {
181- message := fmt.Sprintf("Remove target '%s'?", name)
182- if name == active {
183- message = fmt.Sprintf("Remove target '%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.RemoveTarget(name); err != nil {
197- return fmt.Errorf("failed to remove target: %w", err)
198- }
199-
200- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Target 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-// newTargetsUseCmd creates the targets use command.
211-func newTargetsUseCmd() *cobra.Command {
212- return &cobra.Command{
213- Use: "use <name>",
214- Short: "Set the active target",
215- Long: `Set a target as the active default.
216-
217-When a target is active, it will be used automatically for remote commands
218-without needing to specify --target.
219-
220-Examples:
221- gordon targets use prod
222- gordon routes list # Uses prod target automatically`,
223- Args: cobra.ExactArgs(1),
224- RunE: func(cmd *cobra.Command, args []string) error {
225- name := args[0]
226-
227- if err := remote.SetActiveTarget(name); err != nil {
228- if err.Error() == fmt.Sprintf("target '%s' not found", name) {
229- fmt.Println(styles.RenderError(fmt.Sprintf("Target '%s' not found", name)))
230- fmt.Println()
231- fmt.Println("Available targets:")
232-
233- targets, _, _ := remote.ListTargets()
234- for targetName := range targets {
235- fmt.Printf(" %s\n", targetName)
236- }
237- return nil
238- }
239- return fmt.Errorf("failed to set active target: %w", err)
240- }
241-
242- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Active target set to '%s'", name)))
243- return nil
244- },
245- }
246-}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 106

↑↓ moveenter openesc close