5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 09:00:38 +08:00

Initial commit of the Setup command

This commit is contained in:
Lea Anthony 2018-12-16 18:28:18 +11:00
parent 5de8efff32
commit db550f81cd
11 changed files with 850 additions and 0 deletions

268
cmd/cli.go Normal file
View File

@ -0,0 +1,268 @@
package cmd
import (
"flag"
"fmt"
"os"
"strings"
)
// NewCli - Creates a new Cli application object
func NewCli(name, description string) *Cli {
result := &Cli{}
result.rootCommand = NewCommand(name, description, result, "")
result.log = NewLogger()
return result
}
// Cli - The main application object
type Cli struct {
rootCommand *Command
defaultCommand *Command
preRunCommand func(*Cli) error
log *Logger
}
// Version - Set the Application version string
func (c *Cli) Version(version string) {
c.rootCommand.AppVersion = version
}
// PrintHelp - Prints the application's help
func (c *Cli) PrintHelp() {
c.rootCommand.PrintHelp()
}
// Run - Runs the application with the given arguments
func (c *Cli) Run(args ...string) error {
if c.preRunCommand != nil {
err := c.preRunCommand(c)
if err != nil {
return err
}
}
if len(args) == 0 {
args = os.Args[1:]
}
return c.rootCommand.Run(args)
}
// DefaultCommand - Sets the given command as the command to run when
// no other commands given
func (c *Cli) DefaultCommand(defaultCommand *Command) *Cli {
c.defaultCommand = defaultCommand
return c
}
// Command - Adds a command to the application
func (c *Cli) Command(name, description string) *Command {
return c.rootCommand.Command(name, description)
}
// PreRun - Calls the given function before running the specific command
func (c *Cli) PreRun(callback func(*Cli) error) {
c.preRunCommand = callback
}
// BoolFlag - Adds a boolean flag to the root command
func (c *Cli) BoolFlag(name, description string, variable *bool) *Command {
c.rootCommand.BoolFlag(name, description, variable)
return c.rootCommand
}
// StringFlag - Adds a string flag to the root command
func (c *Cli) StringFlag(name, description string, variable *string) *Command {
c.rootCommand.StringFlag(name, description, variable)
return c.rootCommand
}
// Action represents a function that gets calls when the command is called by
// the user
type Action func() error
// Command represents a command that may be run by the user
type Command struct {
Name string
CommandPath string
Shortdescription string
Longdescription string
AppVersion string
SubCommands []*Command
SubCommandsMap map[string]*Command
longestSubcommand int
ActionCallback Action
App *Cli
Flags *flag.FlagSet
flagCount int
log *Logger
helpFlag bool
}
// NewCommand creates a new Command
func NewCommand(name string, description string, app *Cli, parentCommandPath string) *Command {
result := &Command{
Name: name,
Shortdescription: description,
SubCommandsMap: make(map[string]*Command),
App: app,
}
// Set up command path
if parentCommandPath != "" {
result.CommandPath += parentCommandPath + " "
}
result.CommandPath += name
// Set up flag set
result.Flags = flag.NewFlagSet(result.CommandPath, flag.ContinueOnError)
result.BoolFlag("help", "Get help on the '"+result.CommandPath+"' command.", &result.helpFlag)
// result.Flags.Usage = result.PrintHelp
return result
}
// parseFlags parses the given flags
func (c *Command) parseFlags(args []string) error {
// Parse flags
tmp := os.Stderr
os.Stderr = nil
err := c.Flags.Parse(args)
os.Stderr = tmp
if err != nil {
fmt.Printf("Error: %s\n\n", err.Error())
c.PrintHelp()
}
return err
}
// Run - Runs the Command with the given arguments
func (c *Command) Run(args []string) error {
// If we have arguments, process them
if len(args) > 0 {
// Check for subcommand
subcommand := c.SubCommandsMap[args[0]]
if subcommand != nil {
return subcommand.Run(args[1:])
}
// Parse flags
err := c.parseFlags(args)
if err != nil {
fmt.Printf("Error: %s\n\n", err.Error())
c.PrintHelp()
return err
}
// Help takes precedence
if c.helpFlag {
c.PrintHelp()
return nil
}
}
// Do we have an action?
if c.ActionCallback != nil {
return c.ActionCallback()
}
// If we haven't specified a subcommand
// check for an app level default command
if c.App.defaultCommand != nil {
// Prevent recursion!
if c.App.defaultCommand != c {
return c.App.defaultCommand.Run(args)
}
}
// Nothing left we can do
c.PrintHelp()
return nil
}
// Action - Define an action from this command
func (c *Command) Action(callback Action) *Command {
c.ActionCallback = callback
return c
}
// PrintHelp - Output the help text for this command
func (c *Command) PrintHelp() {
versionString := c.AppVersion
if versionString != "" {
versionString = " " + versionString
}
commandTitle := c.CommandPath
if c.Shortdescription != "" {
commandTitle += " - " + c.Shortdescription
}
// Ignore root command
if c.CommandPath != c.Name {
c.log.Yellow(commandTitle)
}
if c.Longdescription != "" {
fmt.Println()
fmt.Println(c.Longdescription + "\n")
}
if len(c.SubCommands) > 0 {
fmt.Println("")
c.log.White("Available commands:")
fmt.Println("")
for _, subcommand := range c.SubCommands {
spacer := strings.Repeat(" ", 3+c.longestSubcommand-len(subcommand.Name))
isDefault := ""
if subcommand.isDefaultCommand() {
isDefault = "[default]"
}
fmt.Printf(" %s%s%s %s\n", subcommand.Name, spacer, subcommand.Shortdescription, isDefault)
}
}
if c.flagCount > 0 {
fmt.Println("")
c.log.White("Flags:")
fmt.Println()
c.Flags.SetOutput(os.Stdout)
c.Flags.PrintDefaults()
c.Flags.SetOutput(os.Stderr)
}
fmt.Println()
}
// isDefaultCommand returns true if called on the default command
func (c *Command) isDefaultCommand() bool {
return c.App.defaultCommand == c
}
// Command - Defines a subcommand
func (c *Command) Command(name, description string) *Command {
result := NewCommand(name, description, c.App, c.CommandPath)
result.log = c.log
c.SubCommands = append(c.SubCommands, result)
c.SubCommandsMap[name] = result
if len(name) > c.longestSubcommand {
c.longestSubcommand = len(name)
}
return result
}
// BoolFlag - Adds a boolean flag to the command
func (c *Command) BoolFlag(name, description string, variable *bool) *Command {
c.Flags.BoolVar(variable, name, *variable, description)
c.flagCount++
return c
}
// StringFlag - Adds a string flag to the command
func (c *Command) StringFlag(name, description string, variable *string) *Command {
c.Flags.StringVar(variable, name, *variable, description)
c.flagCount++
return c
}
// LongDescription - Sets the long description for the command
func (c *Command) LongDescription(Longdescription string) *Command {
c.Longdescription = Longdescription
return c
}

