hookdeck-cli 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.github/workflows/publish-npm.yml +19 -0
  2. package/.github/workflows/release.yml +85 -0
  3. package/.goreleaser/linux.yml +68 -0
  4. package/.goreleaser/mac.yml +69 -0
  5. package/.goreleaser/windows.yml +52 -0
  6. package/.tool-versions +1 -0
  7. package/Dockerfile +5 -0
  8. package/LICENSE +202 -0
  9. package/README.md +223 -0
  10. package/docs/cli-demo.gif +0 -0
  11. package/go.mod +58 -0
  12. package/go.sum +444 -0
  13. package/main.go +22 -0
  14. package/package.json +30 -0
  15. package/pkg/ansi/ansi.go +208 -0
  16. package/pkg/ansi/init_windows.go +23 -0
  17. package/pkg/cmd/completion.go +128 -0
  18. package/pkg/cmd/listen.go +114 -0
  19. package/pkg/cmd/login.go +37 -0
  20. package/pkg/cmd/logout.go +35 -0
  21. package/pkg/cmd/root.go +112 -0
  22. package/pkg/cmd/version.go +25 -0
  23. package/pkg/cmd/whoami.go +50 -0
  24. package/pkg/config/config.go +326 -0
  25. package/pkg/config/config_test.go +20 -0
  26. package/pkg/config/profile.go +296 -0
  27. package/pkg/config/profile_test.go +109 -0
  28. package/pkg/hookdeck/client.go +210 -0
  29. package/pkg/hookdeck/client_test.go +203 -0
  30. package/pkg/hookdeck/connections.go +61 -0
  31. package/pkg/hookdeck/destinations.go +14 -0
  32. package/pkg/hookdeck/guest.go +37 -0
  33. package/pkg/hookdeck/session.go +37 -0
  34. package/pkg/hookdeck/sources.go +73 -0
  35. package/pkg/hookdeck/telemetry.go +84 -0
  36. package/pkg/hookdeck/telemetry_test.go +35 -0
  37. package/pkg/hookdeck/verbosetransport.go +82 -0
  38. package/pkg/hookdeck/verbosetransport_test.go +47 -0
  39. package/pkg/listen/connection.go +91 -0
  40. package/pkg/listen/listen.go +140 -0
  41. package/pkg/listen/source.go +84 -0
  42. package/pkg/login/client_login.go +209 -0
  43. package/pkg/login/interactive_login.go +180 -0
  44. package/pkg/login/login_message.go +24 -0
  45. package/pkg/login/poll.go +78 -0
  46. package/pkg/login/validate.go +52 -0
  47. package/pkg/logout/logout.go +48 -0
  48. package/pkg/open/open.go +50 -0
  49. package/pkg/proxy/proxy.go +433 -0
  50. package/pkg/useragent/uname_unix.go +25 -0
  51. package/pkg/useragent/uname_unix_test.go +24 -0
  52. package/pkg/useragent/uname_windows.go +9 -0
  53. package/pkg/useragent/useragent.go +74 -0
  54. package/pkg/validators/cmds.go +71 -0
  55. package/pkg/validators/validate.go +144 -0
  56. package/pkg/version/version.go +58 -0
  57. package/pkg/version/version_test.go +15 -0
  58. package/pkg/websocket/attempt_messages.go +47 -0
  59. package/pkg/websocket/client.go +525 -0
  60. package/pkg/websocket/connection_messages.go +11 -0
  61. package/pkg/websocket/messages.go +64 -0
