├── firebase.json
├── functions
├── .gitignore
├── package.json
├── foundry.yaml
└── index.js
├── .firebaserc
├── cmd
├── rootFlags-release.go
├── rootFlags-debug.go
├── signout.go
├── envprint.go
├── signin.go
├── envset.go
├── signup.go
├── envdel.go
├── init.go
├── root.go
└── go.go
├── .gitignore
├── main.go
├── connection
├── endpoint
│ ├── release.go
│ └── local.go
├── msg
│ ├── watchfn.go
│ ├── response.go
│ ├── ping.go
│ ├── chunk.go
│ └── env.go
└── connection.go
├── logger
├── release.go
├── shared.go
└── debug.go
├── prompt
├── cmd
│ ├── cmd.go
│ ├── exit.go
│ ├── watch.go
│ ├── envprint.go
│ ├── envset.go
│ └── envdel.go
├── buffer.go
├── prompt.txt
└── prompt.go
├── go.mod
├── .vscode
└── launch.json
├── config
└── config.go
├── firebase
└── functions.go
├── auth
├── tokens.go
└── auth.go
├── install
├── files
└── files.go
├── README.md
├── rwatch
└── rwatch.go
├── zip
└── zip.go
├── LICENSE
└── go.sum
/firebase.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "foundryapp"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/cmd/rootFlags-release.go:
--------------------------------------------------------------------------------
1 | // +build !debug
2 |
3 | package cmd
4 |
5 | import "github.com/spf13/cobra"
6 |
7 | func AddRootFlags(cmd *cobra.Command) {}
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/**/vendor
2 | **/keyfile.json
3 | **/terraform_*
4 | tmp/
5 | deploy/deploy
6 | deploy/
7 | cli
8 | build/
9 |
10 | debug.txt
11 | race.txt*
12 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "foundry/cli/cmd"
5 | "foundry/cli/config"
6 | "foundry/cli/logger"
7 | )
8 |
9 | func main() {
10 | if err := config.Init(); err != nil {
11 | logger.FatalLogln("Couldn't init config", err)
12 | }
13 |
14 | cmd.Execute()
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/rootFlags-debug.go:
--------------------------------------------------------------------------------
1 | // +build debug
2 |
3 | package cmd
4 |
5 | import "github.com/spf13/cobra"
6 |
7 | func AddRootFlags(rootCmd *cobra.Command) {
8 | rootCmd.PersistentFlags().StringVarP(&debugFile, "debug-file", "d", "", "path to file where the debug logs are written --d='path/to/file.txt'")
9 | }
10 |
--------------------------------------------------------------------------------
/connection/endpoint/release.go:
--------------------------------------------------------------------------------
1 | // +build !local
2 |
3 | package endpoint
4 |
5 | const (
6 | WebSocketURL = "ide.foundryapp.co"
7 | WebSocketScheme = "wss"
8 |
9 | PingURL = "ide.foundryapp.co"
10 | PingScheme = "https"
11 |
12 | SetEnvURL = "ide.foundryapp.co"
13 | SetEnvScheme = "https"
14 | )
15 |
--------------------------------------------------------------------------------
/connection/endpoint/local.go:
--------------------------------------------------------------------------------
1 | // +build local
2 |
3 | package endpoint
4 |
5 | const (
6 | // WebSocketURL = "127.0.0.1:8000" // autorun
7 | WebSocketURL = "127.0.0.1:3500" // podm
8 | WebSocketScheme = "ws"
9 |
10 | // PingURL = "ide.foundryapp.co"
11 | PingURL = "127.0.0.1:3500"
12 | PingScheme = "http"
13 |
14 | SetEnvURL = "127.0.0.1:3500" // podm
15 | SetEnvScheme = "http"
16 | )
17 |
--------------------------------------------------------------------------------
/logger/release.go:
--------------------------------------------------------------------------------
1 | // +build !debug
2 |
3 | package logger
4 |
5 | func InitDebug(path string) error { return nil }
6 | func Close() {}
7 | func Fdebugln(v ...interface{}) {}
8 | func FdebuglnError(v ...interface{}) {}
9 | func FdebuglnFatal(v ...interface{}) {}
10 | func Debugln(v ...interface{}) {}
11 | func DebuglnError(v ...interface{}) {}
12 | func DebuglnFatal(v ...interface{}) {}
13 |
--------------------------------------------------------------------------------
/prompt/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | c "foundry/cli/connection"
6 |
7 | goprompt "github.com/mlejva/go-prompt"
8 | )
9 |
10 | type Args []string
11 | type RunChannelType chan Args
12 |
13 | type Cmd interface {
14 | Run(conn *c.Connection, args Args) (promptOutput string, promptInfo string, err error)
15 | RunRequest(args Args)
16 | ToSuggest() goprompt.Suggest
17 | Name() string
18 | fmt.Stringer
19 | }
20 |
--------------------------------------------------------------------------------
/connection/msg/watchfn.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | type WatchfnContent struct {
4 | RunAll bool `json:"runAll"`
5 | Run []string `json:"run"`
6 | }
7 |
8 | type WatchfnMsg struct {
9 | Type string `json:"type"`
10 | Content WatchfnContent `json:"content"`
11 | }
12 |
13 | func NewWatchfnMsg(all bool, fns []string) *WatchfnMsg {
14 | c := WatchfnContent{all, fns}
15 | return &WatchfnMsg{"watch", c}
16 | }
17 |
18 | func (wm *WatchfnMsg) Body() interface{} {
19 | return wm
20 | }
21 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "serve": "firebase emulators:start --only functions",
6 | "shell": "firebase functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "firebase deploy --only functions",
9 | "logs": "firebase functions:log"
10 | },
11 | "engines": {
12 | "node": "10"
13 | },
14 | "dependencies": {
15 | "firebase-admin": "^8.10.0",
16 | "firebase-functions": "^3.3.0"
17 | },
18 | "devDependencies": {
19 | "firebase-functions-test": "^0.1.6"
20 | },
21 | "private": true
22 | }
23 |
--------------------------------------------------------------------------------
/connection/msg/response.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 |
4 | const (
5 | LogResponseMsg string = "log"
6 | WatchResponseMsg = "watch"
7 | ErrorResponseMsg = "error"
8 | )
9 |
10 | type ResponseError struct {
11 | Msg string `json:"message"`
12 | }
13 |
14 | type ErrorContent struct {
15 | OriginalMsg interface{} `json:"originalMessage"`
16 | Error ResponseError `json:"error"`
17 | }
18 |
19 | type WatchContent struct {
20 | RunAll bool `json:"runAll"`
21 | Run []string `json:"run"`
22 | }
23 |
24 | type LogContent struct {
25 | Msg string `json:"msg"`
26 | }
27 |
28 | type ResponseMsgType struct {
29 | Type string `json:"type"`
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/signout.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "foundry/cli/logger"
5 |
6 | "github.com/fatih/color"
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var (
11 | signOutCmd = &cobra.Command{
12 | Use: "sign-out",
13 | Short: "Sign out from your Foundry account",
14 | Example: "foundry sign-out",
15 | Run: runSignOut,
16 | }
17 | )
18 |
19 | func init() {
20 | rootCmd.AddCommand(signOutCmd)
21 | }
22 |
23 | func runSignOut(cmd *cobra.Command, args []string) {
24 | if err := authClient.SignOut(); err != nil {
25 | logger.FdebuglnFatal("Sign out error", err)
26 | logger.FatalLogln("Sign out error", err)
27 | }
28 | color.Green("✔ Signed Out")
29 | }
30 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module foundry/cli
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/AlecAivazis/survey/v2 v2.0.7
7 | github.com/fatih/color v1.9.0
8 | github.com/fsnotify/fsnotify v1.4.9
9 | github.com/gobwas/glob v0.2.3
10 | github.com/golang/gddo v0.0.0-20200324184333-3c2cc9a6329d
11 | github.com/gorilla/websocket v1.4.2
12 | github.com/mattn/go-runewidth v0.0.8 // indirect
13 | github.com/mattn/go-tty v0.0.3 // indirect
14 | github.com/mlejva/go-prompt v0.2.4-0.20200408092807-6312c0dbbff2
15 | github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect
16 | github.com/spf13/cobra v0.0.7
17 | github.com/spf13/viper v1.6.2
18 | gopkg.in/yaml.v2 v2.2.8
19 | )
20 |
21 | // replace github.com/mlejva/go-prompt v0.2.4-0.20200325150717-647876a5db0d => /Users/vasekmlejnsky/Developer/foundry/go-prompt
22 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | // {
5 | // "name": "Launch",
6 | // "type": "go",
7 | // "request": "launch",
8 | // "mode": "auto",
9 | // "program": "${fileDirname}",
10 | // "env": {},
11 | // "args": [],
12 | // "buildFlags": "-tags=build local"
13 | // }
14 | // {
15 | // "name": "Launch executable",
16 | // "type": "go",
17 | // "request": "launch",
18 | // "mode": "exec",
19 | // "program": "/Users/vasekmlejnsky/Developer/foundry/testing-cf-project/foundry-debug-local"
20 | // }
21 | {
22 | "name": "Attach to local process",
23 | "type": "go",
24 | "request": "attach",
25 | "mode": "local",
26 | "processId": 88730
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/prompt/cmd/exit.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | c "foundry/cli/connection"
6 | "os"
7 |
8 | goprompt "github.com/mlejva/go-prompt"
9 | )
10 |
11 | type ExitCmd struct {
12 | Text string
13 | Desc string
14 | RunCh RunChannelType
15 | }
16 |
17 | func NewExitCmd() *ExitCmd {
18 | return &ExitCmd{
19 | Text: "exit",
20 | Desc: "Stop Foundry CLI",
21 | RunCh: make(chan Args),
22 | }
23 | }
24 |
25 | // Implement Cmd interface
26 |
27 | func (c *ExitCmd) Run(conn *c.Connection, args Args) (promptOutput string, promptInfo string, err error) {
28 | os.Exit(0)
29 | return "", "", err
30 | }
31 |
32 | func (c *ExitCmd) RunRequest(args Args) {
33 | c.RunCh <- args
34 | }
35 |
36 | func (c *ExitCmd) ToSuggest() goprompt.Suggest {
37 | return goprompt.Suggest{c.Text, c.Desc}
38 | }
39 |
40 | func (c *ExitCmd) Name() string {
41 | return c.Text
42 | }
43 |
44 | func (c *ExitCmd) String() string {
45 | return fmt.Sprintf("%s - %s", c.Text, c.Desc)
46 | }
47 |
--------------------------------------------------------------------------------
/connection/msg/ping.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io/ioutil"
7 | "net/http"
8 |
9 | "foundry/cli/logger"
10 | )
11 |
12 | type PingBody struct {
13 | Token string `json:"token"`
14 | }
15 |
16 | type PingMsg struct {
17 | URL string
18 | Body PingBody
19 | }
20 |
21 | func NewPingMsg(url, t string) *PingMsg {
22 | return &PingMsg{
23 | URL: url,
24 | Body: PingBody{t},
25 | }
26 | }
27 |
28 | func (pm *PingMsg) Send() error {
29 | j, err := json.Marshal(pm.Body)
30 | if err != nil {
31 | return err
32 | }
33 |
34 | res, err := http.Post(pm.URL, "application/json", bytes.NewBuffer(j))
35 | if err != nil {
36 | return err
37 | }
38 |
39 | if res.StatusCode != http.StatusOK {
40 | bodyBytes, err := ioutil.ReadAll(res.Body)
41 | if err != nil {
42 | logger.FdebuglnError("[ping] Error reading ping response body: ", err)
43 | return err
44 | }
45 |
46 | bodyString := string(bodyBytes)
47 | logger.FdebuglnError("[ping] non-ok response:", bodyString)
48 | }
49 |
50 | return nil
51 | }
52 |
--------------------------------------------------------------------------------
/prompt/buffer.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "bytes"
5 | "foundry/cli/logger"
6 | "io"
7 | "sync"
8 | "time"
9 | )
10 |
11 | // Buffer is a thread safe wrapper for buffer
12 | type Buffer struct {
13 | buf bytes.Buffer
14 | mut sync.Mutex
15 | }
16 |
17 | // NewBuffer returns a pointer to new Buffer
18 | func NewBuffer() *Buffer {
19 | return &Buffer{buf: bytes.Buffer{}}
20 | }
21 |
22 | func (b *Buffer) Write(p []byte) (n int, err error) {
23 | b.mut.Lock()
24 | defer b.mut.Unlock()
25 | return b.buf.Write(p)
26 | }
27 |
28 | func (b *Buffer) Read(bufCh chan<- []byte, stopCh <-chan struct{}) {
29 | for {
30 | select {
31 | case <-stopCh:
32 | return
33 | default:
34 | b.mut.Lock()
35 |
36 | buf := make([]byte, 1024)
37 | n, err := b.buf.Read(buf)
38 |
39 | if err == nil {
40 | bufCh <- buf[:n]
41 | } else if err != io.EOF {
42 | logger.FdebuglnFatal("Error reading from prompt buffer", err)
43 | logger.FatalLogln("Error reading from prompt buffer", err)
44 | }
45 |
46 | b.mut.Unlock()
47 | }
48 | time.Sleep(time.Millisecond * 10)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/functions/foundry.yaml:
--------------------------------------------------------------------------------
1 | # An array of glob patterns for files that should be ignored. The path is relative to the root dir.
2 | # If the array is changed, the CLI must be restarted for it to take the effect
3 | ignore:
4 | - node_modules # Skip the whole node_modules directory
5 | - .git # Skip the whole .git directory
6 | - "**/firebase-debug.log"
7 | - "**/*.*[0-9]" # Skip all tmp files ending with number
8 | - "**/.*" # Skip all hidden files
9 | - "**/*~" # Skip vim's temp files
10 | # An array of Firebase functions that should be evaluated by Foundry. All these functions must be exported in your root index.js
11 |
12 | serviceAcc: "/Users/vasekmlejnsky/Downloads/foundryapp-firebase-adminsdk-9hj8q-20401ed01c.json"
13 |
14 | users:
15 | - getFromProd: ["DbJD37dhx4VqNF3dtN7zrK6CCB13"]
16 |
17 | firestore:
18 | - collection: envs
19 | docs:
20 | - getFromProd: 5
21 |
22 | functions:
23 | - name: getUserEnvs
24 | type: httpsCallable
25 | asUser:
26 | id: DbJD37dhx4VqNF3dtN7zrK6CCB13
27 |
28 | - name: deleteUserEnvs
29 | type: httpsCallable
30 | asUser:
31 | id: DbJD37dhx4VqNF3dtN7zrK6CCB13
32 | payload: '{"delete": ["env1", "env2"]}'
33 |
--------------------------------------------------------------------------------
/logger/shared.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | )
7 |
8 | const (
9 | bold = "\x1b[1m"
10 | red = "\x1b[31m"
11 | green = "\x1b[32m"
12 | yellow = "\x1b[33m"
13 | endSeq = "\x1b[0m"
14 | )
15 |
16 | var (
17 | successPrefix = fmt.Sprintf("%s%sSUCCESS%s", bold, green, endSeq)
18 | warningPrefix = fmt.Sprintf("%s%sWARNING%s", bold, yellow, endSeq)
19 | errorPrefix = fmt.Sprintf("%s%sERROR%s", bold, red, endSeq)
20 | )
21 |
22 | func ErrorLogln(args ...interface{}) {
23 | t := fmt.Sprintf("%s %s", errorPrefix, fmt.Sprint(args...))
24 | fmt.Println(t)
25 | }
26 |
27 | func FatalLogln(args ...interface{}) {
28 | t := fmt.Sprintf("%s %s", errorPrefix, fmt.Sprint(args...))
29 | fmt.Println(t)
30 | os.Exit(1)
31 | }
32 |
33 | func WarningLogln(args ...interface{}) {
34 | t := fmt.Sprintf("%s %s", warningPrefix, fmt.Sprint(args...))
35 | fmt.Println(t)
36 | }
37 |
38 | func SuccessLogln(args ...interface{}) {
39 | t := fmt.Sprintf("%s %s", successPrefix, fmt.Sprint(args...))
40 | fmt.Println(t)
41 | }
42 |
43 | func Log(args ...interface{}) {
44 | fmt.Print(args...)
45 | }
46 |
47 | func Logln(args ...interface{}) {
48 | fmt.Println(args...)
49 | }
50 |
--------------------------------------------------------------------------------
/connection/msg/chunk.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "encoding/hex"
5 | )
6 |
7 | type ChunkContent struct {
8 | Data string `json:"data"`
9 | PreviousChecksum string `json:"previousChecksum"`
10 | Checksum string `json:"checksum"`
11 | IsLast bool `json:"isLast"`
12 | }
13 |
14 | type ChunkMsg struct {
15 | Type string `json:"type"`
16 | Content ChunkContent `json:"content"`
17 | }
18 |
19 | func NewChunkMsg(b []byte, checksum, checksumPrev string, last bool) *ChunkMsg {
20 | c := ChunkContent{hex.EncodeToString(b), checksumPrev, checksum, last}
21 | return &ChunkMsg{"chunk", c}
22 | }
23 |
24 | func (cm *ChunkMsg) Body() interface{} {
25 | return cm
26 | }
27 |
28 | // msg := struct {
29 | // Data string `json:"data"`
30 | // PreviousChecksum string `json:"previousChecksum"`
31 | // Checksum string `json:"checksum"`
32 | // IsLast bool `json:"isLast"`
33 | // RunAll bool `json:"runAll"`
34 | // Run []string `json:"run"`
35 | // }{hex.EncodeToString(b),
36 | // prevChecksum,
37 | // checksum,
38 | // last,
39 | // true,
40 | // []string{},
41 | // }
42 | // err := c.WriteJSON(msg)
43 | // if err != nil {
44 | // return err
45 | // }
--------------------------------------------------------------------------------
/connection/msg/env.go:
--------------------------------------------------------------------------------
1 | package msg
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 |
10 | "foundry/cli/connection/endpoint"
11 | "foundry/cli/logger"
12 | )
13 |
14 | type Env struct {
15 | Name string `json:"name"`
16 | Value string `json:"value"`
17 | }
18 |
19 | type EnvBody struct {
20 | Token string `json:"token"`
21 | Envs []Env `json:"envs"`
22 | }
23 |
24 | type EnvMsg struct {
25 | url string
26 | Body EnvBody
27 | }
28 |
29 | func SetEnvURL() string {
30 | return fmt.Sprintf("%s://%s/setenv", endpoint.SetEnvScheme, endpoint.SetEnvURL)
31 | }
32 |
33 | func NewEnvMsg(token string, envs []Env) *EnvMsg {
34 | return &EnvMsg{
35 | url: SetEnvURL(),
36 | Body: EnvBody{token, envs},
37 | }
38 | }
39 |
40 | func (em *EnvMsg) Send() error {
41 | j, err := json.Marshal(em.Body)
42 | if err != nil {
43 | return err
44 | }
45 |
46 | res, err := http.Post(em.url, "application/json", bytes.NewBuffer(j))
47 | if err != nil {
48 | return err
49 | }
50 |
51 | if res.StatusCode != http.StatusOK {
52 | bodyBytes, err := ioutil.ReadAll(res.Body)
53 | if err != nil {
54 | logger.FdebuglnError("Error reading env msg response body: ", err)
55 | return err
56 | }
57 |
58 | bodyString := string(bodyBytes)
59 | logger.FdebuglnError("Non-ok env msg response:", bodyString)
60 | }
61 | return nil
62 | }
63 |
--------------------------------------------------------------------------------
/cmd/envprint.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "foundry/cli/firebase"
7 | "foundry/cli/logger"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | var (
13 | envPrintCmd = &cobra.Command{
14 | Use: "env-print",
15 | Short: "Print all environment variables in your cloud environment",
16 | Example: "foundry env-print",
17 | Run: runEnvPrint,
18 | }
19 | )
20 |
21 | func init() {
22 | rootCmd.AddCommand(envPrintCmd)
23 | }
24 |
25 | func runEnvPrint(cmd *cobra.Command, args []string) {
26 | res, err := firebase.Call("getUserEnvs", authClient.IDToken, nil)
27 | if err != nil {
28 | logger.FdebuglnFatal("Error calling getUserEnvs:", err)
29 | logger.FatalLogln("Error printing environment variables (1):", err)
30 | }
31 | if res.Error != nil {
32 | logger.FdebuglnFatal("Error calling getUserEnvs:", res.Error)
33 | logger.FatalLogln("Error printing environment variables (2):", res.Error)
34 | }
35 |
36 | envs, ok := res.Result.(map[string]interface{})
37 | if !ok {
38 | logger.FdebuglnFatal("Failed to type assert res.Result")
39 | logger.FatalLogln("Error printing environment variables. Failed to convert the response.")
40 | }
41 |
42 | if len(envs) == 0 {
43 | logger.SuccessLogln("No environment variable has been set yet")
44 | } else {
45 | logger.SuccessLogln("Following environment variables are set:")
46 | logger.Logln("")
47 | for k, v := range envs {
48 | s := fmt.Sprintf("%s=%s\n", k, v.(string))
49 | logger.Log(s)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/prompt/cmd/watch.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | c "foundry/cli/connection"
6 | connMsg "foundry/cli/connection/msg"
7 |
8 | goprompt "github.com/mlejva/go-prompt"
9 | )
10 |
11 | type WatchCmd struct {
12 | Text string
13 | Desc string
14 | RunCh RunChannelType
15 | }
16 |
17 | func NewWatchCmd() *WatchCmd {
18 | return &WatchCmd{
19 | Text: "watch",
20 | Desc: "Watch only specific function(s)",
21 | RunCh: make(chan Args),
22 | }
23 | }
24 |
25 | func NewWatchAllCmd() *WatchCmd {
26 | return &WatchCmd{
27 | Text: "watch:all",
28 | Desc: "Disable all active watch filters and watch all functions",
29 | RunCh: make(chan Args),
30 | }
31 | }
32 |
33 | // Implement Cmd interface
34 | func (c *WatchCmd) Run(conn *c.Connection, args Args) (promptOutput string, promptInfo string, err error) {
35 | watchAll := false
36 | fns := args
37 | if c.Text == "watch:all" {
38 | watchAll = true
39 | fns = []string{}
40 | } else {
41 | if len(args) == 0 {
42 | return "", "No argument specified. Example usage: 'watch myFunction'", nil
43 | }
44 | }
45 |
46 | msg := connMsg.NewWatchfnMsg(watchAll, fns)
47 | err = conn.Send(msg)
48 | return "", "", err
49 | }
50 |
51 | func (c *WatchCmd) RunRequest(args Args) {
52 | c.RunCh <- args
53 | }
54 |
55 | func (c *WatchCmd) ToSuggest() goprompt.Suggest {
56 | return goprompt.Suggest{c.Text, c.Desc}
57 | }
58 |
59 | func (c *WatchCmd) Name() string {
60 | return c.Text
61 | }
62 |
63 | func (c *WatchCmd) String() string {
64 | return fmt.Sprintf("%s - %s", c.Text, c.Desc)
65 | }
66 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 |
6 | "foundry/cli/logger"
7 |
8 | "github.com/spf13/viper"
9 | )
10 |
11 | func Init() error {
12 | // /Users/vasekmlejnsky/Library/Application Support/foundrycli
13 | configDir, err := os.UserConfigDir()
14 | if err != nil {
15 | return err
16 | }
17 |
18 | dirPath := configDir + "/foundrycli"
19 | confName := "config"
20 | ext := "json"
21 | fullPath := dirPath + "/" + confName + "." + ext
22 |
23 | viper.SetConfigName(confName)
24 | viper.SetConfigType(ext)
25 | viper.AddConfigPath(dirPath)
26 |
27 | if _, err := os.Stat(fullPath); os.IsNotExist(err) {
28 | os.MkdirAll(dirPath, os.ModePerm)
29 |
30 | f, err := os.Create(fullPath)
31 | if err != nil {
32 | return err
33 | }
34 | defer f.Close()
35 | f.WriteString("{}")
36 | } else if err != nil && !os.IsNotExist(err) {
37 | return err
38 | }
39 |
40 | if err = viper.ReadInConfig(); err != nil {
41 | return err
42 | }
43 |
44 | return nil
45 | }
46 |
47 | func Set(key string, val interface{}) {
48 | logger.Fdebugln("Set to config (key, val)", key, val)
49 | viper.Set(key, val)
50 | }
51 |
52 | func GetString(key string) string {
53 | val := viper.GetString(key)
54 | logger.Fdebugln("Get string from config (key, val):", key, val)
55 | return val
56 | }
57 |
58 | func GetInt(key string) int {
59 | val := viper.GetInt(key)
60 | logger.Fdebugln("Get int from config (key, val):", key, val)
61 | return val
62 | }
63 |
64 | func Write() error {
65 | logger.Fdebugln("Write config")
66 | return viper.WriteConfig()
67 | }
68 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const admin = require('firebase-admin');
3 |
4 | admin.initializeApp();
5 | let FieldValue = admin.firestore.FieldValue;
6 |
7 | ////// getUserEnvs
8 | exports.getUserEnvs = functions.https.onCall(async (data, context) => {
9 | const envsDoc = await admin.firestore()
10 | .collection('envs')
11 | .doc(context.auth.uid)
12 | .get();
13 | return envsDoc.data().envs;
14 | });
15 |
16 |
17 | ////// deleteUserEnvs
18 | exports.deleteUserEnvs = functions.https.onCall(async (data, context) => {
19 | const toDeleteArr = data.delete;
20 | if (!toDeleteArr) {
21 | throw new functions.https.HttpsError('invalid-argument', `Expected "delete" array in the body. Got: ${toDeleteArr}`);
22 | }
23 |
24 | const envsDocRef = admin.firestore()
25 | .collection('envs')
26 | .doc(context.auth.uid);
27 |
28 | const currentEnvs = (await envsDocRef.get()).data().envs;
29 |
30 | console.log(`Current envs: "${Object.keys(currentEnvs)}", for user "${context.auth.uid}"`);
31 | console.log(`Will delete envs "${toDeleteArr}"`);
32 |
33 | const newEnvs = {}
34 | Object.keys(currentEnvs).forEach(envName => {
35 | if (!toDeleteArr.includes(envName)) {
36 | newEnvs[envName] = currentEnvs[envName]
37 | }
38 | });
39 |
40 | try {
41 | await envsDocRef.update({ envs: newEnvs });
42 | console.log("New envs:", Object.keys(newEnvs));
43 | return newEnvs;
44 | } catch (error) {
45 | throw new functions.https.HttpsError('internal', `Error updating user envs: ${error}`);
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/cmd/signin.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "foundry/cli/logger"
8 |
9 | "github.com/AlecAivazis/survey/v2"
10 | "github.com/AlecAivazis/survey/v2/terminal"
11 | "github.com/fatih/color"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | var (
16 | signInCmd = &cobra.Command{
17 | Use: "sign-in",
18 | Short: "Sign in to your Foundry account",
19 | Example: "foundry sign-in",
20 | Run: runSignIn,
21 | }
22 |
23 | qs = []*survey.Question{
24 | {
25 | Name: "email",
26 | Prompt: &survey.Input{Message: "Foundry email:"},
27 | Validate: survey.Required,
28 | },
29 | {
30 | Name: "pass",
31 | Prompt: &survey.Password{Message: "Foundry password:"},
32 | Validate: survey.Required,
33 | },
34 | }
35 | )
36 |
37 | func init() {
38 | rootCmd.AddCommand(signInCmd)
39 | }
40 |
41 | func runSignIn(cmd *cobra.Command, args []string) {
42 | creds := struct {
43 | Email string `survey:"email`
44 | Pass string `survey:"pass`
45 | }{}
46 |
47 | logger.Logln("Sign in to your Foundry account\n")
48 |
49 | err := survey.Ask(qs, &creds)
50 | // Without this specific "if" SIGINT (Ctrl+C) would only
51 | // interrupt the survey's prompt and not the whole program
52 | if err == terminal.InterruptErr {
53 | os.Exit(0)
54 | } else if err != nil {
55 | log.Println(err)
56 | }
57 |
58 | if err = authClient.SignIn(creds.Email, creds.Pass); err != nil {
59 | logger.FdebuglnFatal("Sign in error", err)
60 | logger.FatalLogln("Sign in error (1)", err)
61 | }
62 |
63 | if authClient.Error != nil {
64 | logger.FdebuglnFatal("Sign in error", err)
65 | logger.FatalLogln("Sign in error (2)", authClient.Error)
66 | }
67 |
68 | color.Green("✔ Signed in")
69 | }
70 |
--------------------------------------------------------------------------------
/firebase/functions.go:
--------------------------------------------------------------------------------
1 | package firebase
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | type Error struct {
13 | Message string `json:"message"`
14 | Status string `json:"status"`
15 | }
16 |
17 | type Request struct {
18 | Data interface{} `json:"data"`
19 | }
20 |
21 | type Response struct {
22 | Error *Error `json:"error"`
23 | Result interface{} `json:"result"`
24 | }
25 |
26 | func Call(funcName, IDToken string, data interface{}) (*Response, error) {
27 | url := fmt.Sprintf("https://us-central1-foundryapp.cloudfunctions.net/%s", funcName)
28 |
29 | var reqBody Request
30 | if data == nil {
31 | // Firebase httpsCallable functions requires that there's always at least
32 | // empty 'data' field (e.i.: '"data": {}') in the body
33 | reqBody = Request{struct{}{}}
34 | } else {
35 | reqBody = struct {
36 | Data interface{} `json:"data"`
37 | }{data}
38 | }
39 |
40 | marshaledBody, err := json.Marshal(reqBody)
41 | if err != nil {
42 | return nil, err
43 | }
44 | req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(marshaledBody))
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | bearer := "Bearer " + IDToken
50 | req.Header.Add("Authorization", bearer)
51 | req.Header.Add("Content-Type", "application/json")
52 |
53 | client := &http.Client{Timeout: time.Second * 30}
54 | res, err := client.Do(req)
55 | if err != nil {
56 | return nil, err
57 | }
58 | defer res.Body.Close()
59 |
60 | bodyBytes, err := ioutil.ReadAll(res.Body)
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | var respBody Response
66 | if err = json.Unmarshal(bodyBytes, &respBody); err != nil {
67 | return nil, err
68 | }
69 |
70 | return &respBody, nil
71 | }
72 |
--------------------------------------------------------------------------------
/auth/tokens.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "foundry/cli/config"
8 | "foundry/cli/logger"
9 | )
10 |
11 | // Exchanges a refresh token for an ID token
12 | func (a *Auth) RefreshIDToken() error {
13 | now := time.Now()
14 | origin := a.originDate
15 |
16 | if a.ExpiresIn == "" { a.ExpiresIn = "0" }
17 |
18 | expireSeconds, err := strconv.Atoi(a.ExpiresIn)
19 | if err != nil {
20 | return err
21 | }
22 |
23 | end := origin.Add(time.Duration(expireSeconds))
24 |
25 | // log.Println("now", now)
26 | // log.Println("origin", origin)
27 | // log.Println("expireSeconds", expireSeconds)
28 | // log.Println("end", end)
29 |
30 | if now.After(end) {
31 | if err := a.doRefreshReq(); err != nil {
32 | return err
33 | }
34 | if err := a.saveTokensAndState(); err != nil {
35 | return err
36 | }
37 | }
38 | return nil
39 | }
40 |
41 | func (a *Auth) saveTokensAndState() error {
42 | config.Set(idTokenKey, a.IDToken)
43 | config.Set(refreshTokenKey, a.RefreshToken)
44 | config.Set(authStateKey, a.AuthState)
45 | return config.Write()
46 | }
47 |
48 | func (a *Auth) loadTokensAndState() error {
49 | idtok := config.GetString(idTokenKey)
50 | a.IDToken = idtok
51 |
52 | rtok := config.GetString(refreshTokenKey)
53 | a.RefreshToken = rtok
54 |
55 | state := config.GetInt(authStateKey)
56 | // State is 0 when the config file is empty
57 | if state != 0 {
58 | a.AuthState = AuthStateType(state)
59 | } else {
60 | a.AuthState = AuthStateTypeSignedOut
61 | }
62 |
63 | logger.Fdebugln("Loaded AuthState from config (1 = signed out, 2 = signed in, 3 = anonymous):", a.AuthState)
64 |
65 | return nil
66 | }
67 |
68 | func (a *Auth) clearTokensAndState() error {
69 | config.Set(idTokenKey, "")
70 | config.Set(refreshTokenKey, "")
71 | config.Set(authStateKey, AuthStateTypeSignedOut)
72 | return config.Write()
73 | }
74 |
--------------------------------------------------------------------------------
/cmd/envset.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 | "strings"
6 |
7 | "foundry/cli/connection/msg"
8 | "foundry/cli/logger"
9 |
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var (
14 | envSetCmd = &cobra.Command{
15 | Use: "env-set",
16 | Short: "Set environment variable(s) in your cloud environment",
17 | Example: "foundry env-set ENV_1=VALUE_1 ENV_2=VALUE_2",
18 | Run: runEnvSet,
19 | }
20 | )
21 |
22 | func init() {
23 | rootCmd.AddCommand(envSetCmd)
24 | }
25 |
26 | func runEnvSet(cmd *cobra.Command, args []string) {
27 | if len(args) == 0 {
28 | logger.WarningLogln("No envs specified. Example usage: 'foundry env-set MY_ENV=ENV_VALUE ANOTHER_ENV=ANOTHER_VALUE'")
29 | os.Exit(0)
30 | }
31 |
32 | envs := []msg.Env{}
33 |
34 | for _, env := range args {
35 | arr := strings.Split(env, "=")
36 |
37 | if len(arr) != 2 {
38 | logger.FdebuglnFatal("Error parsing environment variable:", env)
39 | logger.FatalLogln("Error parsing environment variable. Expected format 'env=value'. Got:", env)
40 | }
41 |
42 | name := arr[0]
43 | val := arr[1]
44 |
45 | if name == "" {
46 | logger.FdebuglnFatal("Error parsing environment variable - name is empty:", env)
47 | logger.FatalLogln("Error parsing environment variable. Expected format 'env=value'. Got:", env)
48 | }
49 | if val == "" {
50 | logger.FdebuglnFatal("Error parsing environment variable - val is empty:", env)
51 | logger.FatalLogln("Error parsing environment variable. Expected format 'env=value'. Got:", env)
52 | }
53 |
54 | envs = append(envs, msg.Env{name, val})
55 | }
56 |
57 | envMsg := msg.NewEnvMsg(authClient.IDToken, envs)
58 | if err := envMsg.Send(); err != nil {
59 | logger.FdebuglnError("Error setting environment variables:", err)
60 | logger.DebuglnError("Error setting environment variables:", err)
61 | return
62 | }
63 | logger.SuccessLogln("Variables Set")
64 | }
65 |
--------------------------------------------------------------------------------
/install:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | {
4 |
5 | set -e
6 |
7 | install_dir='/usr/local/bin'
8 | install_path='/usr/local/bin/foundry'
9 | OS=$(uname | tr '[:upper:]' '[:lower:]')
10 | ARCH=$(uname -m | tr '[:upper:]' '[:lower:]')
11 | cmd_exists() {
12 | command -v "$@" > /dev/null 2>&1
13 | }
14 |
15 | latestURL=https://github.com/FoundryApp/foundry-cli/releases/latest/download
16 |
17 |
18 | case "$OS" in
19 | darwin)
20 | URL=${latestURL}/foundry-macos-x86_64
21 | ;;
22 | linux)
23 | case "$ARCH" in
24 | x86_64)
25 | URL=${latestURL}/foundry-linux-x86_64
26 | ;;
27 | amd64)
28 | URL=${latestURL}/foundry-linux-x86_64
29 | ;;
30 | armv8*)
31 | URL=${latestURL}/foundry-linux-arm64
32 | ;;
33 | aarch64)
34 | URL=${latestURL}/foundry-linux-arm64
35 | ;;
36 | *)
37 | printf "$red> The architecture (${ARCH}) is not supported.$reset\n"
38 | exit 1
39 | ;;
40 | esac
41 | ;;
42 | *)
43 | printf "$red> The OS (${OS}) is not supported.$reset\n"
44 | exit 1
45 | ;;
46 | esac
47 |
48 | sh_c='sh -c'
49 | if [ ! -w "$install_dir" ]; then
50 | if [ "$user" != 'root' ]; then
51 | if cmd_exists sudo; then
52 | sh_c='sudo -E sh -c'
53 | elif cmd_exists su; then
54 | sh_c='su -c'
55 | else
56 | echo 'This script requires to run command as sudo. We are unable to find either "sudo" or "su".'
57 | exit 1
58 | fi
59 | fi
60 | fi
61 |
62 | printf "> Downloading $URL\n"
63 | download_path=$(mktemp)
64 | curl -fSL "$URL" -o "$download_path"
65 | chmod +x "$download_path"
66 |
67 | printf "> Installing $install_path\n"
68 | $sh_c "mv -f $download_path $install_path"
69 |
70 | printf "$green> Foundry successfully installed!\n$reset"
71 | }
72 |
--------------------------------------------------------------------------------
/prompt/cmd/envprint.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | c "foundry/cli/connection"
6 | "foundry/cli/firebase"
7 | "foundry/cli/logger"
8 |
9 | goprompt "github.com/mlejva/go-prompt"
10 | )
11 |
12 | type EnvPrintCmd struct {
13 | Text string
14 | Desc string
15 | RunCh RunChannelType
16 | IDToken string
17 | }
18 |
19 | func NewEnvPrintCmd(IDToken string) *EnvPrintCmd {
20 | return &EnvPrintCmd{
21 | Text: "env-print",
22 | Desc: "Print all environment variables in your cloud environment",
23 | RunCh: make(chan Args),
24 | IDToken: IDToken,
25 | }
26 | }
27 |
28 | // Implement Cmd interface
29 | func (c *EnvPrintCmd) Run(conn *c.Connection, args Args) (promptOutput string, promptInfo string, err error) {
30 | res, err := firebase.Call("getUserEnvs", c.IDToken, nil)
31 | if err != nil {
32 | logger.FdebuglnFatal("Error calling getUserEnvs:", err)
33 | return "", "", err
34 | }
35 | if res.Error != nil {
36 | logger.FdebuglnFatal("Error calling getUserEnvs:", res.Error)
37 | return "", "", fmt.Errorf(res.Error.Message)
38 | }
39 |
40 | envs, ok := res.Result.(map[string]interface{})
41 | if !ok {
42 | return "", "", fmt.Errorf("error printing environment variables. Failed to convert the response")
43 | }
44 |
45 | if len(envs) == 0 {
46 | return "", "No environment variable has been set yet", nil
47 | }
48 |
49 | delimiter := "-----------------------------------------------------"
50 | msg := "\n" + delimiter + "\n|\n"
51 | msg += "| Following environment variables are set:\n|"
52 | for k, v := range envs {
53 | msg += fmt.Sprintf("\n| %s=%s", k, v.(string))
54 | }
55 | msg += "\n|\n" + delimiter + "\n"
56 |
57 | return msg, "", nil
58 | }
59 |
60 | func (c *EnvPrintCmd) RunRequest(args Args) {
61 | c.RunCh <- args
62 | }
63 |
64 | func (c *EnvPrintCmd) ToSuggest() goprompt.Suggest {
65 | return goprompt.Suggest{c.Text, c.Desc}
66 | }
67 |
68 | func (c *EnvPrintCmd) Name() string {
69 | return c.Text
70 | }
71 |
72 | func (c *EnvPrintCmd) String() string {
73 | return fmt.Sprintf("%s - %s", c.Text, c.Desc)
74 | }
75 |
--------------------------------------------------------------------------------
/files/files.go:
--------------------------------------------------------------------------------
1 | package files
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "io"
7 |
8 | conn "foundry/cli/connection"
9 | connMsg "foundry/cli/connection/msg"
10 | "foundry/cli/logger"
11 |
12 | "foundry/cli/zip"
13 |
14 | "github.com/gobwas/glob"
15 | )
16 |
17 | var (
18 | lastArchiveChecksum = ""
19 | )
20 |
21 | func Upload(c *conn.Connection, rootDir, serviceAccPath string, promptNotifCh chan<- string, ignore ...glob.Glob) {
22 | // Zip the project in the memory and send the file in chunks
23 | buf, err := zip.ArchiveDir(rootDir, serviceAccPath, ignore)
24 | if err != nil {
25 | logger.FdebuglnFatal("ArchiveDir error:", err)
26 | logger.FatalLogln("Error uploading files:", err)
27 | }
28 |
29 | // err = ioutil.WriteFile("./source.zip", buf.Bytes(), 0644)
30 | // logger.FatalLogln("Written", err)
31 |
32 | archiveChecksum := checksum(buf.Bytes())
33 |
34 | // TODO: Temporarily disables
35 | // if lastArchiveChecksum == archiveChecksum {
36 | // promptNotifCh <- "No change in the code detected. Make change to upload the code."
37 | // return
38 | // }
39 | lastArchiveChecksum = archiveChecksum
40 |
41 | bufferSize := 1024 // 1024B, size of a single chunk
42 | buffer := make([]byte, bufferSize)
43 | chunkCount := (buf.Len() / bufferSize) + 1
44 |
45 | checksum := [md5.Size]byte{}
46 | previousChecksum := [md5.Size]byte{}
47 |
48 | for i := 0; i < chunkCount; i++ {
49 | bytesread, err := buf.Read(buffer)
50 | // TODO: Why did this work without err != io.EOF?
51 | if err != nil && err != io.EOF {
52 | logger.FdebuglnFatal("Error reading chunk from buffer:", err)
53 | logger.FatalLogln("Error reading chunk from buffer:", err)
54 | }
55 |
56 | previousChecksum = checksum
57 | bytes := buffer[:bytesread]
58 | checksum = md5.Sum(bytes)
59 |
60 | checkStr := hex.EncodeToString(checksum[:])
61 | prevCheckStr := hex.EncodeToString(previousChecksum[:])
62 |
63 | lastChunk := i == chunkCount-1
64 |
65 | chunk := connMsg.NewChunkMsg(bytes, checkStr, prevCheckStr, lastChunk)
66 | if err = c.Send(chunk); err != nil {
67 | logger.FdebuglnFatal("Error sending chunk", err)
68 | logger.FatalLogln("Error sending chunk", err)
69 | }
70 | }
71 | }
72 |
73 | func checksum(data []byte) string {
74 | hashInBytes := md5.Sum(data)
75 | return hex.EncodeToString(hashInBytes[:])
76 | }
77 |
--------------------------------------------------------------------------------
/connection/connection.go:
--------------------------------------------------------------------------------
1 | package connection
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "time"
7 |
8 | "foundry/cli/connection/endpoint"
9 | "foundry/cli/connection/msg"
10 | "foundry/cli/logger"
11 |
12 | "github.com/gorilla/websocket"
13 | )
14 |
15 | type ListenCallback func(data []byte, err error)
16 |
17 | type Connection struct {
18 | token string
19 | wsconn *websocket.Conn
20 | }
21 |
22 | type ConnectionMessage interface {
23 | Body() interface{}
24 | }
25 |
26 | // TODO: Use channels so the Connection struct is thread safe.
27 | // Gorilla's websocket.Conn can be accessed only from a single
28 | // goroutine.
29 |
30 | func New(token string, admin bool) (*Connection, error) {
31 | logger.Fdebugln("WS dialing")
32 | url := WebSocketURL(token, admin)
33 | c, _, err := websocket.DefaultDialer.Dial(url, nil)
34 | if err != nil {
35 | return nil, err
36 | }
37 | logger.Fdebugln("WS connected")
38 |
39 | return &Connection{token, c}, nil
40 | }
41 |
42 | func (c *Connection) Close() {
43 | if c.wsconn == nil {
44 | return
45 | }
46 | logger.Fdebugln("WS closing")
47 | c.wsconn.Close()
48 | }
49 |
50 | func (c *Connection) Listen(cb ListenCallback) {
51 | logger.Fdebugln(" WS listening")
52 | for {
53 | _, msg, err := c.wsconn.ReadMessage()
54 | cb(msg, err)
55 | }
56 | }
57 |
58 | // Sends WS message
59 | func (c *Connection) Send(cm ConnectionMessage) error {
60 | b := cm.Body()
61 | err := c.wsconn.WriteJSON(b)
62 | if err != nil {
63 | return err
64 | }
65 | return nil
66 | }
67 |
68 | // Pings server so the WS connection stays open
69 | func (c *Connection) Ping(pm *msg.PingMsg, ticker *time.Ticker, stop <-chan struct{}) {
70 | logger.Fdebugln("Ping")
71 | for {
72 | select {
73 | case <-ticker.C:
74 | if err := pm.Send(); err != nil {
75 | logger.FdebuglnFatal("Failed to ping server", err)
76 | logger.FatalLogln("Failed to ping server", err)
77 | }
78 | case <-stop:
79 | logger.Fdebugln("Stop pinging")
80 | ticker.Stop()
81 | return
82 | }
83 | }
84 | }
85 |
86 | func WebSocketURL(token string, admin bool) string {
87 | return fmt.Sprintf("%s://%s/ws/%s?admin=%s", endpoint.WebSocketScheme, endpoint.WebSocketURL, token, strconv.FormatBool(admin))
88 | }
89 |
90 | func PingURL() string {
91 | return fmt.Sprintf("%s://%s/ping", endpoint.PingScheme, endpoint.PingURL)
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/signup.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "foundry/cli/logger"
5 | "log"
6 | "os"
7 |
8 | "github.com/AlecAivazis/survey/v2"
9 | "github.com/AlecAivazis/survey/v2/terminal"
10 | "github.com/fatih/color"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var (
15 | signUpCmd = &cobra.Command{
16 | Use: "sign-up",
17 | Short: "Create new Foundry account in your terminal",
18 | Example: "foundry sign-up",
19 | Run: runSignUp,
20 | }
21 |
22 | emailQ = []*survey.Question{
23 | {
24 | Name: "email",
25 | Prompt: &survey.Input{Message: "Email:"},
26 | Validate: survey.Required,
27 | },
28 | }
29 |
30 | passQs = []*survey.Question{
31 | {
32 | Name: "pass",
33 | Prompt: &survey.Password{Message: "Password:"},
34 | Validate: survey.Required,
35 | },
36 | {
37 | Name: "passAgain",
38 | Prompt: &survey.Password{Message: "Password again:"},
39 | Validate: survey.Required,
40 | },
41 | }
42 | )
43 |
44 | func init() {
45 | rootCmd.AddCommand(signUpCmd)
46 | }
47 |
48 | func runSignUp(cmd *cobra.Command, args []string) {
49 | creds := struct {
50 | Email string `survey:"email`
51 | Pass string `survey:"pass`
52 | PassAgain string `survey:"passAgain`
53 | }{}
54 |
55 | logger.Logln("Create new Foundry account\n")
56 |
57 | // Ask for email
58 | err := survey.Ask(emailQ, &creds)
59 | // Without this specific "if" SIGINT (Ctrl+C) would only
60 | // interrupt the survey's prompt and not the whole program
61 | if err == terminal.InterruptErr {
62 | os.Exit(0)
63 | } else if err != nil {
64 | log.Println(err)
65 | }
66 |
67 | // Ask for password
68 | err = survey.Ask(passQs, &creds)
69 | // Without this specific "if" SIGINT (Ctrl+C) would only
70 | // interrupt the survey's prompt and not the whole program
71 | if err == terminal.InterruptErr {
72 | os.Exit(0)
73 | } else if err != nil {
74 | log.Println(err)
75 | }
76 |
77 | if creds.Pass != "" && creds.Pass != creds.PassAgain {
78 | color.Red("\n⨯ Passwords don't match. Please try again.")
79 | return
80 | }
81 |
82 | if err = authClient.SignUp(creds.Email, creds.Pass); err != nil {
83 | color.Red("⨯ Error")
84 | log.Println("HTTP request error", err)
85 | return
86 | }
87 |
88 | if authClient.Error != nil {
89 | color.Red("⨯ Error")
90 | log.Println("Auth error", authClient.Error)
91 | return
92 | }
93 |
94 | color.Green("\n✔ Signed up")
95 | }
96 |
--------------------------------------------------------------------------------
/prompt/cmd/envset.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | c "foundry/cli/connection"
6 | "foundry/cli/connection/msg"
7 | "foundry/cli/logger"
8 | "strings"
9 |
10 | goprompt "github.com/mlejva/go-prompt"
11 | )
12 |
13 | type EnvSetCmd struct {
14 | Text string
15 | Desc string
16 | RunCh RunChannelType
17 | IDToken string
18 | }
19 |
20 | func NewEnvSetCmd(IDToken string) *EnvSetCmd {
21 | return &EnvSetCmd{
22 | Text: "env-set",
23 | Desc: "Set environment variable(s) in your cloud environment",
24 | RunCh: make(chan Args),
25 | IDToken: IDToken,
26 | }
27 | }
28 |
29 | // Implement Cmd interface
30 | func (c *EnvSetCmd) Run(conn *c.Connection, args Args) (promptOutput string, promptInfo string, err error) {
31 | if len(args) == 0 {
32 | return "", "No envs specified. Example usage: 'foundry env-set MY_ENV=ENV_VALUE ANOTHER_ENV=ANOTHER_VALUE'", nil
33 | }
34 |
35 | envs := []msg.Env{}
36 | for _, env := range args {
37 | arr := strings.Split(env, "=")
38 |
39 | if len(arr) != 2 {
40 | logger.FdebuglnFatal("Error parsing environment variable:", env)
41 | return "", "", fmt.Errorf(fmt.Sprintf("error parsing environment variable. Expected format 'env=value'. Got: %s", env))
42 | }
43 |
44 | name := arr[0]
45 | val := arr[1]
46 |
47 | if name == "" {
48 | logger.FdebuglnFatal("Error parsing environment variable - name is empty:", env)
49 | return "", "", fmt.Errorf(fmt.Sprintf("error parsing environment variable. Expected format 'env=value'. Got: %s", env))
50 | }
51 | if val == "" {
52 | logger.FdebuglnFatal("Error parsing environment variable - val is empty:", env)
53 | return "", "", fmt.Errorf(fmt.Sprintf("error parsing environment variable. Expected format 'env=value'. Got: %s", env))
54 | }
55 |
56 | envs = append(envs, msg.Env{name, val})
57 | }
58 |
59 | envMsg := msg.NewEnvMsg(c.IDToken, envs)
60 | if err := envMsg.Send(); err != nil {
61 | logger.FdebuglnError("Error setting environment variables:", err)
62 | return "", "", err
63 | }
64 | return "", "Variables set", nil
65 | }
66 |
67 | func (c *EnvSetCmd) RunRequest(args Args) {
68 | c.RunCh <- args
69 | }
70 |
71 | func (c *EnvSetCmd) ToSuggest() goprompt.Suggest {
72 | return goprompt.Suggest{c.Text, c.Desc}
73 | }
74 |
75 | func (c *EnvSetCmd) Name() string {
76 | return c.Text
77 | }
78 |
79 | func (c *EnvSetCmd) String() string {
80 | return fmt.Sprintf("%s - %s", c.Text, c.Desc)
81 | }
82 |
--------------------------------------------------------------------------------
/prompt/cmd/envdel.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | c "foundry/cli/connection"
6 | "foundry/cli/connection/msg"
7 | "foundry/cli/firebase"
8 | "foundry/cli/logger"
9 | "strings"
10 |
11 | goprompt "github.com/mlejva/go-prompt"
12 | )
13 |
14 | type EnvDelCmd struct {
15 | Text string
16 | Desc string
17 | RunCh RunChannelType
18 | IDToken string
19 | }
20 |
21 | func NewEnvDelCmd(IDToken string) *EnvDelCmd {
22 | return &EnvDelCmd{
23 | Text: "env-delete",
24 | Desc: "Delete environment variable(s) from your cloud environment",
25 | RunCh: make(chan Args),
26 | IDToken: IDToken,
27 | }
28 | }
29 |
30 | // Implement Cmd interface
31 | func (c *EnvDelCmd) Run(conn *c.Connection, args Args) (promptOutput string, promptInfo string, err error) {
32 | if len(args) == 0 {
33 | return "", "No envs to delete specified. Example usage: 'foundry env-delete ENV_1 ENV_2'", nil
34 | }
35 |
36 | reqBody := struct {
37 | Delete []string `json:"delete"`
38 | }{args}
39 | res, err := firebase.Call("deleteUserEnvs", c.IDToken, reqBody)
40 | if err != nil {
41 | logger.FdebuglnFatal("Error calling deleteUserEnvs:", err)
42 | return "", "", fmt.Errorf(fmt.Sprintf("error deleting environment variables: %s", err))
43 | }
44 | if res.Error != nil {
45 | logger.FdebuglnFatal("Error calling deleteUserEnvs:", res.Error)
46 | return "", "", fmt.Errorf(fmt.Sprintf("error deleting environment variables: %s", err))
47 | }
48 |
49 | // Report new envs to Autorun
50 | envsMap, ok := res.Result.(map[string]interface{})
51 | if !ok {
52 | logger.FdebuglnFatal("Failed to type assert res.Result")
53 | return "", "", fmt.Errorf("error deleting environment variables")
54 | }
55 |
56 | envs := []msg.Env{}
57 | for name, val := range envsMap {
58 | envs = append(envs, msg.Env{name, val.(string)})
59 | }
60 | logger.Fdebugln("Sending new envs vars to Autorun:", envs)
61 |
62 | envMsg := msg.NewEnvMsg(c.IDToken, envs)
63 | if err = envMsg.Send(); err != nil {
64 | logger.FdebuglnError("Failed to report new env vars (after deletion) to Autorun", err)
65 | }
66 |
67 | return "", "Deleted " + strings.Join(args, ", "), err
68 | }
69 |
70 | func (c *EnvDelCmd) RunRequest(args Args) {
71 | c.RunCh <- args
72 | }
73 |
74 | func (c *EnvDelCmd) ToSuggest() goprompt.Suggest {
75 | return goprompt.Suggest{c.Text, c.Desc}
76 | }
77 |
78 | func (c *EnvDelCmd) Name() string {
79 | return c.Text
80 | }
81 |
82 | func (c *EnvDelCmd) String() string {
83 | return fmt.Sprintf("%s - %s", c.Text, c.Desc)
84 | }
85 |
--------------------------------------------------------------------------------
/cmd/envdel.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "foundry/cli/connection/msg"
6 | "foundry/cli/firebase"
7 | "foundry/cli/logger"
8 | "os"
9 | "strings"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var (
15 | envDelCmd = &cobra.Command{
16 | Use: "env-delete",
17 | Short: "Delete environment variable(s) from your cloud environment",
18 | Example: "foundry env-delete ENV_1 ENV_2",
19 | Run: runEnvDel,
20 | }
21 | )
22 |
23 | func init() {
24 | rootCmd.AddCommand(envDelCmd)
25 | }
26 |
27 | func runEnvDel(cmd *cobra.Command, args []string) {
28 | if len(args) == 0 {
29 | logger.WarningLogln("No envs to delete specified. Example usage: 'foundry env-delete ENV_1 ENV_2'")
30 | os.Exit(0)
31 | }
32 |
33 | reqBody := struct {
34 | Delete []string `json:"delete"`
35 | }{args}
36 |
37 | s := fmt.Sprintf("Deleting following env variables: '%s'...", strings.Join(args, ","))
38 | logger.Logln(s)
39 |
40 | res, err := firebase.Call("deleteUserEnvs", authClient.IDToken, reqBody)
41 | if err != nil {
42 | logger.FdebuglnFatal("Error calling deleteUserEnvs:", err)
43 | logger.FatalLogln("Error deleting environment variables (1):", err)
44 | }
45 | if res.Error != nil {
46 | logger.FdebuglnFatal("Error calling deleteUserEnvs:", res.Error)
47 | logger.FatalLogln("Error deleting environment variables (2):", res.Error)
48 | }
49 |
50 | // Send new envs to Autorun
51 | logger.Fdebugln("New env vars after deletion:", res.Result)
52 |
53 | envsMap, ok := res.Result.(map[string]interface{})
54 | if !ok {
55 | logger.FdebuglnFatal("Failed to type assert res.Result")
56 | logger.FatalLogln("Error deleting environment variables (3)")
57 | }
58 |
59 | envs := []msg.Env{}
60 | for name, val := range envsMap {
61 | envs = append(envs, msg.Env{name, val.(string)})
62 | }
63 | logger.Fdebugln("Sending new envs vars to Autorun:", envs)
64 |
65 | envMsg := msg.NewEnvMsg(authClient.IDToken, envs)
66 | if err := envMsg.Send(); err != nil {
67 | logger.FdebuglnError("Failed to report new env vars (after deletion) to Autorun", err)
68 | logger.DebuglnError("Error deleting environment variables (4)", err)
69 | return
70 | }
71 |
72 | // Print new envs
73 | logger.SuccessLogln("Deleted")
74 | logger.Logln("---------------")
75 |
76 | logger.Logln("")
77 | logger.Logln("Env variables now:")
78 | if len(envsMap) == 0 {
79 | logger.Logln("There are no env variables set in your environment")
80 | } else {
81 | for k, v := range envsMap {
82 | s := fmt.Sprintf("\t%s=%s\n", k, v.(string))
83 | logger.Log(s)
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/logger/debug.go:
--------------------------------------------------------------------------------
1 | // +build debug
2 |
3 | package logger
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "runtime"
9 | "time"
10 | )
11 |
12 | type PrefixType int
13 |
14 | const (
15 | DebugPrefix PrefixType = iota
16 | ErrorPrefix
17 | FatalPrefix
18 | )
19 |
20 | var (
21 | debugFile *os.File
22 | )
23 |
24 | func InitDebug(path string) error {
25 | if path == "" {
26 | return nil
27 | }
28 |
29 | dfile, err := os.Create(path)
30 | if err != nil {
31 | return err
32 | }
33 | debugFile = dfile
34 |
35 | Fdebugln("################## STARTING SESSION")
36 | return nil
37 | }
38 |
39 | func Close() {
40 | if debugFile == nil {
41 | return
42 | }
43 | debugFile.Close()
44 | }
45 |
46 | func Fdebugln(v ...interface{}) {
47 | if debugFile == nil {
48 | return
49 | }
50 |
51 | str := fmt.Sprintf("%s %s", prefix(DebugPrefix), fmt.Sprintln(v...))
52 | fmt.Fprint(debugFile, str)
53 | }
54 |
55 | func FdebuglnError(v ...interface{}) {
56 | if debugFile == nil {
57 | return
58 | }
59 |
60 | str := fmt.Sprintf("%s %s", prefix(ErrorPrefix), fmt.Sprintln(v...))
61 | fmt.Fprint(debugFile, str)
62 | }
63 |
64 | func FdebuglnFatal(v ...interface{}) {
65 | if debugFile == nil {
66 | return
67 | }
68 |
69 | str := fmt.Sprintf("%s %s", prefix(FatalPrefix), fmt.Sprintln(v...))
70 | fmt.Fprint(debugFile, str)
71 | // fmt.FPrint(debugFile, runtimeDebug.Stack())
72 | panic(str)
73 | }
74 |
75 | // Doesn't write to the debug file
76 | func Debugln(v ...interface{}) {
77 | str := fmt.Sprintf("%s %s", prefix(DebugPrefix), fmt.Sprintln(v...))
78 | fmt.Print(str)
79 | }
80 |
81 | // Doesn't write to the debug file
82 | func DebuglnError(v ...interface{}) {
83 | str := fmt.Sprintf("%s %s", prefix(ErrorPrefix), fmt.Sprintln(v...))
84 | fmt.Print(str)
85 | }
86 |
87 | // Doesn't write to the debug file
88 | func DebuglnFatal(v ...interface{}) {
89 | str := fmt.Sprintf("%s %s", prefix(FatalPrefix), fmt.Sprintln(v...))
90 | panic(str)
91 | }
92 |
93 | func prefix(t PrefixType) (prefix string) {
94 | h, m, s := time.Now().Clock()
95 | timePrefix := fmt.Sprintf("%d:%02d:%02d", h, m, s)
96 |
97 | bold := "\x1b[1m"
98 | red := "\x1b[31m"
99 | endSeq := "\x1b[0m"
100 |
101 | switch t {
102 | case DebugPrefix:
103 | prefix = fmt.Sprintf("%sDEBUG%s", bold, endSeq)
104 | case FatalPrefix:
105 | prefix = fmt.Sprintf("%s%sFATAL%s", red, bold, endSeq)
106 | case ErrorPrefix:
107 | prefix = fmt.Sprintf("%s%sERROR%s", red, bold, endSeq)
108 | default:
109 | prefix = fmt.Sprintf("%sDEBUG%s", bold, endSeq)
110 | }
111 |
112 | // We're using 2, to ascend 2 stack frames
113 | pc, _, line, _ := runtime.Caller(2)
114 | debugInfo := fmt.Sprintf("[%s:%d]", runtime.FuncForPC(pc).Name(), line)
115 |
116 | return fmt.Sprintf("%s %s %s", timePrefix, prefix, debugInfo)
117 | }
118 |
--------------------------------------------------------------------------------
/cmd/init.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "foundry/cli/logger"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | var (
13 | initCmd = &cobra.Command{
14 | Use: "init",
15 | Short: "Create the initial foundry.yaml config file",
16 | Example: "foundry init",
17 | Run: runInit,
18 | }
19 | )
20 |
21 | func init() {
22 | rootCmd.AddCommand(initCmd)
23 | }
24 |
25 | func runInit(cmd *cobra.Command, args []string) {
26 | if _, err := os.Stat(confFile); !os.IsNotExist(err) {
27 | logger.FdebuglnError("Foundry config file 'foundry.yaml' already exists")
28 | logger.FatalLogln("Foundry config file 'foundry.yaml' already exists")
29 | }
30 |
31 | dest := filepath.Join(foundryConf.CurrentDir, "foundry.yaml")
32 | err := ioutil.WriteFile(dest, []byte(getInitYaml()), 0644)
33 | if err != nil {
34 | logger.FdebuglnError("Error writing foundry.yaml:", err)
35 | logger.FatalLogln("Error creating Foundry config file 'foundry.yaml':", err)
36 | }
37 |
38 | logger.SuccessLogln("Config file 'foundry.yaml' created")
39 | }
40 |
41 | func getInitYaml() string {
42 | // TODO: Update to a final version of the init config yaml
43 | return `
44 | # An array of glob patterns for files that should be ignored. The path is relative to the root dir.
45 | # If the array is changed, the CLI must be restarted for it to take the effect
46 | # See https://docs.foundryapp.co/configuration-file/ignore-directories-or-files
47 | ignore:
48 | # Skip the whole node_modules directory
49 | - node_modules
50 | # Skip the whole .git directory
51 | - .git
52 | # Skip all hidden files
53 | - "**/.*"
54 | # Skip vim's temp files
55 | - "**/*~"
56 | # Ignore Firebase log files
57 | - "**/firebase-debug.log"
58 |
59 | # Enable TypeScript
60 | # See https://docs.foundryapp.co/resources/supported-languages#using-foundry-with-cloud-functions-in-typescript
61 | # typescript: true
62 |
63 | # An array describing emulated Firebase Auth users in your cloud environment
64 | # See https://docs.foundryapp.co/configuration-file/emulate-users
65 | users:
66 | - id: user-id-1
67 | # The 'data' field takes a JSON string
68 | data: '{"email": "user-id-1-email@email.com"}'
69 |
70 |
71 | # An array describing emulated Firestore in your cloud environment
72 | # See https://docs.foundryapp.co/configuration-file/emulate-firestore
73 | firestore:
74 | # You can describe your emulated Firestore either directly
75 | - collection: workspaces
76 | docs:
77 | - id: ws-id-1
78 | data: '{"userId": "user-id-1"}'
79 |
80 | # An array describing your Firebase functions that should be evaluated by Foundry.
81 | # All described functions must be exported in the function's root index.js file.
82 | # In this array, you describe how Foundry should trigger each function in every run.
83 | # See https://docs.foundryapp.co/configuration-file/config-functions
84 | functions:
85 | - name: myHttpsFunction
86 | type: https
87 | payload: '{"field":"value"}'
88 | `
89 | }
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Foundry - The fastest way to build Firebase CloudFunctions
3 |
4 | - Website: [https://foundryapp.co](https://foundryapp.co)
5 | - Docs: [https://docs.foundryapp.co](https://docs.foundryapp.co)
6 | - Community Slack: [Join Foundry Community Slack](https://join.slack.com/t/community-foundry/shared_invite/zt-dcpyblnb-JSSWviMFbRvjGnikMAWJeA)
7 | - Youtube channel: [Foundry Youtube Channel](https://www.youtube.com/channel/UCvNVqSIXlW6nSPlAvW78TQg)
8 |
9 |
10 |
11 | Foundry lets you build your Firebase Cloud Functions notably faster, with less configuration, and with easy access to your production data.
12 | Foundry consists of an open-sourced command-line tool called Foundry CLI and a pre-configured cloud environment for your development.
13 |
14 |
15 | ## Watch the 5-minute video explaining Foundry
16 |
17 | [](https://youtu.be/wYPbR8MnNfE)
18 |
19 |
20 | The key features of Foundry are:
21 | - **Develop with a copy of your production data:** Specify what data you want to copy from your production Firestore, production RealtimeDB and production users. We copy the data and fill the emulated Firestore, emulated RealtimeDB, and Firebase Auth. No need to maintain any custom scripts. You access this data as you would normally in your Firebase functions code - with the official Admin SDK.
22 |
23 | - **Real-time feedback:** You don't have to manually trigger your functions to run them, Foundry triggers them for you every time you make a change in your code and sends you back the output usually within 1-2 seconds. You just define your Cloud Functions and how you want to trigger them in the configuration file. It's like Read-Eval-Print-Loop for your Cloud Functions.
24 |
25 | - **Develop in the environment identical to the production environment:** Your Firebase Cloud Functions will run in a cloud environment that is identical to the environment where your functions are deployed. This way, you won't have unexpected production bugs. You don't have to create a separate Firebase project as your staging environment. Foundry is your staging environment.
26 |
27 | - **Zero environment configuration:** There isn't any configuration. Just run `$ foundry init` and then `$ foundry go` and you're ready.
28 |
29 | - **Easily test integration of your Cloud Functions:** Foundry gives you an access the emulated Firestore database, emulated Realtime DB, and emulated users. You can specify with what data they should be filled with and what parts of production Firestore, productiom RealtimeDB and users data should be copied to the cloud development environment. Together with the specification of how your Cloud Functions should be triggered every time you save your code, Foundry can load and trigger your Cloud Functions in the same way as they would be triggered on the Firebase platform.
30 |
31 |
32 | ## Getting Started & Documentation
33 | Documentation is available on the [Foundry website](https://docs.foundryapp.co)
34 |
35 | ### Quick start
36 |
37 | Installation via curl:
38 | ```bash
39 | $ curl https://get.foundryapp.co -sSfL | sh
40 | ```
41 |
42 | Installation view Brew on macOS:
43 | ```bash
44 | $ brew tap foundryapp/foundry-cli
45 | $ brew install foundry
46 | ```
47 |
48 | Run Foundry:
49 | ```bash
50 | $ cd
51 | $ foundry init
52 | $ foundry go
53 | ```
54 |
55 | ## License
56 | [Mozilla Public License v2.0](https://github.com/foundryapp/foundry-cli/blob/master/LICENSE)
57 |
--------------------------------------------------------------------------------
/rwatch/rwatch.go:
--------------------------------------------------------------------------------
1 | package rwatch
2 |
3 | import (
4 | "foundry/cli/logger"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/fsnotify/fsnotify"
9 | "github.com/gobwas/glob"
10 | )
11 |
12 | type Watcher struct {
13 | Events chan fsnotify.Event
14 | Errors chan error
15 |
16 | fsnotify *fsnotify.Watcher
17 | done chan struct{}
18 |
19 | ignore []glob.Glob
20 | }
21 |
22 | // var ignore = []string{".git", "node_modules", ".foundry"}
23 |
24 | func New(ignore []glob.Glob) (*Watcher, error) {
25 | fsw, err := fsnotify.NewWatcher()
26 | if err != nil {
27 | return nil, err
28 | }
29 | w := &Watcher{
30 | fsnotify: fsw,
31 | Events: make(chan fsnotify.Event),
32 | Errors: make(chan error),
33 | done: make(chan struct{}),
34 | ignore: ignore,
35 | }
36 |
37 | go w.start()
38 | return w, nil
39 | }
40 |
41 | func (w *Watcher) AddRecursive(dir string) error {
42 | return w.traverse(dir, true)
43 | }
44 |
45 | func (w *Watcher) Close() {
46 | logger.Fdebugln("Closing rwatch")
47 | w.fsnotify.Close()
48 | close(w.done)
49 | }
50 |
51 | func (w *Watcher) start() {
52 | for {
53 | select {
54 | case ev := <-w.fsnotify.Events:
55 | fi, err := os.Stat(ev.Name)
56 | if err == nil && fi != nil && fi.IsDir() {
57 | if ev.Op == fsnotify.Create {
58 | if err = w.traverse(ev.Name, true); err != nil {
59 | w.Errors <- err
60 | }
61 | }
62 | }
63 |
64 | // os.Stat() can't be used on deleted dir/file
65 | // Pretend it was a directory (we don't really know)
66 | // and try to remove it
67 | if ev.Op == fsnotify.Remove {
68 | w.fsnotify.Remove(ev.Name)
69 | }
70 |
71 | if ev.Op != fsnotify.Chmod {
72 | w.Events <- ev
73 | }
74 | case err := <-w.fsnotify.Errors:
75 | w.Errors <- err
76 |
77 | case <-w.done:
78 | close(w.Events)
79 | close(w.Errors)
80 | return
81 | }
82 | }
83 | }
84 |
85 | // Traverses the root directory and adds watcher for each directory along the way
86 | // We don't care for files, only for directories because we are watching whole dirs
87 | func (w *Watcher) traverse(start string, watch bool) error {
88 | walkfn := func(path string, info os.FileInfo, err error) error {
89 | logger.Fdebugln("")
90 | logger.Fdebugln("path in rwatch", path)
91 |
92 | // Prepend path with the "./" so the prefix
93 | // is same as the ignore array in the config
94 | // file.
95 | // TODO: Should the prefix be foundryConf.RootDir?
96 | // path = "." + string(os.PathSeparator) + path
97 |
98 | if err != nil {
99 | // TODO: If path is in the ignored array, should we ignore the error?
100 | // Note: we can't use info.IsDir() here because of the error - the file
101 | // might not even exist. Using info.IsDir() would cause panic
102 | logger.FdebuglnError("rwatch walk error - path", path)
103 | logger.FdebuglnError("rwatch walk error - error", err)
104 | if w.ignored(path) {
105 | return nil
106 | }
107 | return err
108 | }
109 |
110 | isIgnored := w.ignored(path)
111 | logger.Fdebugln("is ignored?", isIgnored)
112 |
113 | if isIgnored {
114 | // If it's a directory, skip the whole directory
115 | if info.IsDir() {
116 | logger.Fdebugln("\t- Skipping dir")
117 | // No need to remove watcher on an ignored dir because watcher isn't recursive
118 | // i.e.: if we have following folder structure:
119 | // rootDir/
120 | // file1
121 | // subDir/
122 | // file2
123 | // then when we add rootDir to watcher, the subDir isn't added
124 | return filepath.SkipDir
125 | }
126 |
127 | // Always remove watcher on an ignored file because the file could be in a folder that is watched
128 | logger.Fdebugln("\t- Skipping file (removing watch)")
129 | return w.fsnotify.Remove(path)
130 | }
131 |
132 | if watch && !isIgnored {
133 | logger.Fdebugln("\t- Adding file/dir to rwatch")
134 | return w.fsnotify.Add(path)
135 | } else if !watch {
136 | logger.Fdebugln("\t- Removing file/dir from rwatch")
137 | return w.fsnotify.Remove(path)
138 | }
139 |
140 | return nil
141 | }
142 | return filepath.Walk(start, walkfn)
143 | }
144 |
145 | func (w *Watcher) ignored(s string) bool {
146 | logger.Fdebugln("string to match:", s)
147 | for _, g := range w.ignore {
148 | logger.Fdebugln("\t- glob:", g)
149 | logger.Fdebugln("\t- match:", g.Match(s))
150 | if g.Match(s) {
151 | return true
152 | }
153 | }
154 | return false
155 | }
156 |
--------------------------------------------------------------------------------
/zip/zip.go:
--------------------------------------------------------------------------------
1 | package zip
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "foundry/cli/logger"
7 | "io"
8 | "os"
9 | "path/filepath"
10 | "time"
11 |
12 | "github.com/gobwas/glob"
13 | )
14 |
15 | var (
16 | buf = new(bytes.Buffer)
17 | )
18 |
19 | // Recursively zips the directory
20 | func ArchiveDir(rootDir, serviceAccPath string, ignore []glob.Glob) (*bytes.Buffer, error) {
21 | buf.Reset()
22 | zw := zip.NewWriter(buf)
23 | defer zw.Close()
24 |
25 | // Walk all dirs inside the dir and
26 | // return all file paths (also walks
27 | // all subdirs)
28 | fPaths, err := walk(rootDir, ignore)
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | for _, fPath := range fPaths {
34 | err = addToZip(rootDir, fPath, zw)
35 | if err != nil {
36 | return nil, err
37 | }
38 | }
39 |
40 | // Zip service account - service account
41 | // might be in a completely different dir.
42 | // That's why we are adding it separately
43 | if serviceAccPath != "" {
44 | err = addServiceAccToZip(serviceAccPath, zw)
45 | if err != nil {
46 | return nil, err
47 | }
48 | }
49 | return buf, nil
50 | }
51 |
52 | func walk(start string, ignore []glob.Glob) ([]string, error) {
53 | var filePaths []string
54 |
55 | walkfn := func(path string, info os.FileInfo, err error) error {
56 | logger.Fdebugln("")
57 | logger.Fdebugln("path in zip", path)
58 |
59 | if err != nil {
60 | // TODO: If path is in the ignored array, should we ignore the error?
61 | if ignored(path, ignore) {
62 | return nil
63 | }
64 | logger.FdebuglnError("Zip walk error - path", path)
65 | logger.FdebuglnError("Zip walk error - error", err)
66 | return err
67 | }
68 |
69 | if ignored(path, ignore) {
70 | // If it's a directory, skip the whole directory
71 | if info.IsDir() {
72 | logger.Fdebugln("\t- Skipping dir")
73 | return filepath.SkipDir
74 | }
75 | // If it's a file, skip the file by returning nil
76 | logger.Fdebugln("\t- Skipping file")
77 | return nil
78 | }
79 |
80 | // Dirs aren't zipped - zip file creates a folder structure
81 | // automatically if we later specify full paths for files
82 | if !info.IsDir() {
83 | filePaths = append(filePaths, path)
84 | }
85 |
86 | return nil
87 | }
88 |
89 | err := filepath.Walk(start, walkfn)
90 |
91 | return filePaths, err
92 | }
93 |
94 | func addToZip(rootDir, fPath string, zw *zip.Writer) error {
95 | fileToZip, err := os.Open(fPath)
96 | // fi, err := f.Stat()
97 | // log.Println("add", fi.Size())
98 | if err != nil {
99 | return err
100 | }
101 | defer fileToZip.Close()
102 |
103 | // Get the file information
104 | info, err := fileToZip.Stat()
105 | if err != nil {
106 | return err
107 | }
108 |
109 | // file -> info header -> edit header -> create hader in the zip using zip writer
110 |
111 | h, err := zip.FileInfoHeader(info)
112 | if err != nil {
113 | return err
114 | }
115 |
116 | // Using FileInfoHeader() above only uses the basename of the file. If we want
117 | // to preserve the folder structure we want to get a relative path of the fPath
118 | // to the current working directory
119 | relativeFilePath, err := filepath.Rel(rootDir, fPath)
120 | if err != nil {
121 | return err
122 | }
123 | h.Name = relativeFilePath
124 |
125 | // Change to deflate to gain better compression
126 | // see http://golang.org/pkg/archive/zip/#pkg-constants
127 | h.Method = zip.Deflate
128 |
129 | // Reset time values so they don't influence
130 | // the checksum of the created zip file
131 | h.Modified = time.Time{}
132 | h.ModifiedTime = uint16(0)
133 | h.ModifiedDate = uint16(0)
134 |
135 | headerWriter, err := zw.CreateHeader(h)
136 | if err != nil {
137 | return err
138 | }
139 |
140 | _, err = io.Copy(headerWriter, fileToZip)
141 | return err
142 | }
143 |
144 | // Everything is same as addToZip besides preserving the
145 | // serviceAcc's file path structure. We want to get only
146 | // the last part of the serviceAcc's path so it's in the
147 | // root of the zip file
148 | func addServiceAccToZip(fPath string, zw *zip.Writer) error {
149 | fileToZip, err := os.Open(fPath)
150 | // fi, err := f.Stat()
151 | // log.Println("add", fi.Size())
152 | if err != nil {
153 | return err
154 | }
155 | defer fileToZip.Close()
156 |
157 | // Get the file information
158 | info, err := fileToZip.Stat()
159 | if err != nil {
160 | return err
161 | }
162 |
163 | // file -> info header -> edit header -> create hader in the zip using zip writer
164 |
165 | h, err := zip.FileInfoHeader(info)
166 | if err != nil {
167 | return err
168 | }
169 |
170 | // We want to add service account into the root of the zip file
171 | // Therefore we take only the last part (= file name) of its path
172 | _, fName := filepath.Split(fPath)
173 | h.Name = fName
174 |
175 | // Change to deflate to gain better compression
176 | // see http://golang.org/pkg/archive/zip/#pkg-constants
177 | h.Method = zip.Deflate
178 |
179 | // Reset time values so they don't influence
180 | // the checksum of the created zip file
181 | h.Modified = time.Time{}
182 | h.ModifiedTime = uint16(0)
183 | h.ModifiedDate = uint16(0)
184 |
185 | headerWriter, err := zw.CreateHeader(h)
186 | if err != nil {
187 | return err
188 | }
189 |
190 | _, err = io.Copy(headerWriter, fileToZip)
191 | return err
192 | return nil
193 | }
194 |
195 | func ignored(s string, globs []glob.Glob) bool {
196 | logger.Fdebugln("string to match:", s)
197 | for _, g := range globs {
198 | logger.Fdebugln("\t- glob:", g)
199 | logger.Fdebugln("\t- match:", g.Match(s))
200 | if g.Match(s) {
201 | return true
202 | }
203 | }
204 | return false
205 | }
206 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | "time"
9 |
10 | "foundry/cli/auth"
11 | conn "foundry/cli/connection"
12 | "foundry/cli/logger"
13 |
14 | "github.com/gobwas/glob"
15 | "github.com/spf13/cobra"
16 | "gopkg.in/yaml.v2"
17 | )
18 |
19 | type FoundryConf struct {
20 | ServiceAccPath string `yaml:"serviceAcc"`
21 | IgnoreStrPatterns []string `yaml:"ignore"`
22 | Admin bool `yaml:"admin"`
23 |
24 | CurrentDir string `yaml:"-"` // Current working directory of CLI
25 | Ignore []glob.Glob `yaml:"-"`
26 | }
27 |
28 | // Search a Foundry config file in the same directory from what was the foundry CLI called
29 | const confFile = "./foundry.yaml"
30 |
31 | var (
32 | debugFile = ""
33 | authClient *auth.Auth
34 | connectionClient *conn.Connection
35 | foundryConf = FoundryConf{}
36 |
37 | rootCmd = &cobra.Command{
38 | Use: "foundry",
39 | Short: "Better serverless dev",
40 | Example: "foundry --help",
41 | Run: func(cmd *cobra.Command, args []string) {
42 | logger.Logln("Foundry v0.2.4\n")
43 | logger.Logln("No subcommand was specified. To see all commands type 'foundry --help'")
44 | },
45 | }
46 | )
47 |
48 | func init() {
49 | // WARNING: logger's debug file isn't initialized yet. We can log only to the stdout or stderr.
50 |
51 | if len(os.Args) == 1 {
52 | return
53 | }
54 |
55 | cmd := os.Args[1]
56 |
57 | cobra.OnInitialize(func() { cobraInitCallback(cmd) })
58 |
59 | AddRootFlags(rootCmd)
60 |
61 | // TODO: Can this be in cobraInitCallback instead of here?
62 | if cmd != "init" &&
63 | cmd != "sign-out" &&
64 | cmd != "sign-in" &&
65 | cmd != "sign-up" &&
66 | cmd != "env-set" &&
67 | cmd != "env-print" &&
68 | cmd != "--help" {
69 | fmt.Println("Loading foundry.yaml...")
70 |
71 | if _, err := os.Stat(confFile); os.IsNotExist(err) {
72 | logger.DebuglnError("Foundry config file 'foundry.yaml' not found in the current directory")
73 | logger.FatalLogln("Foundry config file 'foundry.yaml' not found in the current directory. Run '\x1b[1mfoundry init\x1b[0m'.")
74 | }
75 |
76 | confData, err := ioutil.ReadFile(confFile)
77 | if err != nil {
78 | logger.DebuglnError("Can't read 'foundry.yaml' file", err)
79 | logger.FatalLogln("Can't read 'foundry.yaml' file", err)
80 | }
81 |
82 | err = yaml.Unmarshal(confData, &foundryConf)
83 | if err != nil {
84 | logger.DebuglnError("Config file 'foundry.yaml' isn't valid", err)
85 | logger.FatalLogln("Config file 'foundry.yaml' isn't valid", err)
86 | }
87 |
88 | dir, err := os.Getwd()
89 | if err != nil {
90 | logger.DebuglnError("Couldn't get current working directory", err)
91 | logger.FatalLogln("Couldn't get current working directory", err)
92 | }
93 | foundryConf.CurrentDir = dir
94 |
95 | // Parse IgnoreStr to globs
96 | for _, p := range foundryConf.IgnoreStrPatterns {
97 | // Add foundryConf.CurrentDir as a prefix to every glob pattern so
98 | // the prefix is same with file paths from watcher and zipper
99 |
100 | // last := foundryConf.RootDir[len(foundryConf.RootDir)-1:]
101 | // if last != string(os.PathSeparator) {
102 | // p = foundryConf.RootDir + string(os.PathSeparator) + p
103 | // } else {
104 | // p = foundryConf.RootDir + p
105 | // }
106 |
107 | p = filepath.Join(foundryConf.CurrentDir, p)
108 | g, err := glob.Compile(p)
109 | if err != nil {
110 | logger.DebuglnError("Invalid glob pattern in the 'ignore' field in the foundry.yaml file")
111 | logger.FatalLogln("Invalid glob pattern in the 'ignore' field in the foundry.yaml file")
112 | }
113 | foundryConf.Ignore = append(foundryConf.Ignore, g)
114 | }
115 | }
116 | }
117 |
118 | func cobraInitCallback(cmd string) {
119 | if err := logger.InitDebug(debugFile); err != nil {
120 | logger.DebuglnFatal("Failed to initialize a debug file for logger", err)
121 | }
122 |
123 | a, err := auth.New()
124 | if err != nil {
125 | logger.FdebuglnError("Error initializing Auth", err)
126 | logger.FatalLogln("Error initializing Auth", err)
127 | }
128 | if err := a.RefreshIDToken(); err != nil {
129 | logger.FdebuglnError("Error refreshing ID token: ", err)
130 | logger.FatalLogln("Error refreshing ID token: ", err)
131 | }
132 | authClient = a
133 |
134 | if cmd != "init" &&
135 | cmd != "sign-out" &&
136 | cmd != "sign-in" &&
137 | cmd != "sign-up" &&
138 | cmd != "--help" {
139 | logger.Log("\n")
140 | warningText := "You aren't signed in. Some features won't be available! To sign in, run \x1b[1m'foundry sign-in'\x1b[0m or \x1b[1m'foundry sign-up'\x1b[0m to sign up.\nThis message will self-destruct in 5s...\n"
141 |
142 | // Check if user signed in
143 | switch authClient.AuthState {
144 | case auth.AuthStateTypeSignedOut:
145 | // Sign in anonmoysly + notify user
146 | if err := authClient.SignUpAnonymously(); err != nil {
147 | logger.FdebuglnFatal(err)
148 | logger.FatalLogln(err)
149 | }
150 |
151 | if authClient.Error != nil {
152 | logger.FdebuglnFatal(authClient.Error)
153 | logger.FatalLogln(authClient.Error)
154 | }
155 |
156 | logger.WarningLogln(warningText)
157 | time.Sleep(time.Second * 5)
158 | case auth.AuthStateTypeSignedInAnonymous:
159 | // Notify user
160 | logger.WarningLogln(warningText)
161 | time.Sleep(time.Second)
162 | }
163 |
164 | // TODO: Now only 'go' command can use connectionClient variable
165 | // This should be handled better
166 | if cmd == "go" {
167 | // Create a new connection to the cloud env
168 | fmt.Println("Connecting to your cloud environment...")
169 | c, err := conn.New(authClient.IDToken, foundryConf.Admin)
170 | if err != nil {
171 | logger.FdebuglnFatal("Connection error", err)
172 | logger.FatalLogln(err)
173 | }
174 | connectionClient = c
175 | }
176 | }
177 | }
178 |
179 | func Execute() {
180 | defer func() {
181 | if connectionClient != nil {
182 | connectionClient.Close()
183 | }
184 | logger.Close()
185 | }()
186 |
187 | if err := rootCmd.Execute(); err != nil {
188 | logger.FdebuglnError("Error executing root command", err)
189 | logger.FatalLogln(err)
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/cmd/go.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | // "foundry go" or "foundry connect" or "foundry " or "foundry start" or "foundry link"?
4 |
5 | import (
6 | "encoding/json"
7 | "fmt"
8 | "os"
9 | "strings"
10 | "time"
11 |
12 | conn "foundry/cli/connection"
13 | connMsg "foundry/cli/connection/msg"
14 | "foundry/cli/files"
15 | "foundry/cli/logger"
16 | p "foundry/cli/prompt"
17 | promptCmd "foundry/cli/prompt/cmd"
18 | "foundry/cli/rwatch"
19 |
20 | "github.com/gobwas/glob"
21 | "github.com/spf13/cobra"
22 | )
23 |
24 | var (
25 | lastArchiveChecksum = ""
26 | goCmd = &cobra.Command{
27 | Use: "go",
28 | Short: "Connect to your cloud environment and start watching your Firebase Functions",
29 | Example: "foundy go",
30 | Run: runGo,
31 | }
32 |
33 | prompt *p.Prompt
34 | df *os.File
35 | )
36 |
37 | func init() {
38 | rootCmd.AddCommand(goCmd)
39 | }
40 |
41 | func runGo(cmd *cobra.Command, args []string) {
42 | done := make(chan struct{})
43 |
44 | watchCmd := promptCmd.NewWatchCmd()
45 | watchAllCmd := promptCmd.NewWatchAllCmd()
46 | exitCmd := promptCmd.NewExitCmd()
47 | envPrintCmd := promptCmd.NewEnvPrintCmd(authClient.IDToken)
48 | envSetCmd := promptCmd.NewEnvSetCmd(authClient.IDToken)
49 | envDelCmd := promptCmd.NewEnvDelCmd(authClient.IDToken)
50 |
51 | cmds := []promptCmd.Cmd{watchCmd, watchAllCmd, exitCmd, envPrintCmd, envSetCmd, envDelCmd}
52 | prompt = p.NewPrompt(cmds)
53 | go prompt.Run()
54 |
55 | // Listen for messages from the WS connection
56 | go connectionClient.Listen(listenCallback)
57 |
58 | // Start periodically pinging server so the env isn't killed
59 | pingMsg := connMsg.NewPingMsg(conn.PingURL(), authClient.IDToken)
60 | ticker := time.NewTicker(time.Second * 10)
61 | go connectionClient.Ping(pingMsg, ticker, done)
62 |
63 | // Start the file watcher
64 | w, err := rwatch.New(foundryConf.Ignore)
65 | if err != nil {
66 | logger.FdebuglnFatal("Watcher error", err)
67 | logger.FatalLogln(err)
68 | }
69 | defer w.Close()
70 |
71 | err = w.AddRecursive(foundryConf.CurrentDir)
72 | if err != nil {
73 | logger.FdebuglnFatal("watcher AddRecursive", err)
74 | logger.FatalLogln(err)
75 | }
76 |
77 | initialUploadCh := make(chan struct{}, 1)
78 | promptNotifCh := make(chan string)
79 |
80 | // The main goroutine handling all file events + prompt command requests
81 | // Command requests are all handled from a single goroutine because
82 | // Gorilla's websocket connection supports only one concurrent reader
83 | // and one concurrent writer.
84 | // More info - https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency
85 | go func() {
86 | for {
87 | select {
88 | case event := <-prompt.Events:
89 | if event.Type == p.PromptEventTypeRerender {
90 | _ = prompt.ShowLoading()
91 | files.Upload(connectionClient, foundryConf.CurrentDir, foundryConf.ServiceAccPath, promptNotifCh, foundryConf.Ignore...)
92 | }
93 | case msg := <-promptNotifCh:
94 | prompt.SetInfoln(msg, p.InfoLineSeverityNormal)
95 | case args := <-envDelCmd.RunCh:
96 | _ = prompt.ShowLoading()
97 | pOut, pInfo, err := envDelCmd.Run(connectionClient, args)
98 | if err != nil {
99 | prompt.SetInfoln(err.Error(), p.InfoLineSeverityError)
100 | continue
101 | }
102 | prompt.SetInfoln(pInfo, p.InfoLineSeverityNormal)
103 | prompt.Writeln(pOut)
104 | case args := <-envSetCmd.RunCh:
105 | _ = prompt.ShowLoading()
106 | pOut, pInfo, err := envSetCmd.Run(connectionClient, args)
107 | if err != nil {
108 | prompt.SetInfoln(err.Error(), p.InfoLineSeverityError)
109 | continue
110 | }
111 | prompt.SetInfoln(pInfo, p.InfoLineSeverityNormal)
112 | prompt.Writeln(pOut)
113 | case args := <-envPrintCmd.RunCh:
114 | _ = prompt.ShowLoading()
115 | pOut, pInfo, err := envPrintCmd.Run(connectionClient, args)
116 | if err != nil {
117 | prompt.SetInfoln(err.Error(), p.InfoLineSeverityError)
118 | continue
119 | }
120 | prompt.SetInfoln(pInfo, p.InfoLineSeverityNormal)
121 | prompt.Writeln(pOut)
122 | case args := <-watchAllCmd.RunCh:
123 | if _, _, err := watchAllCmd.Run(connectionClient, args); err != nil {
124 | prompt.SetInfoln(err.Error(), p.InfoLineSeverityError)
125 | continue
126 | }
127 | case args := <-watchCmd.RunCh:
128 | _, pInfo, err := watchCmd.Run(connectionClient, args)
129 | if err != nil {
130 | prompt.SetInfoln(err.Error(), p.InfoLineSeverityError)
131 | continue
132 | }
133 | prompt.SetInfoln(pInfo, p.InfoLineSeverityError)
134 | case args := <-exitCmd.RunCh:
135 | _, _, _ = exitCmd.Run(connectionClient, args)
136 | case <-initialUploadCh:
137 | files.Upload(connectionClient, foundryConf.CurrentDir, foundryConf.ServiceAccPath, promptNotifCh, foundryConf.Ignore...)
138 | case e := <-w.Events:
139 | path := "." + string(os.PathSeparator) + e.Name
140 | if !ignored(path, foundryConf.Ignore) {
141 | logger.Fdebugln("Watcher event", e.Name)
142 | _ = prompt.ShowLoading()
143 | files.Upload(connectionClient, foundryConf.CurrentDir, foundryConf.ServiceAccPath, promptNotifCh, foundryConf.Ignore...)
144 | }
145 | case err := <-w.Errors:
146 | logger.FdebuglnFatal("File watcher error", err)
147 | logger.FatalLogln("File watcher error", err)
148 | }
149 | }
150 | }()
151 |
152 | // Don't wait for the first save event to send the code.
153 | // Send it as soon as user calls 'foundry go'
154 | initialUploadCh <- struct{}{}
155 |
156 | <-done
157 | }
158 |
159 | func ignored(s string, globs []glob.Glob) bool {
160 | logger.Fdebugln("string to match:", s)
161 | for _, g := range globs {
162 | logger.Fdebugln("\t- glob:", g)
163 | logger.Fdebugln("\t- match:", g.Match(s))
164 | if g.Match(s) {
165 | return true
166 | }
167 | }
168 | return false
169 | }
170 |
171 | func listenCallback(data []byte, err error) {
172 | logger.Fdebugln(string(data))
173 |
174 | if err != nil {
175 | logger.FdebuglnFatal("WebSocket error", err)
176 | logger.FatalLogln("WebSocket error", err)
177 | }
178 |
179 | t := connMsg.ResponseMsgType{}
180 | if err := json.Unmarshal(data, &t); err != nil {
181 | logger.FdebuglnFatal("Unmarshaling response error", err)
182 | logger.FatalLogln("Parsing server JSON response error", err)
183 | }
184 |
185 | _ = prompt.HideLoading()
186 | switch t.Type {
187 | case connMsg.LogResponseMsg:
188 | var s struct{ Content connMsg.LogContent }
189 |
190 | if err := json.Unmarshal(data, &s); err != nil {
191 | logger.FdebuglnFatal("Unmarshaling response error", err)
192 | logger.FatalLogln("Parsing server log message error", err)
193 | }
194 |
195 | if _, err := prompt.Writeln(s.Content.Msg); err != nil {
196 | logger.FdebuglnFatal("Error writing output", err)
197 | logger.FatalLogln("Error writing output", err)
198 | }
199 |
200 | case connMsg.WatchResponseMsg:
201 | var s struct{ Content connMsg.WatchContent }
202 |
203 | if err := json.Unmarshal(data, &s); err != nil {
204 | logger.FdebuglnFatal("Unmarshaling response error", err)
205 | logger.FatalLogln("Parsing server wathc message error", err)
206 | }
207 |
208 | var info string
209 | if s.Content.RunAll {
210 | info = "All filters disabled. Will display output from all functions."
211 | } else {
212 | info = fmt.Sprintf("Displaying output from: %s.", strings.Join(s.Content.Run, ", "))
213 | }
214 |
215 | prompt.SetInfoln(info, p.InfoLineSeverityWarning)
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/auth/auth.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "strings"
11 | "time"
12 |
13 | "foundry/cli/logger"
14 | )
15 |
16 | const (
17 | apiKey = "AIzaSyAqL--IsyZd3cQTUgXR3KRWZZN-M6jR1kE"
18 | idTokenKey = "FOUNDRY_AUTH_ID_TOKEN"
19 | refreshTokenKey = "FOUNDRY_AUTH_REFRESH_TOKEN"
20 | authStateKey = "FOUNDRY_AUTH_STATE"
21 | )
22 |
23 | type AuthStateType int
24 |
25 | // WARNING: It's important the order doesn't change because the AuthState field on Auth struct
26 | // is serialized in the config file.
27 | // Changing the order of the following consts would cause that serialized values would have
28 | // a different logical meaning
29 | const (
30 | AuthStateTypeSignedOut AuthStateType = iota + 1 // +1 so the first const's value is different from zero int value
31 | AuthStateTypeSignedIn
32 | AuthStateTypeSignedInAnonymous
33 | )
34 |
35 | type AuthError struct {
36 | Message string `json:"message"`
37 | StatusCode int `json:"code"`
38 | }
39 |
40 | func (ae *AuthError) Error() string {
41 | return fmt.Sprintf("[%v] %v\n", ae.StatusCode, ae.Message)
42 | }
43 |
44 | // TODO: Find out if we can serialize structures using viper
45 | type Auth struct {
46 | Error *AuthError `json:"error"`
47 | UserID string `json:"localId"`
48 | Email string `json:"email"`
49 | IDToken string `json:"idToken"`
50 | RefreshToken string `json:"refreshToken"`
51 |
52 | ExpiresIn string `json:"expiresIn"`
53 | originDate time.Time
54 |
55 | AuthState AuthStateType
56 |
57 | DisplayName string `json:"displayName"`
58 | }
59 |
60 | func New() (*Auth, error) {
61 | a := &Auth{
62 | AuthState: AuthStateTypeSignedOut,
63 | }
64 | if err := a.loadTokensAndState(); err != nil {
65 | return nil, err
66 | }
67 | return a, nil
68 | }
69 |
70 | func (a *Auth) SignUp(email, pass string) error {
71 | baseURL := "https://identitytoolkit.googleapis.com/v1"
72 | var endpoint string
73 | var reqBody interface{}
74 |
75 | if a.AuthState == AuthStateTypeSignedInAnonymous {
76 | // Check if auth state is AuthStateTypeSignedInAnonymous
77 | // If so, link the anonymous user with email, password, and IDToken
78 | logger.Fdebugln("Signing up an anonymous user (= linking email + pass)")
79 | endpoint = fmt.Sprintf("accounts:update?key=%v", apiKey)
80 | reqBody = struct {
81 | IDToken string `json:"idToken"`
82 | Email string `json:"email"`
83 | Password string `json:"password"`
84 | ReturnSecureToken bool `json:"returnSecureToken"`
85 | }{a.IDToken, email, pass, true}
86 | } else {
87 | logger.Fdebugln("Signing up a new user")
88 | endpoint = fmt.Sprintf("accounts:signUp?key=%v", apiKey)
89 | reqBody = struct {
90 | Email string `json:"email"`
91 | Password string `json:"password"`
92 | ReturnSecureToken bool `json:"returnSecureToken"`
93 | }{email, pass, true}
94 | }
95 |
96 | url := fmt.Sprintf("%v/%v", baseURL, endpoint)
97 |
98 | if err := a.doAuthReq(url, reqBody); err != nil {
99 | return err
100 | }
101 |
102 | if a.Error != nil {
103 | return nil
104 | }
105 |
106 | oldState := a.AuthState
107 | a.AuthState = AuthStateTypeSignedIn
108 | if err := a.saveTokensAndState(); err != nil {
109 | a.AuthState = oldState
110 | return err
111 | }
112 | return nil
113 | }
114 |
115 | func (a *Auth) SignUpAnonymously() error {
116 | reqBody := struct {
117 | ReturnSecureToken bool `json:"returnSecureToken"`
118 | }{true}
119 |
120 | baseURL := "https://identitytoolkit.googleapis.com/v1"
121 | endpoint := fmt.Sprintf("accounts:signUp?key=%v", apiKey)
122 | url := fmt.Sprintf("%v/%v", baseURL, endpoint)
123 |
124 | if err := a.doAuthReq(url, reqBody); err != nil {
125 | return err
126 | }
127 |
128 | if a.Error != nil {
129 | return nil
130 | }
131 |
132 | oldState := a.AuthState
133 | a.AuthState = AuthStateTypeSignedInAnonymous
134 | if err := a.saveTokensAndState(); err != nil {
135 | a.AuthState = oldState
136 | return err
137 | }
138 | return nil
139 | }
140 |
141 | func (a *Auth) SignIn(email, pass string) error {
142 | reqBody := struct {
143 | Email string `json:"email"`
144 | Password string `json:"password"`
145 | ReturnSecureToken bool `json:"returnSecureToken"`
146 | }{email, pass, true}
147 |
148 | baseURL := "https://identitytoolkit.googleapis.com/v1"
149 | endpoint := fmt.Sprintf("accounts:signInWithPassword?key=%v", apiKey)
150 | url := fmt.Sprintf("%v/%v", baseURL, endpoint)
151 |
152 | if err := a.doAuthReq(url, reqBody); err != nil {
153 | return err
154 | }
155 |
156 | if a.Error != nil {
157 | return nil
158 | }
159 |
160 | oldState := a.AuthState
161 | a.AuthState = AuthStateTypeSignedIn
162 | if err := a.saveTokensAndState(); err != nil {
163 | a.AuthState = oldState
164 | return err
165 | }
166 |
167 | return nil
168 | }
169 |
170 | func (a *Auth) SignOut() error {
171 | a.Error = nil
172 | a.UserID = ""
173 | a.Email = ""
174 | a.IDToken = ""
175 | a.RefreshToken = ""
176 | a.ExpiresIn = "0"
177 | a.AuthState = AuthStateTypeSignedOut
178 | return a.clearTokensAndState()
179 | }
180 |
181 | func (a *Auth) doAuthReq(url string, body interface{}) error {
182 | a.Error = nil
183 |
184 | jBody, err := json.Marshal(body)
185 | if err != nil {
186 | return err
187 | }
188 |
189 | res, err := http.Post(url, "application/json", bytes.NewBuffer(jBody))
190 | if err != nil {
191 | return err
192 | }
193 | defer res.Body.Close()
194 |
195 | bodyBytes, err := ioutil.ReadAll(res.Body)
196 | if err != nil {
197 | return err
198 | }
199 |
200 | err = json.Unmarshal(bodyBytes, a)
201 | if err != nil {
202 | return err
203 | }
204 |
205 | if a.Error != nil {
206 | return nil
207 | }
208 |
209 | // Save the time when we originaly acquired the ID token
210 | // for checking whether we need to refresh it
211 | a.originDate = time.Now()
212 |
213 | return nil
214 | }
215 |
216 | func (a *Auth) doRefreshReq() error {
217 | logger.Fdebugln("Refreshing ID token")
218 |
219 | u := fmt.Sprintf("https://securetoken.googleapis.com/v1/token?key=%v", apiKey)
220 | data := url.Values{}
221 | data.Set("refresh_token", a.RefreshToken)
222 | data.Set("grant_type", "refresh_token")
223 |
224 | req, err := http.NewRequest("POST", u, strings.NewReader(data.Encode()))
225 | if err != nil {
226 | return err
227 | }
228 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
229 |
230 | client := &http.Client{Timeout: time.Second * 30}
231 | res, err := client.Do(req)
232 | if err != nil {
233 | return err
234 | }
235 | defer res.Body.Close()
236 |
237 | bodyBytes, err := ioutil.ReadAll(res.Body)
238 | if err != nil {
239 | return err
240 | }
241 |
242 | // Sigh... Firebase has different keys in the response payload
243 | // for token refresh flow from the response payload in a sign
244 | // in flow. Also, its content-type isn't application/json but
245 | // application/x-www-form-urlencoded.
246 | var j struct {
247 | ExpiresIn string `json:"expires_in"`
248 | RefreshToken string `json:"refresh_token"`
249 | IDToken string `json:"id_token"`
250 | }
251 |
252 | err = json.Unmarshal(bodyBytes, &j)
253 | if err != nil {
254 | return err
255 | }
256 |
257 | a.ExpiresIn = j.ExpiresIn
258 | a.IDToken = j.IDToken
259 | a.RefreshToken = j.RefreshToken
260 | a.originDate = time.Now()
261 |
262 | // TODO: error checking
263 | // TOKEN_EXPIRED: The user's credential is no longer valid. The user must sign in again.
264 | // USER_DISABLED: The user account has been disabled by an administrator.
265 | // USER_NOT_FOUND: The user corresponding to the refresh token was not found. It is likely the user was deleted.
266 | // API key not valid. Please pass a valid API key. (invalid API key provided)
267 | // INVALID_REFRESH_TOKEN: An invalid refresh token is provided.
268 | // Invalid JSON payload received. Unknown name \"refresh_tokens\": Cannot bind query parameter. Field 'refresh_tokens' could not be found in request message.
269 | // INVALID_GRANT_TYPE: the grant type specified is invalid.
270 | // MISSING_REFRESH_TOKEN: no refresh token provided.
271 |
272 | return nil
273 | }
274 |
--------------------------------------------------------------------------------
/prompt/prompt.txt:
--------------------------------------------------------------------------------
1 | // package prompt
2 |
3 | // import (
4 | // "bytes"
5 | // "fmt"
6 |
7 | // "io"
8 | // "os"
9 | // "os/signal"
10 | // "strings"
11 | // "sync"
12 | // "syscall"
13 | // "time"
14 |
15 | // "foundry/cli/logger"
16 | // "foundry/cli/prompt/cmd"
17 |
18 | // goprompt "github.com/mlejva/go-prompt"
19 | // )
20 |
21 | // // type CmdRunFunc func(args []string) error
22 |
23 | // // type Cmd struct {
24 | // // Text string
25 | // // Desc string
26 | // // Do CmdRunFunc
27 | // // }
28 |
29 | // // func (c *Cmd) String() string {
30 | // // return fmt.Sprintf("%s - %s\n", c.Text, c.Desc)
31 | // // }
32 |
33 | // // func (c *Cmd) ToSuggest() goprompt.Suggest {
34 | // // return goprompt.Suggest{Text: c.Text, Description: c.Desc}
35 | // // }
36 |
37 | // type Prompt struct {
38 | // cmds []*cmd.Cmd
39 | // // TODO: vars should be here? At least writer
40 |
41 | // // buf *bytes.Buffer
42 | // buffer bytes.Buffer
43 | // mutex sync.Mutex
44 | // }
45 |
46 | // var (
47 | // promptPrefix = "> "
48 |
49 | // promptText = ""
50 | // promptRow = 0
51 |
52 | // errorText = ""
53 | // errorRow = 0
54 |
55 | // totalRows = 0
56 | // freeRows = 0
57 |
58 | // parser = goprompt.NewStandardInputParser()
59 | // writer = goprompt.NewStandardOutputWriter()
60 |
61 | // // waitDuration = time.Millisecond * 400
62 | // waitDuration = time.Millisecond * 10
63 | // )
64 |
65 | // func NewPrompt(cmds []*cmd.Cmd) *Prompt {
66 | // return &Prompt{cmds: cmds, buffer: bytes.Buffer{}}
67 | // }
68 |
69 | // // func (p *Prompt) WriteToBuffer(s string) error {
70 | // // _, err := p.buf.Write([]byte(s))
71 | // // return err
72 | // // }
73 |
74 | // // func (p *Prompt) watchBuffer() {
75 | // // for {
76 | // // // logger.Fdebugln("Watch Buffer")
77 |
78 | // // b := make([]byte, 1024)
79 | // // if n, err := p.buf.Read(b); err == nil && n > 0 {
80 | // // p.Print(string(b))
81 | // // // logger.Fdebugln("BUFFER:", string(b))
82 | // // } else if err != nil && err != io.EOF {
83 | // // logger.FdebuglnFatal(err)
84 | // // logger.FatalLogln(err)
85 | // // }
86 |
87 | // // // time.Sleep(time.Millisecond * 10)
88 | // // }
89 | // // }
90 |
91 | // // Write appends the contents of p to the buffer, growing the buffer as needed. It returns
92 | // // the number of bytes written.
93 | // func (p *Prompt) Write(b []byte) (n int, err error) {
94 | // p.mutex.Lock()
95 | // defer p.mutex.Unlock()
96 | // return p.buffer.Write(b)
97 | // }
98 |
99 | // func (p *Prompt) read(b []byte) (n int, err error) {
100 | // p.mutex.Lock()
101 | // defer p.mutex.Unlock()
102 | // return p.buffer.Read(b)
103 | // }
104 |
105 | // func (p *Prompt) print2() {
106 | // for {
107 | // b := make([]byte, 1024)
108 | // if n, err := p.read(b); err == nil && n > 0 {
109 | // p.Print(string(b[:n]))
110 | // } else if err != nil && err != io.EOF {
111 | // logger.Fdebugln(err)
112 | // logger.FatalLogln(err)
113 | // }
114 | // }
115 | // }
116 |
117 | // func (p *Prompt) Print(s string) {
118 | // p.mutex.Lock()
119 | // defer p.mutex.Unlock()
120 | // logger.Fdebugln("[print] totalRows:", totalRows)
121 | // logger.Fdebugln("[print] promptRow:", promptRow)
122 | // logger.Fdebugln("[print] errorRow:", errorRow)
123 |
124 | // logger.Fdebugln("[print] raw:", s)
125 | // trimmed := strings.TrimSpace(s)
126 | // logger.Fdebugln("[print] trimmed:", trimmed)
127 | // lines := strings.Split(trimmed, "\n")
128 | // logger.Fdebugln("[print] totalLines:", len(lines))
129 |
130 | // for _, l := range lines {
131 | // logger.Fdebugln("[prompt] freeRows start:", freeRows)
132 | // logger.Fdebugln("[prompt] line:", l)
133 |
134 | // freeRows--
135 |
136 | // // p.wGoToAndErasePrompt()
137 | // // writer.Flush()
138 |
139 | // writer.UnSaveCursor()
140 | // writer.Flush()
141 |
142 | // // t := fmt.Sprintf("[%v]%s\n", ix, l)
143 | // // writer.WriteRawStr(t)
144 | // writer.WriteRawStr(l + "\n")
145 | // writer.Flush()
146 |
147 | // writer.SaveCursor()
148 | // writer.Flush()
149 |
150 | // if freeRows <= 3 {
151 | // newRows := 4 - freeRows
152 | // logger.Fdebugln("[prompt] newRows:", newRows)
153 |
154 | // // p.wGoToAndEraseError()
155 | // // writer.CursorGoTo(errorRow, 0)
156 | // // writer.Flush()
157 | // // writer.EraseLine()
158 | // // writer.Flush()
159 |
160 | // p.wGoToAndErasePrompt()
161 | // writer.Flush()
162 | // // writer.CursorGoTo(promptRow, 0)
163 | // // writer.Flush()
164 | // // writer.EraseLine()
165 | // // writer.Flush()
166 | // // time.Sleep(waitDuration)
167 | // for i := 0; i < newRows; i++ {
168 | // writer.WriteRawStr("\n")
169 | // writer.Flush()
170 | // }
171 | // // writer.WriteRawStr(strings.Repeat("\n", newRows))
172 | // // writer.Flush()
173 |
174 | // freeRows += newRows
175 |
176 | // writer.UnSaveCursor()
177 | // writer.Flush()
178 | // // time.Sleep(waitDuration)
179 |
180 | // writer.CursorUp(newRows)
181 | // writer.Flush()
182 | // // if newRows > 0 {
183 | // // }
184 | // writer.SaveCursor()
185 | // writer.Flush()
186 | // // time.Sleep(waitDuration)
187 | // }
188 |
189 | // logger.Fdebugln("[prompt] freeRows end:", freeRows)
190 | // }
191 |
192 | // p.wGoToAndRestoreError()
193 | // // writer.CursorGoTo(errorRow, 0)
194 | // writer.Flush()
195 | // // writer.WriteRawStr(errorText)
196 | // // writer.Flush()
197 |
198 | // p.wGoToAndRestorePrompt()
199 | // // writer.CursorGoTo(promptRow, 0)
200 | // // writer.Flush()
201 | // // writer.WriteRawStr(promptPrefix + promptText)
202 | // // writer.Flush()
203 |
204 | // writer.Flush()
205 | // }
206 |
207 | // func (p *Prompt) PrintInfo(s string) {
208 | // p.wGoToAndEraseError()
209 |
210 | // writer.SetColor(goprompt.Green, goprompt.DefaultColor, true)
211 | // writer.WriteStr(s)
212 | // writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, true)
213 | // writer.Flush()
214 |
215 | // p.wGoToPrompt()
216 | // }
217 |
218 | // func (p *Prompt) SetPromptPrefix(s string) {
219 | // promptPrefix = s
220 | // }
221 |
222 | // func (p *Prompt) Run() {
223 | // size := parser.GetWinSize()
224 |
225 | // // Watch for terminal size changes
226 | // sigwinch := make(chan os.Signal, 1)
227 | // defer close(sigwinch)
228 | // signal.Notify(sigwinch, syscall.SIGWINCH)
229 | // go func() {
230 | // for {
231 | // if _, ok := <-sigwinch; !ok {
232 | // return
233 | // }
234 | // size = parser.GetWinSize()
235 | // logger.Fdebugln("Terminal size change:", size)
236 | // p.rerender(size)
237 | // }
238 | // }()
239 |
240 | // p.rerender(size)
241 |
242 | // interupOpt := goprompt.OptionAddKeyBind(goprompt.KeyBind{
243 | // Key: goprompt.ControlC,
244 | // Fn: func(buf *goprompt.Buffer) {
245 | // os.Exit(0)
246 | // },
247 | // })
248 | // prefixOpt := goprompt.OptionPrefix(promptPrefix)
249 | // livePrefixOpt := goprompt.OptionLivePrefix(func() (prefix string, useLivePrefix bool) {
250 | // return promptPrefix, true
251 | // })
252 |
253 | // newp := goprompt.New(p.executor, p.completer, interupOpt, prefixOpt, livePrefixOpt)
254 |
255 | // // go p.watchBuffer()
256 |
257 | // go p.print2()
258 |
259 | // newp.Run()
260 | // }
261 |
262 | // func (p *Prompt) completer(d goprompt.Document) []goprompt.Suggest {
263 | // promptText = d.CurrentLine()
264 |
265 | // s := []goprompt.Suggest{}
266 | // for _, c := range p.cmds {
267 | // s = append(s, c.ToSuggest())
268 | // }
269 |
270 | // return []goprompt.Suggest{}
271 | // // return goprompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
272 | // }
273 |
274 | // func (p *Prompt) executor(s string) {
275 | // if s == "" {
276 | // return
277 | // }
278 |
279 | // fields := strings.Fields(s)
280 |
281 | // if cmd := p.getCommand(fields[0]); cmd != nil {
282 | // args := fields[1:]
283 |
284 | // if err := cmd.Do(args); err != nil {
285 | // logger.FdebuglnFatal(err)
286 | // logger.FatalLogln(err)
287 | // }
288 | // } else {
289 | // p.wGoToAndEraseError()
290 |
291 | // writer.SetColor(goprompt.Red, goprompt.DefaultColor, true)
292 | // // errorText = fmt.Sprintf("Unknown command '%s'. Write 'help' to list available commands.\n", fields[0])
293 | // errorText = fmt.Sprintf("Unknown command '%s'", fields[0])
294 | // writer.WriteStr(errorText)
295 | // writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, false)
296 | // writer.Flush()
297 |
298 | // p.wGoToPrompt()
299 | // }
300 | // }
301 |
302 | // func (p *Prompt) getCommand(s string) *cmd.Cmd {
303 | // for _, c := range p.cmds {
304 | // if c.Text == s {
305 | // return c
306 | // }
307 | // }
308 | // return nil
309 | // }
310 |
311 | // func (p *Prompt) rerender(size *goprompt.WinSize) {
312 | // p.mutex.Lock()
313 | // defer p.mutex.Unlock()
314 | // totalRows = int(size.Row)
315 | // promptRow = totalRows
316 | // errorRow = promptRow - 1
317 | // freeRows = totalRows
318 |
319 | // // So the initial UnSave is at 0,0
320 | // writer.CursorGoTo(0, 0)
321 | // writer.Flush()
322 | // writer.SaveCursor()
323 | // writer.Flush()
324 |
325 | // // Clears the screen and moves cursor to promptRow
326 | // p.wReset()
327 |
328 | // // Restore prompt + error
329 | // p.wGoToAndRestoreError()
330 | // p.wGoToAndRestorePrompt()
331 |
332 | // logger.Fdebugln("totalRows:", totalRows)
333 | // logger.Fdebugln("promptRow:", promptRow)
334 | // logger.Fdebugln("errorRow:", errorRow)
335 | // logger.Fdebugln("freeRows:", freeRows)
336 | // }
337 |
338 | // func (p *Prompt) wReset() {
339 | // p.mutex.Lock()
340 | // defer p.mutex.Unlock()
341 | // writer.EraseScreen()
342 | // writer.CursorGoTo(promptRow, 0)
343 | // writer.Flush()
344 | // }
345 |
346 | // func (p *Prompt) wGoToPrompt() {
347 | // p.mutex.Lock()
348 | // defer p.mutex.Unlock()
349 | // writer.CursorGoTo(promptRow, 0)
350 | // writer.Flush()
351 | // }
352 |
353 | // func (p *Prompt) wGoToError() {
354 | // p.mutex.Lock()
355 | // defer p.mutex.Unlock()
356 | // writer.CursorGoTo(errorRow, 0)
357 | // writer.Flush()
358 | // }
359 |
360 | // func (p *Prompt) wGoToAndErasePrompt() {
361 | // p.wGoToPrompt()
362 | // writer.EraseLine()
363 | // writer.Flush()
364 | // }
365 |
366 | // func (p *Prompt) wGoToAndEraseError() {
367 | // p.wGoToError()
368 | // writer.EraseLine()
369 | // writer.Flush()
370 | // }
371 |
372 | // func (p *Prompt) wGoToAndRestorePrompt() {
373 | // p.wGoToPrompt()
374 | // writer.SetColor(goprompt.Blue, goprompt.DefaultColor, false)
375 | // writer.WriteRawStr(promptPrefix)
376 | // writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, false)
377 | // writer.WriteRawStr(promptText)
378 | // writer.Flush()
379 | // }
380 |
381 | // func (p *Prompt) wGoToAndRestoreError() {
382 | // p.wGoToError()
383 |
384 | // writer.SetColor(goprompt.Red, goprompt.DefaultColor, true)
385 | // writer.WriteRawStr(errorText)
386 | // writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, false)
387 |
388 | // // writer.Flush()
389 | // }
390 |
--------------------------------------------------------------------------------
/prompt/prompt.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "fmt"
5 | "foundry/cli/logger"
6 | "os"
7 | "os/signal"
8 | "strings"
9 | "sync"
10 | "syscall"
11 | "time"
12 |
13 | "foundry/cli/prompt/cmd"
14 |
15 | goprompt "github.com/mlejva/go-prompt"
16 | )
17 |
18 | type CursorPos struct {
19 | Row int
20 | Col int
21 | }
22 |
23 | func CursorOutputStart() CursorPos {
24 | return CursorPos{1, 1}
25 | }
26 |
27 | type PromptEventType string
28 |
29 | type PromptEvent struct {
30 | Type PromptEventType
31 | }
32 | type Prompt struct {
33 | cmds []cmd.Cmd
34 |
35 | outBuf *Buffer
36 | // outBufMutex sync.Mutex
37 |
38 | renderMutex sync.Mutex
39 |
40 | promptPrefix string
41 | promptText string
42 | promptRow int // Will be recalculated once the terminal is ready
43 |
44 | infoText string
45 | infoRow int // Will be recalculated once the terminal is ready
46 |
47 | totalColumns int // Will be recalculated once the terminal is ready
48 | totalRows int // Will be recalculated once the terminal is ready
49 | freeRows int // Will be recalculated once the terminal is ready
50 |
51 | parser *goprompt.PosixParser
52 | writer goprompt.ConsoleWriter
53 |
54 | savedPos CursorPos
55 | currentPos CursorPos // Current position of the cursor when printing output
56 |
57 | lastEscapeCode string // Last VT100 terminal escape code that should be applied next time the print() method is called
58 |
59 | printing bool
60 |
61 | Events chan PromptEvent
62 | }
63 |
64 | type InfoLineSeverity int
65 |
66 | const (
67 | PromptEventTypeRerender PromptEventType = "rerender"
68 |
69 | InfoLineSeverityNormal InfoLineSeverity = iota
70 | InfoLineSeverityWarning
71 | InfoLineSeverityError
72 | )
73 |
74 | //////////////////////
75 |
76 | func (p *Prompt) completer(d goprompt.Document) []goprompt.Suggest {
77 | p.renderMutex.Lock()
78 | p.promptText = d.CurrentLine()
79 | p.renderMutex.Unlock()
80 |
81 | return []goprompt.Suggest{}
82 | }
83 |
84 | func (p *Prompt) executor(s string) {
85 | if s == "" {
86 | return
87 | }
88 | logger.Fdebugln("Executor:", s)
89 |
90 | fields := strings.Fields(s)
91 |
92 | if cmd := p.getCommand(fields[0]); cmd != nil {
93 | logger.Fdebugln("cmd:", cmd)
94 | args := fields[1:]
95 | logger.Fdebugln("args:", args)
96 | cmd.RunRequest(args)
97 | } else {
98 | // Delete an old info message and show the new one
99 |
100 | p.renderMutex.Lock()
101 |
102 | // Delete an old info message
103 | p.writer.CursorGoTo(p.infoRow, 1)
104 | p.writer.EraseLine()
105 |
106 | // Print the new info message
107 | p.writer.SetColor(goprompt.Red, goprompt.DefaultColor, true)
108 | p.infoText = fmt.Sprintf("Unknown command '%s'", fields[0])
109 | p.writer.WriteRawStr(p.infoText)
110 | p.writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, false)
111 |
112 | // Move cursor back to the prompt
113 | p.writer.CursorGoTo(p.promptRow, len(p.promptPrefix)+len(p.promptText)+1)
114 |
115 | if err := p.writer.Flush(); err != nil {
116 | logger.FdebuglnFatal("Error flushing prompt buffer", err)
117 | logger.FatalLogln("Error flushing prompt buffer", err)
118 | }
119 |
120 | p.renderMutex.Unlock()
121 | }
122 | }
123 |
124 | func (p *Prompt) getCommand(s string) cmd.Cmd {
125 | for _, c := range p.cmds {
126 | if c.Name() == s {
127 | return c
128 | }
129 | }
130 | return nil
131 | }
132 |
133 | /////////////
134 |
135 | func NewPrompt(cmds []cmd.Cmd) *Prompt {
136 | prefix := "> "
137 | return &Prompt{
138 | cmds: cmds,
139 |
140 | outBuf: NewBuffer(),
141 |
142 | promptPrefix: prefix,
143 |
144 | parser: goprompt.NewStandardInputParser(),
145 | writer: goprompt.NewStandardOutputWriter(),
146 |
147 | // Terminal is indexed from 1
148 | savedPos: CursorOutputStart(),
149 | currentPos: CursorPos{1, len(prefix) + 1},
150 |
151 | Events: make(chan PromptEvent),
152 | }
153 | }
154 |
155 | func (p *Prompt) Run() {
156 | // Read buffer and print anything that gets send to the channel
157 | bufCh := make(chan []byte, 128)
158 | stopReadCh := make(chan struct{})
159 | go p.outBuf.Read(bufCh, stopReadCh)
160 | go func() {
161 | for {
162 | select {
163 | case b := <-bufCh:
164 | p.print(b)
165 | default:
166 | time.Sleep(time.Millisecond * 10)
167 | }
168 | }
169 | }()
170 |
171 | interupOpt := goprompt.OptionAddKeyBind(goprompt.KeyBind{
172 | Key: goprompt.ControlC,
173 | Fn: func(buf *goprompt.Buffer) {
174 | os.Exit(0)
175 | },
176 | })
177 | prefixOpt := goprompt.OptionPrefix(p.promptPrefix)
178 | prefixColOpt := goprompt.OptionPrefixTextColor(goprompt.Green)
179 | prompt := goprompt.New(p.executor, p.completer, interupOpt, prefixOpt, prefixColOpt)
180 | go prompt.Run()
181 |
182 | // The initial rerender for the current terminal size
183 | if err := p.rerender(true); err != nil {
184 | logger.Fdebugln("Error during the initial rerender", err)
185 | logger.FatalLogln("Error during the initial rerender", err)
186 | }
187 |
188 | // Rerender a terminal for every size change
189 | go p.rerenderOnTermSizeChange()
190 | }
191 |
192 | func (p *Prompt) Writeln(s string) (n int, err error) {
193 | return p.outBuf.Write([]byte(s))
194 | }
195 |
196 | func (p *Prompt) SetInfoln(s string, severity InfoLineSeverity) error {
197 | p.renderMutex.Lock()
198 | defer p.renderMutex.Unlock()
199 |
200 | p.writer.CursorGoTo(p.infoRow, 1)
201 | p.writer.EraseLine()
202 |
203 | red := "\x1b[31m"
204 | yellow := "\x1b[33m"
205 | bold := "\x1b[1m"
206 | endSeq := "\x1b[0m"
207 | // resetColor := "\x1b[39m"
208 | var prefix string
209 | switch severity {
210 | case InfoLineSeverityNormal:
211 | // prefix = fmt.Sprintf("%s", endSeq)
212 | prefix = ""
213 | case InfoLineSeverityWarning:
214 | prefix = fmt.Sprintf("%s%sWARNING:%s ", bold, yellow, endSeq)
215 | case InfoLineSeverityError:
216 | prefix = fmt.Sprintf("%s%sERROR:%s ", bold, red, endSeq)
217 | default:
218 | prefix = ""
219 | }
220 |
221 | // p.writer.SetColor(goprompt.Green, goprompt.DefaultColor, true)
222 | t := strings.TrimSpace(s)
223 | info := fmt.Sprintf("%s%s", prefix, t)
224 | logger.Fdebugln("Info line text:", info)
225 | p.infoText = info
226 |
227 | p.writer.WriteRawStr(info)
228 | p.writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, true)
229 |
230 | p.writer.CursorGoTo(p.promptRow, len(p.promptPrefix)+len(p.promptText)+1)
231 |
232 | return p.writer.Flush()
233 | }
234 |
235 | func (p *Prompt) ShowLoading() error {
236 | p.renderMutex.Lock()
237 | defer p.renderMutex.Unlock()
238 |
239 | p.writer.CursorGoTo(p.infoRow, 1)
240 | p.writer.EraseLine()
241 |
242 | p.writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, true)
243 | msg := "Loading..."
244 | p.writer.WriteRawStr(msg)
245 | p.infoText = msg
246 |
247 | if p.printing {
248 | // Was in the middle of printing out the Autorun output
249 | p.writer.CursorGoTo(p.currentPos.Row, p.currentPos.Col)
250 | } else {
251 | p.writer.CursorGoTo(p.promptRow, len(p.promptPrefix)+len(p.promptText)+1)
252 | }
253 |
254 | p.writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, false)
255 | return p.writer.Flush()
256 | }
257 |
258 | func (p *Prompt) HideLoading() error {
259 | p.renderMutex.Lock()
260 | defer p.renderMutex.Unlock()
261 |
262 | if p.infoText != "Loading..." {
263 | return nil
264 | }
265 |
266 | p.writer.CursorGoTo(p.infoRow, 1)
267 | p.writer.EraseLine()
268 | p.infoText = ""
269 |
270 | if p.printing {
271 | // Was in the middle of printing out the autorun output
272 | p.writer.CursorGoTo(p.currentPos.Row, p.currentPos.Col)
273 | } else {
274 | p.writer.CursorGoTo(p.promptRow, len(p.promptPrefix)+len(p.promptText)+1)
275 | }
276 |
277 | return p.writer.Flush()
278 | }
279 |
280 | func (p *Prompt) rerender(initialRun bool) error {
281 | p.renderMutex.Lock()
282 | defer p.renderMutex.Unlock()
283 |
284 | size := p.parser.GetWinSize()
285 | if initialRun {
286 | p.moveWindowDown(int(size.Row))
287 | }
288 |
289 | p.writer.EraseScreen()
290 |
291 | p.currentPos = CursorOutputStart()
292 | p.savedPos = CursorOutputStart()
293 |
294 | p.totalRows = int(size.Row)
295 | p.totalColumns = int(size.Col)
296 | p.promptRow = p.totalRows
297 | p.infoRow = p.totalRows - 1
298 | p.freeRows = p.totalRows
299 |
300 | // Move to the info row and restore the text
301 | p.writer.CursorGoTo(p.infoRow, 1)
302 | p.writer.SetColor(goprompt.Red, goprompt.DefaultColor, true)
303 | p.writer.WriteRawStr(p.infoText)
304 |
305 | p.writer.CursorGoTo(p.promptRow, 1)
306 |
307 | if err := p.writer.Flush(); err != nil {
308 | return err
309 | }
310 |
311 | p.Events <- PromptEvent{PromptEventTypeRerender}
312 | return nil
313 | }
314 |
315 | // Prints # of rows of "\n" - this way the visible terminal window
316 | // is moved down and the previous user's terminal history isn't
317 | // erased on the initial rerender()
318 | func (p *Prompt) moveWindowDown(rows int) error {
319 | p.writer.CursorGoTo(rows, 0)
320 | p.writer.WriteRawStr(strings.Repeat("\n", rows))
321 | return p.writer.Flush()
322 | }
323 |
324 | func (p *Prompt) rerenderOnTermSizeChange() {
325 | sigwinchCh := make(chan os.Signal, 1)
326 | defer close(sigwinchCh)
327 | signal.Notify(sigwinchCh, syscall.SIGWINCH)
328 | for {
329 | if _, ok := <-sigwinchCh; !ok {
330 | return
331 | }
332 | if err := p.rerender(false); err != nil {
333 | logger.FdebuglnFatal("Error during the rerender", err)
334 | logger.FatalLogln("Error during the rerender", err)
335 | }
336 | }
337 | }
338 |
339 | func (p *Prompt) print(b []byte) {
340 | p.renderMutex.Lock()
341 | defer p.renderMutex.Unlock()
342 |
343 | p.printing = true
344 |
345 | // The invariant is that the the p.savedPos always holds
346 | // a position where we stopped printing the text = where
347 | // we should start printing text again.
348 | p.writer.CursorGoTo(p.savedPos.Row, p.savedPos.Col)
349 |
350 | s := string(b)
351 | // s = "\n====================\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \nsint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
352 | logger.Fdebugln(s)
353 |
354 | escapeStart := false
355 | for _, r := range s {
356 | p.writer.WriteRawStr(p.lastEscapeCode)
357 | p.writer.WriteRawStr(string(r))
358 |
359 | // Don't increase p.currentPos.Col while we are processing a terminal VT100 escape code
360 | if r == '\u001b' {
361 | // Reset the the last escape code
362 | p.lastEscapeCode = string('\u001b')
363 | escapeStart = true
364 | continue
365 | }
366 |
367 | if escapeStart {
368 | p.lastEscapeCode += string(r)
369 | // 'm' character signals that the escaped code is ending
370 | if r == 'm' {
371 | escapeStart = false
372 | continue
373 | } else {
374 | continue
375 | }
376 | }
377 |
378 | p.currentPos.Col++
379 |
380 | if r == '\n' {
381 | // On a new line, the cursor moves to the start of a line
382 | p.currentPos.Col = 1
383 |
384 | p.currentPos.Row++
385 | p.freeRows--
386 | }
387 |
388 | // TODO: Is this required?
389 | // This hardcoded solution makes it impossible to have resizable text
390 | // as you resize your terminal
391 | if p.currentPos.Col == p.totalColumns {
392 | // Make a new line
393 | p.writer.WriteRawStr("\n")
394 | p.currentPos.Col = 1
395 | p.currentPos.Row++
396 | p.freeRows--
397 | }
398 |
399 | if p.freeRows == 2 {
400 | p.savedPos = p.currentPos
401 | // Go to a prompt row and create a new line so that we
402 | // once again have 3 free rows.
403 | // The reason we have to go to the prompt row is becauase
404 | // if we had printed a new line anywhere before the prompt
405 | // row, the cursor would simply move down without actually
406 | // creating a new line in the terminal.
407 |
408 | // Erase the info row and prompt row so that a text doesn't stay there
409 | // when the everything is moved up by 1 row
410 | p.writer.CursorGoTo(p.infoRow, 1)
411 | p.writer.EraseLine()
412 | p.writer.CursorGoTo(p.promptRow, 1)
413 | p.writer.EraseLine()
414 |
415 | // Create a new line
416 | p.writer.WriteRawStr("\n")
417 |
418 | // Move cursor back to a position where we stopped outputting
419 | // text. This will be next available new line after the last
420 | // line of printed text
421 | p.writer.CursorGoTo(p.savedPos.Row, p.savedPos.Col)
422 | // The reason it's not sufficient to just go to p.savedPos
423 | // is because we printed a newline. All text moved 1 line up.
424 | p.writer.CursorUp(1)
425 |
426 | p.currentPos.Row--
427 | p.currentPos.Col = 1
428 | p.freeRows = 3
429 | }
430 | }
431 | p.savedPos = p.currentPos
432 |
433 | // Move to the info row and restore the info text
434 | p.writer.CursorGoTo(p.infoRow, 1)
435 | p.writer.SetColor(goprompt.Red, goprompt.DefaultColor, true)
436 | p.writer.WriteRawStr(p.infoText)
437 |
438 | // Move to the prompt row and restore the text
439 | p.writer.CursorGoTo(p.promptRow, 1)
440 | p.writer.SetColor(goprompt.Green, goprompt.DefaultColor, false)
441 | p.writer.WriteRawStr(p.promptPrefix)
442 | p.writer.SetColor(goprompt.DefaultColor, goprompt.DefaultColor, false)
443 | p.writer.WriteRawStr(p.promptText)
444 |
445 | if err := p.writer.Flush(); err != nil {
446 | logger.FdebuglnFatal("Error flushing prompt buffer (2)", err)
447 | logger.FatalLogln("Error flushing prompt buffer", err)
448 | }
449 |
450 | p.printing = false
451 | }
452 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z35w/rc=
4 | github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA=
5 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
7 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
8 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
9 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
11 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
12 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
13 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
14 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
15 | github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
16 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
18 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
19 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
20 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
22 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
23 | github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
24 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
28 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
29 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
30 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
31 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
32 | github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
33 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
34 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
35 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
36 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
37 | github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
38 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
39 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
40 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
41 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
42 | github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
43 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
44 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
45 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
46 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
47 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
48 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
49 | github.com/golang/gddo v0.0.0-20200324184333-3c2cc9a6329d h1:ZJhGJay808i+klrJbox3i5NMVerJ3/tEhtOTeQpPwJQ=
50 | github.com/golang/gddo v0.0.0-20200324184333-3c2cc9a6329d/go.mod h1:sam69Hju0uq+5uvLJUMDlsKlQ21Vrs1Kd/1YFPNYdOU=
51 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
52 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
53 | github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
54 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
55 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
56 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
57 | github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
58 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
59 | github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
60 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
61 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
62 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
63 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
64 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
65 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
66 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
67 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
68 | github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
69 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
70 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
71 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
72 | github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
73 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
74 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
75 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
76 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
77 | github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80 h1:g/SJtZVYc1cxSB8lgrgqeOlIdi4MhqNNHYRAC8y+g4c=
78 | github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
79 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
80 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
81 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
82 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
83 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
84 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
85 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
86 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
87 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
88 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
89 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
90 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
91 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
92 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
93 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
94 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
95 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
96 | github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
97 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
98 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
99 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
100 | github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
101 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
102 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
103 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
104 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
105 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
106 | github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
107 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
108 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
109 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
110 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
111 | github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
112 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
113 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
114 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
115 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
116 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
117 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
118 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
119 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
120 | github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
121 | github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
122 | github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
123 | github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
124 | github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
125 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
126 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
127 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
128 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
129 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
130 | github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
131 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
132 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
133 | github.com/mlejva/go-prompt v0.2.4-0.20200408092807-6312c0dbbff2 h1:D0BoxsmjDXJSdPca53MsX+zkhcTSQt+PZdQ4TEdVhGQ=
134 | github.com/mlejva/go-prompt v0.2.4-0.20200408092807-6312c0dbbff2/go.mod h1:b2+gpEZpPbqsM2wD4HmMi9NSRloobNhuyaRAzMaJwT4=
135 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
136 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
137 | github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
138 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
139 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
140 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
141 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
142 | github.com/pkg/term v0.0.0-20180423043932-cda20d4ac917/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
143 | github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
144 | github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
145 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
146 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
147 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
148 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
149 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
150 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
151 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
152 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
153 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
154 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
155 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
156 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
157 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
158 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
159 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
160 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
161 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
162 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
163 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
164 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
165 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
166 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
167 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
168 | github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
169 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
170 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
171 | github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
172 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
173 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
174 | github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
175 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
176 | github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
177 | github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
178 | github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
179 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
180 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
181 | github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
182 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
183 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
184 | github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
185 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
186 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
187 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=
188 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
189 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
190 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
191 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
192 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
193 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
194 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
195 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
196 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
197 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
198 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
199 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
200 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
201 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
202 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
203 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
204 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
205 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
206 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
207 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
208 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
209 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
210 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
211 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
212 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
213 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
214 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
215 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
216 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
217 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
218 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
219 | golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
220 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
221 | golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
222 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
223 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
224 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
225 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
226 | golang.org/x/sys v0.0.0-20180620133508-ad87a3a340fa/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
227 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
228 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
229 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
230 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
231 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
232 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
233 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
234 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
235 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU=
236 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
237 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
238 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
239 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
240 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
241 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
242 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
243 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
244 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
245 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
246 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
247 | golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
248 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
249 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
250 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
251 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
252 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
253 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
254 | google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
255 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
256 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
257 | google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
258 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
259 | google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
260 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
261 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
262 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
263 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
264 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
265 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
266 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
267 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
268 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
269 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
270 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
271 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
272 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
273 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
274 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
275 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
276 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
277 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
278 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
279 |
--------------------------------------------------------------------------------