130
cmd/fs.go Normal file
View File

@ -0,0 +1,130 @@
package cmd
import (
"crypto/md5"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
)
type FSHelper struct {
}
func NewFSHelper() *FSHelper {
result := &FSHelper{}
return result
}
// Returns true if the given path resolves to a directory on the filesystem
func (fs *FSHelper) DirExists(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
return false
}
return fi.Mode().IsDir()
}
// FileExists returns a boolean value indicating whether
// the given file exists
func (fs *FSHelper) FileExists(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
return false
}
return fi.Mode().IsRegular()
}
// MkDirs creates the given nested directories.
// Returns error on failure
func (fs *FSHelper) MkDirs(fullPath string, mode ...os.FileMode) error {
var perms os.FileMode
perms = 0700
if len(mode) == 1 {
perms = mode[0]
}
return os.MkdirAll(fullPath, perms)
}
// CopyFile from source to target
func (fs *FSHelper) CopyFile(source, target string) error {
s, err := os.Open(source)
if err != nil {
return err
}
defer s.Close()
d, err := os.Create(target)
if err != nil {
return err
}
if _, err := io.Copy(d, s); err != nil {
d.Close()
return err
}
return d.Close()
}
// Cwd returns the current working directory
// Aborts on Failure
func (fs *FSHelper) Cwd() string {
cwd, err := os.Getwd()
if err != nil {
log.Fatal("Unable to get working directory!")
}
return cwd
}
// GetSubdirs will return a list of FQPs to subdirectories in the given directory
func (fs *FSHelper) GetSubdirs(dir string) (map[string]string, error) {
// Read in the directory information
fileInfo, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
// Allocate space for the list
subdirs := make(map[string]string)
// Pull out the directories and store in the map as
// map["directoryName"] = "path/to/directoryName"
for _, file := range fileInfo {
if file.IsDir() {
subdirs[file.Name()] = filepath.Join(dir, file.Name())
}
}
return subdirs, nil
}
// MkDir creates the given directory.
// Returns error on failure
func (fs *FSHelper) MkDir(dir string) error {
return os.Mkdir(dir, 0700)
}
// LoadAsString will attempt to load the given file and return
// its contents as a string
func (fs *FSHelper) LoadAsString(filename string) (string, error) {
bytes, err := ioutil.ReadFile(filename)
return string(bytes), err
}
// FileMD5 returns the md5sum of the given file
func (fs *FSHelper) FileMD5(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

82
cmd/log.go Normal file
View File

@ -0,0 +1,82 @@
package cmd
import (
"fmt"
"strings"
"github.com/fatih/color"
)
// Logger struct
type Logger struct {
}
// NewLogger creates a new logger!
func NewLogger() *Logger {
return &Logger{}
}
// Yellow - Outputs yellow text
func (l *Logger) Yellow(format string, a ...interface{}) {
color.New(color.FgHiYellow).PrintfFunc()(format+"\n", a...)
}
// Yellowf - Outputs yellow text without the newline
func (l *Logger) Yellowf(format string, a ...interface{}) {
color.New(color.FgHiYellow).PrintfFunc()(format, a...)
}
// Green - Outputs Green text
func (l *Logger) Green(format string, a ...interface{}) {
color.New(color.FgHiGreen).PrintfFunc()(format+"\n", a...)
}
// White - Outputs White text
func (l *Logger) White(format string, a ...interface{}) {
color.New(color.FgHiWhite).PrintfFunc()(format+"\n", a...)
}
// WhiteUnderline - Outputs White text with underline
func (l *Logger) WhiteUnderline(format string, a ...interface{}) {
l.White(format, a...)
l.White(l.underline(format))
}
// YellowUnderline - Outputs Yellow text with underline
func (l *Logger) YellowUnderline(format string, a ...interface{}) {
l.Yellow(format, a...)
l.Yellow(l.underline(format))
}
// underline returns a string of a line, the length of the message given to it
func (l *Logger) underline(message string) string {
return strings.Repeat("-", len(message))
}
// Red - Outputs Red text
func (l *Logger) Red(format string, a ...interface{}) {
color.New(color.FgHiRed).PrintfFunc()(format+"\n", a...)
}
// Error - Outputs an Error message
func (l *Logger) Error(format string, a ...interface{}) {
color.New(color.FgHiRed).PrintfFunc()("Error: "+format+"\n", a...)
}
// PrintBanner prints the Wails banner before running commands
func (l *Logger) PrintBanner() error {
banner1 := ` _ __ _ __
| | / /___ _(_) /____
| | /| / / __ ` + "`" + `/ / / ___/
| |/ |/ / /_/ / / (__ ) `
banner2 := `|__/|__/\__,_/_/_/____/ `
l.Yellowf(banner1)
l.Red(Version)
l.Yellowf(banner2)
l.Green("https://wails.app")
l.White("The lightweight framework for web-like apps")
fmt.Println()
return nil
}

43
cmd/program.go Normal file
View File

@ -0,0 +1,43 @@
package cmd
import (
"os/exec"
"path/filepath"
)
// ProgramHelper - Utility functions around installed applications
type ProgramHelper struct{}
// NewProgramHelper - Creates a new ProgramHelper
func NewProgramHelper() *ProgramHelper {
return &ProgramHelper{}
}
// IsInstalled tries to determine if the given binary name is installed
func (p *ProgramHelper) IsInstalled(programName string) bool {
_, err := exec.LookPath(programName)
return err == nil
}
// Program - A struct to define an installed application/binary
type Program struct {
Name string `json:"name"`
Path string `json:"path"`
}
// FindProgram attempts to find the given program on the system.FindProgram
// Returns a struct with the name and path to the program
func (p *ProgramHelper) FindProgram(programName string) *Program {
path, err := exec.LookPath(programName)
if err != nil {
return nil
}
path, err = filepath.Abs(path)
if err != nil {
return nil
}
return &Program{
Name: programName,
Path: path,
}
}

1
cmd/setup.go Normal file
View File

@ -0,0 +1 @@
package cmd

200
cmd/system.go Normal file
View File

@ -0,0 +1,200 @@
package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"strconv"
"time"
"github.com/AlecAivazis/survey"
homedir "github.com/mitchellh/go-homedir"
)
type SystemHelper struct {
log *Logger
fs *FSHelper
configFilename string
homeDir string
wailsSystemDir string
wailsSystemConfig string
}
func NewSystemHelper() *SystemHelper {
result := &SystemHelper{
fs: NewFSHelper(),
log: NewLogger(),
configFilename: "wails.json",
}
result.setSystemDirs()
return result
}
// Internal
// setSystemDirs calculates the system directories it is interested in
func (s *SystemHelper) setSystemDirs() {
var err error
s.homeDir, err = homedir.Dir()
if err != nil {
log.Fatal("Cannot find home directory! Please file a bug report!")
}
// TODO: A better config system
s.wailsSystemDir = filepath.Join(s.homeDir, ".wails")
s.wailsSystemConfig = filepath.Join(s.wailsSystemDir, s.configFilename)
}
// ConfigFileExists - Returns true if it does!
func (s *SystemHelper) ConfigFileExists() bool {
return s.fs.FileExists(s.wailsSystemConfig)
}
// SystemDirExists - Returns true if it does!
func (s *SystemHelper) systemDirExists() bool {
return s.fs.DirExists(s.wailsSystemDir)
}
// LoadConfig attempts to load the Wails system config
func (s *SystemHelper) LoadConfig() (*SystemConfig, error) {
return NewSystemConfig(s.wailsSystemConfig)
}
// ConfigFileIsValid checks if the config file is valid
func (s *SystemHelper) ConfigFileIsValid() bool {
_, err := NewSystemConfig(s.wailsSystemConfig)
return err == nil
}
// BackupConfig attempts to backup the system config file
func (s *SystemHelper) BackupConfig() (string, error) {
now := strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
backupFilename := s.wailsSystemConfig + "." + now
err := s.fs.CopyFile(s.wailsSystemConfig, backupFilename)
if err != nil {
return "", err
}
return backupFilename, nil
}
func (s *SystemHelper) setup() error {
// Answers. We all need them.
answers := &SystemConfig{}
// Try to load current values - ignore errors
config, err := s.LoadConfig()
defaultName := ""
defaultEmail := ""
if config != nil {
defaultName = config.Name
defaultEmail = config.Email
}
// Questions
var simpleQs = []*survey.Question{
{
Name: "Name",
Prompt: &survey.Input{
Message: "What is your name:",
Default: defaultName,
},
Validate: survey.Required,
},
{
Name: "Email",
Prompt: &survey.Input{
Message: "What is your email address:",
Default: defaultEmail,
},
Validate: survey.Required,
},
}
// ask the questions
err = survey.Ask(simpleQs, answers)
if err != nil {
return err
}
// Create the directory
err = s.fs.MkDirs(s.wailsSystemDir)
if err != nil {
return err
}
fmt.Println()
s.log.White("Wails config saved to: " + s.wailsSystemConfig)
s.log.White("Feel free to customise these settings.")
fmt.Println()
return answers.Save(s.wailsSystemConfig)
}
// Initialise attempts to set up the Wails system.
// An error is returns if there is a problem
func (s *SystemHelper) Initialise() error {
// System dir doesn't exist
if !s.systemDirExists() {
s.log.Green("Welcome to Wails!")
s.log.Green("To get you set up, I'll need to ask you a few things...")
return s.setup()
}
// Config doesn't exist
if !s.ConfigFileExists() {
s.log.Green("Looks like the system config is missing.")
s.log.Green("To get you back on track, I'll need to ask you a few things...")
return s.setup()
}
// Config exists but isn't valid.
if !s.ConfigFileIsValid() {
s.log.Green("Looks like the system config got corrupted.")
backupFile, err := s.BackupConfig()
if err != nil {
s.log.Green("I tried to backup your config file but got this error: %s", err.Error())
} else {
s.log.Green("Just in case you needed it, I backed up your config file here: %s", backupFile)
}
s.log.Green("To get you back on track, I'll need to ask you a few things...")
return s.setup()
}
return s.setup()
}
type SystemConfig struct {
Name string `json:"name"`
Email string `json:"email"`
}
func NewSystemConfig(filename string) (*SystemConfig, error) {
result := &SystemConfig{}
err := result.load(filename)
return result, err
}
func (sc *SystemConfig) Save(filename string) error {
// Convert config to JSON string
theJSON, err := json.MarshalIndent(sc, "", " ")
if err != nil {
return err
}
// Write it out to the config file
return ioutil.WriteFile(filename, theJSON, 0644)
}
func (sc *SystemConfig) load(filename string) error {
configData, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
// Load and unmarshall!
err = json.Unmarshal(configData, &sc)
if err != nil {
return err
}
return nil
}

