1package remote
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7
8 "github.com/pelletier/go-toml/v2"
9)
10
11// ClientConfig represents the client-mode configuration.
12type ClientConfig struct {
13 Client ClientSettings `toml:"client"`
14 Remotes map[string]RemoteEntry `toml:"remotes"`
15 Active string `toml:"active"` // Active remote name
16}
17
18// ClientSettings represents the [client] section.
19type ClientSettings struct {
20 Mode string `toml:"mode"` // "local" (default) or "remote"
21 Remote string `toml:"remote"` // Remote Gordon URL
22 Token string `toml:"token"` // Auth token
23 TokenEnv string `toml:"token_env"` // Env var name for token
24}
25
26// RemoteEntry represents a saved remote in [remotes.*].
27type RemoteEntry struct {
28 URL string `toml:"url"`
29 Token string `toml:"token,omitempty"`
30 TokenEnv string `toml:"token_env,omitempty"`
31}
32
33// DefaultClientConfigPath returns the default client config path.
34func DefaultClientConfigPath() string {
35 configDir, err := os.UserConfigDir()
36 if err != nil {
37 configDir = os.Getenv("HOME")
38 }
39 return filepath.Join(configDir, "gordon", "gordon.toml")
40}
41
42// DefaultRemotesPath returns the default remotes config path.
43func DefaultRemotesPath() string {
44 configDir, err := os.UserConfigDir()
45 if err != nil {
46 configDir = os.Getenv("HOME")
47 }
48 return filepath.Join(configDir, "gordon", "remotes.toml")
49}
50
51// LoadClientConfig loads the client configuration.
52func LoadClientConfig(path string) (*ClientConfig, error) {
53 if path == "" {
54 path = DefaultClientConfigPath()
55 }
56
57 data, err := os.ReadFile(path)
58 if err != nil {
59 if os.IsNotExist(err) {
60 // Return default config
61 return &ClientConfig{
62 Client: ClientSettings{
63 Mode: "local",
64 },
65 Remotes: make(map[string]RemoteEntry),
66 }, nil
67 }
68 return nil, fmt.Errorf("failed to read config: %w", err)
69 }
70
71 var config ClientConfig
72 if err := toml.Unmarshal(data, &config); err != nil {
73 return nil, fmt.Errorf("failed to parse config: %w", err)
74 }
75
76 if config.Remotes == nil {
77 config.Remotes = make(map[string]RemoteEntry)
78 }
79
80 return &config, nil
81}
82
83// LoadRemotes loads the remotes configuration.
84func LoadRemotes(path string) (*ClientConfig, error) {
85 if path == "" {
86 path = DefaultRemotesPath()
87 }
88
89 data, err := os.ReadFile(path)
90 if err != nil {
91 if os.IsNotExist(err) {
92 return &ClientConfig{
93 Remotes: make(map[string]RemoteEntry),
94 }, nil
95 }
96 return nil, fmt.Errorf("failed to read remotes: %w", err)
97 }
98
99 var config ClientConfig
100 if err := toml.Unmarshal(data, &config); err != nil {
101 return nil, fmt.Errorf("failed to parse remotes: %w", err)
102 }
103
104 if config.Remotes == nil {
105 config.Remotes = make(map[string]RemoteEntry)
106 }
107
108 return &config, nil
109}
110
111// SaveRemotes saves the remotes configuration.
112func SaveRemotes(path string, config *ClientConfig) error {
113 if path == "" {
114 path = DefaultRemotesPath()
115 }
116
117 // Ensure directory exists
118 dir := filepath.Dir(path)
119 if err := os.MkdirAll(dir, 0700); err != nil {
120 return fmt.Errorf("failed to create config directory: %w", err)
121 }
122
123 data, err := toml.Marshal(config)
124 if err != nil {
125 return fmt.Errorf("failed to marshal remotes: %w", err)
126 }
127
128 if err := os.WriteFile(path, data, 0600); err != nil {
129 return fmt.Errorf("failed to write remotes: %w", err)
130 }
131
132 return nil
133}
134
135// ResolveRemote resolves the remote URL and token from configuration.
136// Precedence: flag > env > config > active remote
137func ResolveRemote(flagRemote, flagToken string) (url, token string, isRemote bool) {
138 config, _ := LoadClientConfig("")
139 remotes, _ := LoadRemotes("")
140
141 url, isRemote = resolveRemoteURL(flagRemote, config, remotes)
142 if isRemote {
143 token = resolveToken(flagToken, config, remotes)
144 }
145
146 return url, token, isRemote
147}
148
149// resolveRemoteURL resolves the remote URL from various sources.
150func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (string, bool) {
151 // 1. Check flag
152 if flagRemote != "" {
153 return flagRemote, true
154 }
155
156 // 2. Check environment variable
157 if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
158 return envRemote, true
159 }
160
161 // 3. Check client config
162 if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
163 return config.Client.Remote, true
164 }
165
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
170 }
171 }
172
173 return "", false
174}
175
176// resolveToken resolves the authentication token from various sources.
177func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
178 // 1. Flag token takes precedence
179 if flagToken != "" {
180 return flagToken
181 }
182
183 // 2. Environment variable
184 if envToken := os.Getenv("GORDON_TOKEN"); envToken != "" {
185 return envToken
186 }
187
188 // 3. Config token
189 if config != nil {
190 if config.Client.Token != "" {
191 return config.Client.Token
192 }
193 if config.Client.TokenEnv != "" {
194 return os.Getenv(config.Client.TokenEnv)
195 }
196 }
197
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
203 }
204 if remote.TokenEnv != "" {
205 return os.Getenv(remote.TokenEnv)
206 }
207 }
208 }
209
210 return ""
211}
212
213// AddRemote adds a new remote to the remotes configuration.
214func AddRemote(name, url, token string) error {
215 config, err := LoadRemotes("")
216 if err != nil {
217 return err
218 }
219
220 config.Remotes[name] = RemoteEntry{
221 URL: url,
222 Token: token,
223 }
224
225 return SaveRemotes("", config)
226}
227
228// RemoveRemote removes a remote from the remotes configuration.
229func RemoveRemote(name string) error {
230 config, err := LoadRemotes("")
231 if err != nil {
232 return err
233 }
234
235 delete(config.Remotes, name)
236
237 // Clear active if it was the removed remote
238 if config.Active == name {
239 config.Active = ""
240 }
241
242 return SaveRemotes("", config)
243}
244
245// SetActiveRemote sets the active remote.
246func SetActiveRemote(name string) error {
247 config, err := LoadRemotes("")
248 if err != nil {
249 return err
250 }
251
252 // Verify remote exists
253 if _, ok := config.Remotes[name]; !ok {
254 return fmt.Errorf("remote '%s' not found", name)
255 }
256
257 config.Active = name
258
259 return SaveRemotes("", config)
260}
261
262// ListRemotes returns all saved remotes.
263func ListRemotes() (map[string]RemoteEntry, string, error) {
264 config, err := LoadRemotes("")
265 if err != nil {
266 return nil, "", err
267 }
268
269 return config.Remotes, config.Active, nil
270}