├── 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 | Foundry 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 | [![Watch the 5-min video explaining Foundry](https://firebasestorage.googleapis.com/v0/b/foundryapp.appspot.com/o/video-thumbnail.png?alt=media&token=a0273107-e55c-42a6-b6d2-bb24a1da722c)](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 | --------------------------------------------------------------------------------