5
cmd/version.go Normal file
View File

@ -0,0 +1,5 @@
package cmd
// Version - Wails version
// ...oO(There must be a better way)
const Version = "v0.5 Alpha"

60
cmd/wails/0_setup.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"fmt"
"runtime"
"github.com/wailsapp/wails/cmd"
)
func init() {
commandDescription := `Sets up your local environment to develop Wails apps.`
initCommand := app.Command("setup", "Setup the Wails environment").
LongDescription(commandDescription)
initCommand.Action(func() error {
system := cmd.NewSystemHelper()
err := system.Initialise()
if err != nil {
return err
}
var successMessage string
logger.Yellow("Checking for prerequisites...")
// Check we have a cgo capable environment
programHelper := cmd.NewProgramHelper()
prerequisites := make(map[string]map[string]string)
prerequisites["darwin"] = make(map[string]string)
prerequisites["darwin"]["clang"] = "Please install with `xcode-select --install` and try again"
prerequisites["darwin"]["npm"] = "Please download and install npm + node from here: https://nodejs.org/en/"
switch runtime.GOOS {
case "darwin":
successMessage = "🚀 Awesome! We are going to the moon! 🚀"
default:
return fmt.Errorf("platform '%s' is unsupported at this time", runtime.GOOS)
}
errors := false
for name, help := range prerequisites[runtime.GOOS] {
bin := programHelper.FindProgram(name)
if bin == nil {
errors = true
logger.Red("Unable to find '%s' - %s", name, help)
} else {
logger.Green("Found program '%s' at '%s'", name, bin.Path)
}
}
if errors {
err = fmt.Errorf("There were missing dependencies")
} else {
logger.Yellow(successMessage)
}
return err
})
}