@@ -0,0 +1,50 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "github.com/hookdeck/hookdeck-cli/pkg/ansi"
8
+ "github.com/hookdeck/hookdeck-cli/pkg/login"
9
+ "github.com/hookdeck/hookdeck-cli/pkg/validators"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ type whoamiCmd struct {
14
+ cmd *cobra.Command
15
+ interactive bool
16
+ }
17
+
18
+ func newWhoamiCmd() *whoamiCmd {
19
+ lc := &whoamiCmd{}
20
+
21
+ lc.cmd = &cobra.Command{
22
+ Use: "whoami",
23
+ Args: validators.NoArgs,
24
+ Short: "Show the logged-in user",
25
+ RunE: lc.runWhoamiCmd,
26
+ }
27
+
28
+ return lc
29
+ }
30
+
31
+ func (lc *whoamiCmd) runWhoamiCmd(cmd *cobra.Command, args []string) error {
32
+ key, err := Config.Profile.GetAPIKey()
33
+ if err != nil {
34
+ return err
35
+ }
36
+ response, err := login.ValidateKey(Config.APIBaseURL, key)
37
+ if err != nil {
38
+ return err
39
+ }
40
+
41
+ color := ansi.Color(os.Stdout)
42
+
43
+ fmt.Printf(
44
+ "Logged in as %s in workspace %s\n",
45
+ color.Bold(response.UserName),
46
+ color.Bold(response.TeamName),
47
+ )
48
+
49
+ return nil
50
+ }
@@ -0,0 +1,326 @@
1
+ package config
2
+
3
+ import (
4
+ "bytes"
5
+ "fmt"
6
+ "io/ioutil"
7
+ "os"
8
+ "os/exec"
9
+ "path/filepath"
10
+ "runtime"
11
+ "strings"
12
+ "time"
13
+
14
+ "github.com/BurntSushi/toml"
15
+ "github.com/mitchellh/go-homedir"
16
+ log "github.com/sirupsen/logrus"
17
+ "github.com/spf13/viper"
18
+ prefixed "github.com/x-cray/logrus-prefixed-formatter"
19
+
20
+ "github.com/hookdeck/hookdeck-cli/pkg/ansi"
21
+ )
22
+
23
+ // ColorOn represnets the on-state for colors
24
+ const ColorOn = "on"
25
+
26
+ // ColorOff represents the off-state for colors
27
+ const ColorOff = "off"
28
+
29
+ // ColorAuto represents the auto-state for colors
30
+ const ColorAuto = "auto"
31
+
32
+ // Config handles all overall configuration for the CLI
33
+ type Config struct {
34
+ Color string
35
+ LogLevel string
36
+ Profile Profile
37
+ ProfilesFile string
38
+ APIBaseURL string
39
+ DashboardBaseURL string
40
+ ConsoleBaseURL string
41
+ Insecure bool
42
+ }
43
+
44
+ // GetConfigFolder retrieves the folder where the profiles file is stored
45
+ // It searches for the xdg environment path first and will secondarily
46
+ // place it in the home directory
47
+ func (c *Config) GetConfigFolder(xdgPath string) string {
48
+ configPath := xdgPath
49
+
50
+ log.WithFields(log.Fields{
51
+ "prefix": "config.Config.GetProfilesFolder",
52
+ "path": configPath,
53
+ }).Debug("Using profiles file")
54
+
55
+ if configPath == "" {
56
+ home, err := homedir.Dir()
57
+ if err != nil {
58
+ fmt.Println(err)
59
+ os.Exit(1)
60
+ }
61
+
62
+ configPath = filepath.Join(home, ".config")
63
+ }
64
+
65
+ return filepath.Join(configPath, "hookdeck")
66
+ }
67
+
68
+ // InitConfig reads in profiles file and ENV variables if set.
69
+ func (c *Config) InitConfig() {
70
+ logFormatter := &prefixed.TextFormatter{
71
+ FullTimestamp: true,
72
+ TimestampFormat: time.RFC1123,
73
+ }
74
+
75
+ c.Profile.Config = c
76
+
77
+ if c.ProfilesFile != "" {
78
+ viper.SetConfigFile(c.ProfilesFile)
79
+ } else {
80
+ configFolder := c.GetConfigFolder(os.Getenv("XDG_CONFIG_HOME"))
81
+ configFile := filepath.Join(configFolder, "config.toml")
82
+ c.ProfilesFile = configFile
83
+ viper.SetConfigType("toml")
84
+ viper.SetConfigFile(configFile)
85
+ viper.SetConfigPermissions(os.FileMode(0600))
86
+
87
+ // Try to change permissions manually, because we used to create files
88
+ // with default permissions (0644)
89
+ err := os.Chmod(configFile, os.FileMode(0600))
90
+ if err != nil && !os.IsNotExist(err) {
91
+ log.Fatalf("%s", err)
92
+ }
93
+ }
94
+
95
+ // If a profiles file is found, read it in.
96
+ if err := viper.ReadInConfig(); err == nil {
97
+ log.WithFields(log.Fields{
98
+ "prefix": "config.Config.InitConfig",
99
+ "path": viper.ConfigFileUsed(),
100
+ }).Debug("Using profiles file")
101
+ }
102
+
103
+ if c.Profile.DeviceName == "" {
104
+ deviceName, err := os.Hostname()
105
+ if err != nil {
106
+ deviceName = "unknown"
107
+ }
108
+
109
+ c.Profile.DeviceName = deviceName
110
+ }
111
+
112
+ color, err := c.Profile.GetColor()
113
+ if err != nil {
114
+ log.Fatalf("%s", err)
115
+ }
116
+
117
+ switch color {
118
+ case ColorOn:
119
+ ansi.ForceColors = true
120
+ logFormatter.ForceColors = true
121
+ case ColorOff:
122
+ ansi.DisableColors = true
123
+ logFormatter.DisableColors = true
124
+ case ColorAuto:
125
+ // Nothing to do
126
+ default:
127
+ log.Fatalf("Unrecognized color value: %s. Expected one of on, off, auto.", c.Color)
128
+ }
129
+
130
+ log.SetFormatter(logFormatter)
131
+
132
+ // Set log level
133
+ switch c.LogLevel {
134
+ case "debug":
135
+ log.SetLevel(log.DebugLevel)
136
+ case "info":
137
+ log.SetLevel(log.InfoLevel)
138
+ case "warn":
139
+ log.SetLevel(log.WarnLevel)
140
+ case "error":
141
+ log.SetLevel(log.ErrorLevel)
142
+ default:
143
+ log.Fatalf("Unrecognized log level value: %s. Expected one of debug, info, warn, error.", c.LogLevel)
144
+ }
145
+ }
146
+
147
+ // EditConfig opens the configuration file in the default editor.
148
+ func (c *Config) EditConfig() error {
149
+ var err error
150
+
151
+ fmt.Println("Opening config file:", c.ProfilesFile)
152
+
153
+ switch runtime.GOOS {
154
+ case "darwin", "linux":
155
+ editor := os.Getenv("EDITOR")
156
+ if editor == "" {
157
+ editor = "vi"
158
+ }
159
+
160
+ cmd := exec.Command(editor, c.ProfilesFile)
161
+ // Some editors detect whether they have control of stdin/out and will
162
+ // fail if they do not.
163
+ cmd.Stdin = os.Stdin
164
+ cmd.Stdout = os.Stdout
165
+
166
+ return cmd.Run()
167
+ case "windows":
168
+ // As far as I can tell, Windows doesn't have an easily accesible or
169
+ // comparable option to $EDITOR, so default to notepad for now
170
+ err = exec.Command("notepad", c.ProfilesFile).Run()
171
+ default:
172
+ err = fmt.Errorf("unsupported platform")
173
+ }
174
+
175
+ return err
176
+ }
177
+
178
+ // PrintConfig outputs the contents of the configuration file.
179
+ func (c *Config) PrintConfig() error {
180
+ if c.Profile.ProfileName == "default" {
181
+ configFile, err := ioutil.ReadFile(c.ProfilesFile)
182
+ if err != nil {
183
+ return err
184
+ }
185
+
186
+ fmt.Print(string(configFile))
187
+ } else {
188
+ configs := viper.GetStringMapString(c.Profile.ProfileName)
189
+
190
+ if len(configs) > 0 {
191
+ fmt.Printf("[%s]\n", c.Profile.ProfileName)
192
+ for field, value := range configs {
193
+ fmt.Printf(" %s=%s\n", field, value)
194
+ }
195
+ }
196
+ }
197
+
198
+ return nil
199
+ }
200
+
201
+ // RemoveProfile removes the profile whose name matches the provided
202
+ // profileName from the config file.
203
+ func (c *Config) RemoveProfile(profileName string) error {
204
+ runtimeViper := viper.GetViper()
205
+ var err error
206
+
207
+ for field, value := range runtimeViper.AllSettings() {
208
+ if isProfile(value) && field == profileName {
209
+ runtimeViper, err = removeKey(runtimeViper, field)
210
+ if err != nil {
211
+ return err
212
+ }
213
+ }
214
+ }
215
+
216
+ return syncConfig(runtimeViper)
217
+ }
218
+
219
+ // RemoveAllProfiles removes all the profiles from the config file.
220
+ func (c *Config) RemoveAllProfiles() error {
221
+ runtimeViper := viper.GetViper()
222
+ var err error
223
+
224
+ for field, value := range runtimeViper.AllSettings() {
225
+ if isProfile(value) {
226
+ runtimeViper, err = removeKey(runtimeViper, field)
227
+ if err != nil {
228
+ return err
229
+ }
230
+ }
231
+ }
232
+
233
+ return syncConfig(runtimeViper)
234
+ }
235
+
236
+ // isProfile identifies whether a value in the config pertains to a profile.
237
+ func isProfile(value interface{}) bool {
238
+ // TODO: ianjabour - ideally find a better way to identify projects in config
239
+ _, ok := value.(map[string]interface{})
240
+ return ok
241
+ }
242
+
243
+ // syncConfig merges a runtimeViper instance with the config file being used.
244
+ func syncConfig(runtimeViper *viper.Viper) error {
245
+ runtimeViper.MergeInConfig()
246
+ profilesFile := viper.ConfigFileUsed()
247
+ runtimeViper.SetConfigFile(profilesFile)
248
+ // Ensure we preserve the config file type
249
+ runtimeViper.SetConfigType(filepath.Ext(profilesFile))
250
+
251
+ err := runtimeViper.WriteConfig()
252
+ if err != nil {
253
+ return err
254
+ }
255
+
256
+ return nil
257
+ }
258
+
259
+ // Temporary workaround until https://github.com/spf13/viper/pull/519 can remove a key from viper
260
+ func removeKey(v *viper.Viper, key string) (*viper.Viper, error) {
261
+ configMap := v.AllSettings()
262
+ path := strings.Split(key, ".")
263
+ lastKey := strings.ToLower(path[len(path)-1])
264
+ deepestMap := deepSearch(configMap, path[0:len(path)-1])
265
+ delete(deepestMap, lastKey)
266
+
267
+ buf := new(bytes.Buffer)
268
+
269
+ encodeErr := toml.NewEncoder(buf).Encode(configMap)
270
+ if encodeErr != nil {
271
+ return nil, encodeErr
272
+ }
273
+
274
+ nv := viper.New()
275
+ nv.SetConfigType("toml") // hint to viper that we've encoded the data as toml
276
+
277
+ err := nv.ReadConfig(buf)
278
+ if err != nil {
279
+ return nil, err
280
+ }
281
+
282
+ return nv, nil
283
+ }
284
+
285
+ func makePath(path string) error {
286
+ dir := filepath.Dir(path)
287
+
288
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
289
+ err = os.MkdirAll(dir, os.ModePerm)
290
+ if err != nil {
291
+ return err
292
+ }
293
+ }
294
+
295
+ return nil
296
+ }
297
+
298
+ // taken from https://github.com/spf13/viper/blob/master/util.go#L199,
299
+ // we need this to delete configs, remove when viper supprts unset natively
300
+ func deepSearch(m map[string]interface{}, path []string) map[string]interface{} {
301
+ for _, k := range path {
302
+ m2, ok := m[k]
303
+ if !ok {
304
+ // intermediate key does not exist
305
+ // => create it and continue from there
306
+ m3 := make(map[string]interface{})
307
+ m[k] = m3
308
+ m = m3
309
+
310
+ continue
311
+ }
312
+
313
+ m3, ok := m2.(map[string]interface{})
314
+ if !ok {
315
+ // intermediate key is a value
316
+ // => replace with a new map
317
+ m3 = make(map[string]interface{})
318
+ m[k] = m3
319
+ }
320
+
321
+ // continue search from here
322
+ m = m3
323
+ }
324
+
325
+ return m
326
+ }
@@ -0,0 +1,20 @@
1
+ package config
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/spf13/viper"
7
+ "github.com/stretchr/testify/require"
8
+ )
9
+
10
+ func TestRemoveKey(t *testing.T) {
11
+ v := viper.New()
12
+ v.Set("remove", "me")
13
+ v.Set("stay", "here")
14
+
15
+ nv, err := removeKey(v, "remove")
16
+ require.NoError(t, err)
17
+
18
+ require.EqualValues(t, []string{"stay"}, nv.AllKeys())
19
+ require.ElementsMatch(t, []string{"stay", "remove"}, v.AllKeys())
20
+ }
@@ -0,0 +1,296 @@
1
+ package config
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io/ioutil"
8
+ "net/url"
9
+ "os"
10
+ "path/filepath"
11
+ "strings"
12
+
13
+ "github.com/spf13/viper"
14
+
15
+ "github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
16
+ "github.com/hookdeck/hookdeck-cli/pkg/validators"
17
+ )
18
+
19
+ // Profile handles all things related to managing the project specific configurations
20
+ type Profile struct {
21
+ Config *Config
22
+ DeviceName string
23
+ ProfileName string
24
+ TeamName string
25
+ TeamID string
26
+ TeamMode string
27
+ APIKey string
28
+ ClientID string
29
+ DisplayName string
30
+ }
31
+
32
+ type partialPollResponse struct {
33
+ TeamName string `json:"team_name"`
34
+ }
35
+
36
+ // CreateProfile creates a profile when logging in
37
+ func (p *Profile) CreateProfile() error {
38
+ writeErr := p.writeProfile(viper.GetViper())
39
+ if writeErr != nil {
40
+ return writeErr
41
+ }
42
+
43
+ return nil
44
+ }
45
+
46
+ // GetColor gets the color setting for the user based on the flag or the
47
+ // persisted color stored in the config file
48
+ func (p *Profile) GetColor() (string, error) {
49
+ color := viper.GetString("color")
50
+ if color != "" {
51
+ return color, nil
52
+ }
53
+
54
+ color = viper.GetString(p.GetConfigField("color"))
55
+ switch color {
56
+ case "", ColorAuto:
57
+ return ColorAuto, nil
58
+ case ColorOn:
59
+ return ColorOn, nil
60
+ case ColorOff:
61
+ return ColorOff, nil
62
+ default:
63
+ return "", fmt.Errorf("color value not supported: %s", color)
64
+ }
65
+ }
66
+
67
+ // GetDeviceName returns the configured device name
68
+ func (p *Profile) GetDeviceName() (string, error) {
69
+ if os.Getenv("HOOKDECK_DEVICE_NAME") != "" {
70
+ return os.Getenv("HOOKDECK_DEVICE_NAME"), nil
71
+ }
72
+
73
+ if p.DeviceName != "" {
74
+ return p.DeviceName, nil
75
+ }
76
+
77
+ if err := viper.ReadInConfig(); err == nil {
78
+ return viper.GetString(p.GetConfigField("device_name")), nil
79
+ }
80
+
81
+ return "", validators.ErrDeviceNameNotConfigured
82
+ }
83
+
84
+ // GetAPIKey will return the existing key for the given profile
85
+ func (p *Profile) GetAPIKey() (string, error) {
86
+ envKey := os.Getenv("HOOKDECK_CLI_KEY")
87
+ if envKey != "" {
88
+ err := validators.APIKey(envKey)
89
+ if err != nil {
90
+ return "", err
91
+ }
92
+
93
+ return envKey, nil
94
+ }
95
+
96
+ if p.APIKey != "" {
97
+ err := validators.APIKey(p.APIKey)
98
+ if err != nil {
99
+ return "", err
100
+ }
101
+
102
+ return p.APIKey, nil
103
+ }
104
+
105
+ // Try to fetch the API key from the configuration file
106
+ if err := viper.ReadInConfig(); err == nil {
107
+ key := viper.GetString(p.GetConfigField("api_key"))
108
+
109
+ err := validators.APIKey(key)
110
+ if err != nil {
111
+ return "", err
112
+ }
113
+
114
+ return key, nil
115
+ }
116
+
117
+ return "", validators.ErrAPIKeyNotConfigured
118
+ }
119
+
120
+ // GetDisplayName returns the account display name of the user
121
+ func (p *Profile) GetDisplayName() string {
122
+ if err := viper.ReadInConfig(); err == nil {
123
+ return viper.GetString(p.GetConfigField("display_name"))
124
+ }
125
+
126
+ return ""
127
+ }
128
+
129
+ func (p *Profile) GetTeamMode() string {
130
+ if p.TeamMode != "" {
131
+ return p.TeamMode
132
+ }
133
+ if err := viper.ReadInConfig(); err == nil {
134
+ return viper.GetString(p.GetConfigField("team_mode"))
135
+ }
136
+
137
+ return ""
138
+ }
139
+
140
+ func (p *Profile) GetTeamId() string {
141
+ if p.TeamID != "" {
142
+ return p.TeamID
143
+ }
144
+ if err := viper.ReadInConfig(); err == nil {
145
+ return viper.GetString(p.GetConfigField("team_id"))
146
+ }
147
+
148
+ return ""
149
+ }
150
+
151
+ func (p *Profile) refreshTeamName() string {
152
+ apiKey, err := p.GetAPIKey()
153
+ if err != nil {
154
+ panic(err)
155
+ }
156
+
157
+ parsedURL, err := url.Parse(p.Config.APIBaseURL + "/cli-auth/poll?key=" + apiKey)
158
+ if err != nil {
159
+ panic(err)
160
+ }
161
+
162
+ client := &hookdeck.Client{
163
+ BaseURL: parsedURL,
164
+ }
165
+
166
+ res, err := client.Get(context.TODO(), parsedURL.Path, parsedURL.Query().Encode(), nil)
167
+ if err != nil {
168
+ panic(err)
169
+ }
170
+
171
+ defer res.Body.Close()
172
+
173
+ body, err := ioutil.ReadAll(res.Body)
174
+ if err != nil {
175
+ panic(err)
176
+ }
177
+
178
+ var response partialPollResponse
179
+
180
+ err = json.Unmarshal(body, &response)
181
+ if err != nil {
182
+ panic(err)
183
+ }
184
+
185
+ p.TeamName = response.TeamName
186
+ p.CreateProfile()
187
+
188
+ return response.TeamName
189
+ }
190
+
191
+ // GetDisplayName returns the account display name of the team
192
+ func (p *Profile) GetTeamName() string {
193
+ if err := viper.ReadInConfig(); err != nil {
194
+ return ""
195
+ }
196
+
197
+ teamName := viper.GetString(p.GetConfigField("team_name"))
198
+
199
+ if teamName == p.GetDisplayName() {
200
+ // Could be a bug where we used to store the display name in the
201
+ // team name field. But it could also be a legit team name that
202
+ // just happens to be the same as the user display name.
203
+ //
204
+ // Call the CLI poll endpoint to make sure.
205
+ teamName = p.refreshTeamName()
206
+ }
207
+
208
+ return teamName
209
+ }
210
+
211
+ // GetTerminalPOSDeviceID returns the device id from the config for Terminal quickstart to use
212
+ func (p *Profile) GetTerminalPOSDeviceID() string {
213
+ if err := viper.ReadInConfig(); err == nil {
214
+ return viper.GetString(p.GetConfigField("terminal_pos_device_id"))
215
+ }
216
+
217
+ return ""
218
+ }
219
+
220
+ // GetConfigField returns the configuration field for the specific profile
221
+ func (p *Profile) GetConfigField(field string) string {
222
+ return p.ProfileName + "." + field
223
+ }
224
+
225
+ // RegisterAlias registers an alias for a given key.
226
+ func (p *Profile) RegisterAlias(alias, key string) {
227
+ viper.RegisterAlias(p.GetConfigField(alias), p.GetConfigField(key))
228
+ }
229
+
230
+ // WriteConfigField updates a configuration field and writes the updated
231
+ // configuration to disk.
232
+ func (p *Profile) WriteConfigField(field, value string) error {
233
+ viper.Set(p.GetConfigField(field), value)
234
+ return viper.WriteConfig()
235
+ }
236
+
237
+ // DeleteConfigField deletes a configuration field.
238
+ func (p *Profile) DeleteConfigField(field string) error {
239
+ v, err := removeKey(viper.GetViper(), p.GetConfigField(field))
240
+ if err != nil {
241
+ return err
242
+ }
243
+
244
+ return p.writeProfile(v)
245
+ }
246
+
247
+ func (p *Profile) writeProfile(runtimeViper *viper.Viper) error {
248
+ profilesFile := viper.ConfigFileUsed()
249
+
250
+ err := makePath(profilesFile)
251
+ if err != nil {
252
+ return err
253
+ }
254
+
255
+ if p.DeviceName != "" {
256
+ runtimeViper.Set(p.GetConfigField("device_name"), strings.TrimSpace(p.DeviceName))
257
+ }
258
+
259
+ if p.APIKey != "" {
260
+ runtimeViper.Set(p.GetConfigField("api_key"), strings.TrimSpace(p.APIKey))
261
+ }
262
+
263
+ if p.ClientID != "" {
264
+ runtimeViper.Set(p.GetConfigField("client_id"), strings.TrimSpace(p.ClientID))
265
+ }
266
+
267
+ if p.DisplayName != "" {
268
+ runtimeViper.Set(p.GetConfigField("display_name"), strings.TrimSpace(p.DisplayName))
269
+ }
270
+
271
+ if p.TeamName != "" {
272
+ runtimeViper.Set(p.GetConfigField("team_name"), strings.TrimSpace(p.TeamName))
273
+ }
274
+
275
+ if p.TeamID != "" {
276
+ runtimeViper.Set(p.GetConfigField("team_id"), strings.TrimSpace(p.TeamID))
277
+ }
278
+
279
+ if p.TeamMode != "" {
280
+ runtimeViper.Set(p.GetConfigField("team_mode"), strings.TrimSpace(p.TeamMode))
281
+ }
282
+
283
+ runtimeViper.MergeInConfig()
284
+
285
+ runtimeViper.SetConfigFile(profilesFile)
286
+
287
+ // Ensure we preserve the config file type
288
+ runtimeViper.SetConfigType(filepath.Ext(profilesFile))
289
+
290
+ err = runtimeViper.WriteConfig()
291
+ if err != nil {
292
+ return err
293
+ }
294
+
295
+ return nil
296
+ }