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,73 @@
1
+ package hookdeck
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "net/http"
8
+ )
9
+
10
+ type Source struct {
11
+ Id string
12
+ Alias string
13
+ Label string
14
+ Url string
15
+ }
16
+
17
+ type CreateSourceInput struct {
18
+ Alias string `json:"alias"`
19
+ Label string `json:"label"`
20
+ }
21
+
22
+ type SourceList struct {
23
+ Count int
24
+ Models []Source
25
+ }
26
+
27
+ func (c *Client) GetSourceByAlias(alias string) (Source, error) {
28
+ res, err := c.Get(context.Background(), "/sources", "alias="+alias, nil)
29
+ if err != nil {
30
+ return Source{}, err
31
+ }
32
+ if res.StatusCode != http.StatusOK {
33
+ return Source{}, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, err)
34
+ }
35
+ sources := SourceList{}
36
+ postprocessJsonResponse(res, &sources)
37
+
38
+ if len(sources.Models) > 0 {
39
+ return sources.Models[0], nil
40
+ }
41
+ return Source{}, nil
42
+ }
43
+
44
+ func (c *Client) ListSources() ([]Source, error) {
45
+ res, err := c.Get(context.Background(), "/sources", "", nil)
46
+ if err != nil {
47
+ return []Source{}, err
48
+ }
49
+ if res.StatusCode != http.StatusOK {
50
+ return []Source{}, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, err)
51
+ }
52
+ sources := SourceList{}
53
+ postprocessJsonResponse(res, &sources)
54
+
55
+ return sources.Models, nil
56
+ }
57
+
58
+ func (c *Client) CreateSource(input CreateSourceInput) (Source, error) {
59
+ input_bytes, err := json.Marshal(input)
60
+ if err != nil {
61
+ return Source{}, err
62
+ }
63
+ res, err := c.Post(context.Background(), "/sources", input_bytes, nil)
64
+ if err != nil {
65
+ return Source{}, err
66
+ }
67
+ if res.StatusCode != http.StatusOK {
68
+ return Source{}, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, err)
69
+ }
70
+ source := Source{}
71
+ postprocessJsonResponse(res, &source)
72
+ return source, nil
73
+ }
@@ -0,0 +1,84 @@
1
+ package hookdeck
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strings"
6
+ "sync"
7
+
8
+ "github.com/spf13/cobra"
9
+ )
10
+
11
+ //
12
+ // Public types
13
+ //
14
+
15
+ // CLITelemetry is the structure that holds telemetry data sent to Hookdeck in
16
+ // API requests.
17
+ type CLITelemetry struct {
18
+ CommandPath string `json:"command_path"`
19
+ DeviceName string `json:"device_name"`
20
+ GeneratedResource bool `json:"generated_resource"`
21
+ }
22
+
23
+ // SetCommandContext sets the telemetry values for the command being executed.
24
+ func (t *CLITelemetry) SetCommandContext(cmd *cobra.Command) {
25
+ t.CommandPath = cmd.CommandPath()
26
+ t.GeneratedResource = false
27
+
28
+ for _, value := range cmd.Annotations {
29
+ // Generated commands have an annotation called "operation", we can
30
+ // search for that to let us know it's generated
31
+ if value == "operation" {
32
+ t.GeneratedResource = true
33
+ }
34
+ }
35
+ }
36
+
37
+ // SetDeviceName puts the device name into telemetry
38
+ func (t *CLITelemetry) SetDeviceName(deviceName string) {
39
+ t.DeviceName = deviceName
40
+ }
41
+
42
+ //
43
+ // Public functions
44
+ //
45
+
46
+ // GetTelemetryInstance returns the CLITelemetry instance (initializing it
47
+ // first if necessary).
48
+ func GetTelemetryInstance() *CLITelemetry {
49
+ once.Do(func() {
50
+ instance = &CLITelemetry{}
51
+ })
52
+
53
+ return instance
54
+ }
55
+
56
+ //
57
+ // Private variables
58
+ //
59
+
60
+ var instance *CLITelemetry
61
+ var once sync.Once
62
+
63
+ //
64
+ // Private functions
65
+ //
66
+
67
+ func getTelemetryHeader() (string, error) {
68
+ telemetry := GetTelemetryInstance()
69
+ b, err := json.Marshal(telemetry)
70
+
71
+ if err != nil {
72
+ return "", err
73
+ }
74
+
75
+ return string(b), nil
76
+ }
77
+
78
+ // telemetryOptedOut returns true if the user has opted out of telemetry,
79
+ // false otherwise.
80
+ func telemetryOptedOut(optoutVar string) bool {
81
+ optoutVar = strings.ToLower(optoutVar)
82
+
83
+ return optoutVar == "1" || optoutVar == "true"
84
+ }
@@ -0,0 +1,35 @@
1
+ package hookdeck
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/spf13/cobra"
7
+ "github.com/stretchr/testify/require"
8
+ )
9
+
10
+ func TestGetTelemetryInstance(t *testing.T) {
11
+ t1 := GetTelemetryInstance()
12
+ t2 := GetTelemetryInstance()
13
+ require.Equal(t, t1, t2)
14
+ }
15
+
16
+ func TestSetCommandContext(t *testing.T) {
17
+ tel := GetTelemetryInstance()
18
+ cmd := &cobra.Command{
19
+ Use: "foo",
20
+ }
21
+ tel.SetCommandContext(cmd)
22
+ require.Equal(t, "foo", tel.CommandPath)
23
+ }
24
+
25
+ func TestTelemetryOptedOut(t *testing.T) {
26
+ require.False(t, telemetryOptedOut(""))
27
+ require.False(t, telemetryOptedOut("0"))
28
+ require.False(t, telemetryOptedOut("false"))
29
+ require.False(t, telemetryOptedOut("False"))
30
+ require.False(t, telemetryOptedOut("FALSE"))
31
+ require.True(t, telemetryOptedOut("1"))
32
+ require.True(t, telemetryOptedOut("true"))
33
+ require.True(t, telemetryOptedOut("True"))
34
+ require.True(t, telemetryOptedOut("TRUE"))
35
+ }
@@ -0,0 +1,82 @@
1
+ package hookdeck
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "net/http"
7
+ "regexp"
8
+ "strings"
9
+
10
+ "github.com/hookdeck/hookdeck-cli/pkg/ansi"
11
+ )
12
+
13
+ // inspectHeaders is the whitelist of headers that will be printed.
14
+ var inspectHeaders = []string{
15
+ "Authorization",
16
+ "Content-Type",
17
+ "Date",
18
+ "Idempotency-Key",
19
+ "Idempotency-Replayed",
20
+ "Request-Id",
21
+ "Hookdeck-Account",
22
+ "Hookdeck-Version",
23
+ }
24
+
25
+ type verboseTransport struct {
26
+ Transport *http.Transport
27
+ Verbose bool
28
+ Out io.Writer
29
+ }
30
+
31
+ func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
32
+ if t.Verbose {
33
+ t.dumpRequest(req)
34
+ }
35
+
36
+ resp, err = t.Transport.RoundTrip(req)
37
+
38
+ if err == nil && t.Verbose {
39
+ t.dumpResponse(resp)
40
+ }
41
+
42
+ return
43
+ }
44
+
45
+ func (t *verboseTransport) dumpRequest(req *http.Request) {
46
+ info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.URL.Host, req.URL.RequestURI())
47
+ t.verbosePrintln(info)
48
+ t.dumpHeaders(req.Header, ">")
49
+ }
50
+
51
+ func (t *verboseTransport) dumpResponse(resp *http.Response) {
52
+ info := fmt.Sprintf("< HTTP %d", resp.StatusCode)
53
+ t.verbosePrintln(info)
54
+ t.dumpHeaders(resp.Header, "<")
55
+ }
56
+
57
+ func (t *verboseTransport) dumpHeaders(header http.Header, indent string) {
58
+ for _, listed := range inspectHeaders {
59
+ for name, vv := range header {
60
+ if !strings.EqualFold(name, listed) {
61
+ continue
62
+ }
63
+
64
+ for _, v := range vv {
65
+ if v != "" {
66
+ r := regexp.MustCompile("(?i)^(basic|bearer) (.+)")
67
+ if r.MatchString(v) {
68
+ v = r.ReplaceAllString(v, "$1 [REDACTED]")
69
+ }
70
+
71
+ info := fmt.Sprintf("%s %s: %s", indent, name, v)
72
+ t.verbosePrintln(info)
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ func (t *verboseTransport) verbosePrintln(msg string) {
80
+ color := ansi.Color(t.Out)
81
+ fmt.Fprintln(t.Out, color.Cyan(msg))
82
+ }
@@ -0,0 +1,47 @@
1
+ package hookdeck
2
+
3
+ import (
4
+ "bytes"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "regexp"
8
+ "testing"
9
+
10
+ "github.com/stretchr/testify/require"
11
+ )
12
+
13
+ func TestVerboseTransport_Verbose(t *testing.T) {
14
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15
+ w.Header().Set("Request-Id", "req_123")
16
+ w.Header().Set("Non-Whitelisted-Header", "foo")
17
+ w.WriteHeader(http.StatusOK)
18
+ }))
19
+ defer ts.Close()
20
+
21
+ var b bytes.Buffer
22
+
23
+ httpTransport := &http.Transport{}
24
+ tr := &verboseTransport{
25
+ Transport: httpTransport,
26
+ Verbose: true,
27
+ Out: &b,
28
+ }
29
+ client := &http.Client{Transport: tr}
30
+ req, err := http.NewRequest("POST", ts.URL+"/test", nil)
31
+ require.NoError(t, err)
32
+ req.Header.Set("Authorization", "Bearer token")
33
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
34
+
35
+ resp, err := client.Do(req)
36
+ require.NoError(t, err)
37
+
38
+ defer resp.Body.Close()
39
+
40
+ out := b.String()
41
+ require.Regexp(t, regexp.MustCompile("> POST http://(.+)/test\n"), out)
42
+ require.Contains(t, out, "> Authorization: Bearer [REDACTED]\n")
43
+ require.Contains(t, out, "> Content-Type: application/x-www-form-urlencoded\n")
44
+ require.Contains(t, out, "< HTTP 200\n")
45
+ require.Contains(t, out, "< Request-Id: req_123\n")
46
+ require.NotContains(t, out, "Non-Whitelisted-Header")
47
+ }
@@ -0,0 +1,91 @@
1
+ package listen
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "strings"
7
+
8
+ "github.com/AlecAivazis/survey/v2"
9
+ "github.com/gosimple/slug"
10
+ "github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
11
+ )
12
+
13
+ func getConnections(client *hookdeck.Client, source hookdeck.Source, connection_query string) ([]hookdeck.Connection, error) {
14
+ // TODO: Filter connections using connection_query
15
+ var connections []hookdeck.Connection
16
+ connections, err := client.ListConnectionsBySource(source.Id)
17
+ if err != nil {
18
+ return connections, err
19
+ }
20
+
21
+ var filtered_connections []hookdeck.Connection
22
+ for _, connection := range connections {
23
+ if connection.Destination.CliPath != "" {
24
+ filtered_connections = append(filtered_connections, connection)
25
+ }
26
+ }
27
+ connections = filtered_connections
28
+
29
+ if connection_query != "" {
30
+ is_path, err := isPath(connection_query)
31
+ if err != nil {
32
+ return connections, err
33
+ }
34
+ var filtered_connections []hookdeck.Connection
35
+ for _, connection := range connections {
36
+ if (is_path && strings.Contains(connection.Destination.CliPath, connection_query)) || connection.Alias == connection_query {
37
+ filtered_connections = append(filtered_connections, connection)
38
+ }
39
+ }
40
+ connections = filtered_connections
41
+ }
42
+
43
+ if len(connections) == 0 {
44
+ answers := struct {
45
+ Label string `survey:"label"`
46
+ Path string `survey:"path"`
47
+ }{}
48
+ var qs = []*survey.Question{
49
+ {
50
+ Name: "path",
51
+ Prompt: &survey.Input{Message: "What path should the webhooks be forwarded to (ie: /webhooks)?"},
52
+ Validate: func(val interface{}) error {
53
+ str, ok := val.(string)
54
+ is_path, err := isPath(str)
55
+ if !ok || !is_path || err != nil {
56
+ return errors.New("invalid path")
57
+ }
58
+ return nil
59
+ },
60
+ },
61
+ {
62
+ Name: "label",
63
+ Prompt: &survey.Input{Message: "What's your connection label (ie: My API)?"},
64
+ Validate: survey.Required,
65
+ },
66
+ }
67
+
68
+ err := survey.Ask(qs, &answers)
69
+ if err != nil {
70
+ fmt.Println(err.Error())
71
+ return connections, err
72
+ }
73
+ alias := slug.Make(answers.Label)
74
+ connection, err := client.CreateConnection(hookdeck.CreateConnectionInput{
75
+ Alias: alias,
76
+ Label: answers.Label,
77
+ SourceId: source.Id,
78
+ Destination: hookdeck.CreateDestinationInput{
79
+ Alias: alias,
80
+ Label: answers.Label,
81
+ CliPath: answers.Path,
82
+ },
83
+ })
84
+ if err != nil {
85
+ return connections, err
86
+ }
87
+ connections = append(connections, connection)
88
+ }
89
+
90
+ return connections, nil
91
+ }
@@ -0,0 +1,140 @@
1
+ /*
2
+ Copyright © 2020 NAME HERE <EMAIL ADDRESS>
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+ package listen
17
+
18
+ import (
19
+ "context"
20
+ "fmt"
21
+ "net/url"
22
+ "regexp"
23
+
24
+ "github.com/hookdeck/hookdeck-cli/pkg/ansi"
25
+ "github.com/hookdeck/hookdeck-cli/pkg/config"
26
+ "github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
27
+ "github.com/hookdeck/hookdeck-cli/pkg/login"
28
+ "github.com/hookdeck/hookdeck-cli/pkg/proxy"
29
+ "github.com/hookdeck/hookdeck-cli/pkg/validators"
30
+ log "github.com/sirupsen/logrus"
31
+ )
32
+
33
+ type Flags struct {
34
+ NoWSS bool
35
+ WSBaseURL string
36
+ }
37
+
38
+ // listenCmd represents the listen command
39
+ func Listen(URL *url.URL, source_alias string, connection_query string, flags Flags, config *config.Config) error {
40
+ var key string
41
+ var err error
42
+ var guest_url string
43
+
44
+ key, err = config.Profile.GetAPIKey()
45
+ if err != nil {
46
+ errString := err.Error()
47
+ if errString == validators.ErrAPIKeyNotConfigured.Error() || errString == validators.ErrDeviceNameNotConfigured.Error() {
48
+ guest_url, _ = login.GuestLogin(config)
49
+ if guest_url == "" {
50
+ return err
51
+ }
52
+
53
+ key, err = config.Profile.GetAPIKey()
54
+ if err != nil {
55
+ return err
56
+ }
57
+ } else {
58
+ return err
59
+ }
60
+ }
61
+
62
+ parsedBaseURL, err := url.Parse(config.APIBaseURL)
63
+ if err != nil {
64
+ return err
65
+ }
66
+
67
+ client := &hookdeck.Client{
68
+ BaseURL: parsedBaseURL,
69
+ APIKey: key,
70
+ }
71
+
72
+ source, err := getSource(client, source_alias)
73
+ if err != nil {
74
+ return err
75
+ }
76
+
77
+ connections, err := getConnections(client, source, connection_query)
78
+ if err != nil {
79
+ return err
80
+ }
81
+
82
+ fmt.Println()
83
+ fmt.Println(ansi.Bold("Dashboard"))
84
+ if guest_url != "" {
85
+ fmt.Println("👤 Console URL: " + guest_url)
86
+ fmt.Println("Sign up in the Console to make your webhook URL permanent.")
87
+ fmt.Println()
88
+ } else {
89
+ var url = config.DashboardBaseURL
90
+ if config.Profile.GetTeamId() != "" {
91
+ url += "?team_id=" + config.Profile.GetTeamId()
92
+ }
93
+ if config.Profile.GetTeamMode() == "console" {
94
+ url = config.ConsoleBaseURL + "?source_id=" + source.Id
95
+ }
96
+ fmt.Println("👉 Inspect and replay webhooks: " + url)
97
+ fmt.Println()
98
+ }
99
+
100
+ fmt.Println(ansi.Bold(source.Label + " Source"))
101
+ fmt.Println("🔌 Webhook URL: " + source.Url)
102
+ fmt.Println()
103
+
104
+ fmt.Println(ansi.Bold("Connections"))
105
+ for _, connection := range connections {
106
+ fmt.Println(connection.Label + " forwarding to " + connection.Destination.CliPath)
107
+ }
108
+ fmt.Println()
109
+
110
+ deviceName, err := config.Profile.GetDeviceName()
111
+ if err != nil {
112
+ return err
113
+ }
114
+
115
+ p := proxy.New(&proxy.Config{
116
+ DeviceName: deviceName,
117
+ Key: key,
118
+ APIBaseURL: config.APIBaseURL,
119
+ DashboardBaseURL: config.DashboardBaseURL,
120
+ ConsoleBaseURL: config.ConsoleBaseURL,
121
+ Profile: config.Profile,
122
+ WSBaseURL: flags.WSBaseURL,
123
+ NoWSS: flags.NoWSS,
124
+ URL: URL,
125
+ Log: log.StandardLogger(),
126
+ Insecure: config.Insecure,
127
+ }, source, connections)
128
+
129
+ err = p.Run(context.Background())
130
+ if err != nil {
131
+ return err
132
+ }
133
+
134
+ return nil
135
+ }
136
+
137
+ func isPath(value string) (bool, error) {
138
+ is_path, err := regexp.MatchString("^(/)+([/.a-zA-Z0-9-_]*)$", value)
139
+ return is_path, err
140
+ }
@@ -0,0 +1,84 @@
1
+ package listen
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/AlecAivazis/survey/v2"
7
+ "github.com/gosimple/slug"
8
+ "github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
9
+ )
10
+
11
+ func getSource(client *hookdeck.Client, source_alias string) (hookdeck.Source, error) {
12
+ var source hookdeck.Source
13
+ if source_alias != "" {
14
+ source, _ = client.GetSourceByAlias(source_alias)
15
+ if source.Id == "" {
16
+ // TODO: Prompt here?
17
+ source, _ = client.CreateSource(hookdeck.CreateSourceInput{
18
+ Alias: source_alias,
19
+ // TODO: labelized alias
20
+ Label: source_alias,
21
+ })
22
+ }
23
+ } else {
24
+ sources, _ := client.ListSources()
25
+ if len(sources) > 0 {
26
+ var sources_alias []string
27
+ for _, temp_source := range sources {
28
+ sources_alias = append(sources_alias, temp_source.Alias)
29
+ }
30
+
31
+ answers := struct {
32
+ SourceAlias string `survey:"source"`
33
+ }{}
34
+
35
+ var qs = []*survey.Question{
36
+ {
37
+ Name: "source",
38
+ Prompt: &survey.Select{
39
+ Message: "Select a source",
40
+ Options: append(sources_alias, "Create new source"),
41
+ },
42
+ },
43
+ }
44
+
45
+ err := survey.Ask(qs, &answers)
46
+ if err != nil {
47
+ fmt.Println(err.Error())
48
+ return source, err
49
+ }
50
+
51
+ if answers.SourceAlias != "Create new source" {
52
+ for _, temp_source := range sources {
53
+ if temp_source.Alias == answers.SourceAlias {
54
+ source = temp_source
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ if source.Id == "" {
61
+ answers := struct {
62
+ Label string `survey:"label"` // or you can tag fields to match a specific name
63
+ }{}
64
+ var qs = []*survey.Question{
65
+ {
66
+ Name: "label",
67
+ Prompt: &survey.Input{Message: "What should be your new source label?"},
68
+ Validate: survey.Required,
69
+ },
70
+ }
71
+
72
+ err := survey.Ask(qs, &answers)
73
+ if err != nil {
74
+ return source, err
75
+ }
76
+
77
+ source, _ = client.CreateSource(hookdeck.CreateSourceInput{
78
+ Alias: slug.Make(answers.Label),
79
+ Label: answers.Label,
80
+ })
81
+ }
82
+ }
83
+ return source, nil
84
+ }