26
cmd/wails/main.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"github.com/wailsapp/wails/cmd"
)
// Create Logger
var logger = cmd.NewLogger()
// Create main app
var app = cmd.NewCli("wails", "A cli tool for building Wails applications.")
// Prints the cli banner
func printBanner(app *cmd.Cli) error {
logger.PrintBanner()
return nil
}
// Main!
func main() {
app.PreRun(printBanner)
err := app.Run()
if err != nil {
logger.Error(err.Error())
}
}

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module github.com/wailsapp/wails
require (
github.com/AlecAivazis/survey v1.7.1
github.com/fatih/color v1.7.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/leaanthony/spinner v0.5.0
github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/go-homedir v1.0.0
gopkg.in/AlecAivazis/survey.v1 v1.7.1 // indirect
)

21
go.sum Normal file
View File

@ -0,0 +1,21 @@
github.com/AlecAivazis/survey v1.7.1 h1:a84v5MG2296rBkTP0e+dd4l7NxFQ69v4jzMpErkjVxc=
github.com/AlecAivazis/survey v1.7.1/go.mod h1:MVECab6WqEH1aXhj8nKIwF7HEAJAj2bhhGiSjNy3wII=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/leaanthony/spinner v0.5.0 h1:OJKn+0KP6ilHxwCEOv5Lo0wPM4PgWZWLJTeUprGJK0g=
github.com/leaanthony/spinner v0.5.0/go.mod h1:2Mmv+8Brcw3NwPT1DdOLmW6+zWpSamDDFFsUvVHo2cc=
github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0 h1:1bGojw4YacLY5bqQalojiQ7mSfQbe4WIWCEgPZagowU=
github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/wailsapp/wails v0.0.0-20181215232634-5de8efff325d h1:lk91T4sKD98eGcaz/xC6ER+3o9Kaun7Mk8e/cNZOPMc=
gopkg.in/AlecAivazis/survey.v1 v1.7.1 h1:mzQIVyOPSXJaQWi1m6AFCjrCEPIwQBSOn48Ri8ZpzAg=
gopkg.in/AlecAivazis/survey.v1 v1.7.1/go.mod h1:2Ehl7OqkBl3Xb8VmC4oFW2bItAhnUfzIjrOzwRxCrOU=