From 63a991edd370561228cb8b30f3b5e43674d8eca4 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Tue, 19 Apr 2022 09:51:29 -0700 Subject: [PATCH 1/5] Port link to cobra --- cli/internal/login/link.go | 137 ++++++++++++++++++++++--------------- cli/internal/util/cmd.go | 17 +++++ 2 files changed, 98 insertions(+), 56 deletions(-) create mode 100644 cli/internal/util/cmd.go diff --git a/cli/internal/login/link.go b/cli/internal/login/link.go index 73c3071f55259..4010d60fb0248 100644 --- a/cli/internal/login/link.go +++ b/cli/internal/login/link.go @@ -6,6 +6,8 @@ import ( "path/filepath" "strings" + "github.com/pkg/errors" + "github.com/spf13/cobra" "github.com/vercel/turborepo/cli/internal/client" "github.com/vercel/turborepo/cli/internal/config" "github.com/vercel/turborepo/cli/internal/fs" @@ -24,47 +26,77 @@ type LinkCommand struct { Ui *cli.ColoredUi } +type link struct { + config *config.Config + ui cli.Ui + modifyGitIgnore bool +} + +func getCmd(config *config.Config, ui cli.Ui) *cobra.Command { + var dontModifyGitIgnore bool + cmd := &cobra.Command{ + Use: "turbo link", + Short: "Link your local directory to a Vercel organization and enable remote caching.", + RunE: func(cmd *cobra.Command, args []string) error { + link := &link{ + config: config, + ui: ui, + modifyGitIgnore: !dontModifyGitIgnore, + } + return link.run() + }, + } + cmd.Flags().BoolVar(&dontModifyGitIgnore, "no-gitignore", false, "Do not create or modify .gitignore (default false)") + return cmd +} + // Synopsis of link command func (c *LinkCommand) Synopsis() string { - return "Link your local directory to a Vercel organization and enable remote caching." + cmd := getCmd(c.Config, c.Ui) + return cmd.Short } // Help returns information about the `link` command func (c *LinkCommand) Help() string { - helpText := ` -Usage: turbo link - - Link your local directory to a Vercel organization and enable remote caching. - -Options: - --help Show this screen. - --no-gitignore Do not create or modify .gitignore - (default false) -` - return strings.TrimSpace(helpText) + cmd := getCmd(c.Config, c.Ui) + return util.HelpForCobraCmd(cmd) } // Run links a local directory to a Vercel organization and enables remote caching func (c *LinkCommand) Run(args []string) int { - var dontModifyGitIgnore bool - shouldSetup := true - dir, homeDirErr := homedir.Dir() - if homeDirErr != nil { - c.logError(fmt.Errorf("could not find home directory.\n%w", homeDirErr)) + cmd := getCmd(c.Config, c.Ui) + cmd.SetArgs(args) + err := cmd.Execute() + if err != nil { + if errors.Is(err, errUserCancelled) { + c.Ui.Info("Cancelled. Turborepo not set up.") + } else { + c.logError(err) + } return 1 } - c.Ui.Info(">>> Remote Caching (beta)") - c.Ui.Info("") - c.Ui.Info(" Remote Caching shares your cached Turborepo task outputs and logs across") - c.Ui.Info(" all your team’s Vercel projects. It also can share outputs") - c.Ui.Info(" with other services that enable Remote Caching, like CI/CD systems.") - c.Ui.Info(" This results in faster build times and deployments for your team.") - c.Ui.Info(util.Sprintf(" For more info, see ${UNDERLINE}https://turborepo.org/docs/features/remote-caching${RESET}")) - c.Ui.Info("") - currentDir, fpErr := filepath.Abs(".") - if fpErr != nil { - c.logError(fmt.Errorf("could figure out file path.\n%w", fpErr)) - return 1 + return 0 +} + +var errUserCancelled = errors.New("cancelled") + +func (l *link) run() error { + shouldSetup := true + dir, err := homedir.Dir() + if err != nil { + return fmt.Errorf("could not find home directory.\n%w", err) + } + l.ui.Info(">>> Remote Caching (beta)") + l.ui.Info("") + l.ui.Info(" Remote Caching shares your cached Turborepo task outputs and logs across") + l.ui.Info(" all your team’s Vercel projects. It also can share outputs") + l.ui.Info(" with other services that enable Remote Caching, like CI/CD systems.") + l.ui.Info(" This results in faster build times and deployments for your team.") + l.ui.Info(util.Sprintf(" For more info, see ${UNDERLINE}https://turborepo.org/docs/features/remote-caching${RESET}")) + l.ui.Info("") + currentDir, err := filepath.Abs(".") + if err != nil { + return fmt.Errorf("could figure out file path.\n%w", err) } survey.AskOne( @@ -79,24 +111,20 @@ func (c *LinkCommand) Run(args []string) int { })) if !shouldSetup { - c.Ui.Info("> Canceled.") - return 1 + return errUserCancelled } - if c.Config.Token == "" { - c.logError(fmt.Errorf(util.Sprintf("User not found. Please login to Turborepo first by running ${BOLD}`npx turbo login`${RESET}."))) - return 1 + if l.config.Token == "" { + return fmt.Errorf(util.Sprintf("User not found. Please login to Turborepo first by running ${BOLD}`npx turbo login`${RESET}.")) } - teamsResponse, err := c.Config.ApiClient.GetTeams() + teamsResponse, err := l.config.ApiClient.GetTeams() if err != nil { - c.logError(fmt.Errorf("could not get team information.\n%w", err)) - return 1 + return fmt.Errorf("could not get team information.\n%w", err) } - userResponse, err := c.Config.ApiClient.GetUser() + userResponse, err := l.config.ApiClient.GetUser() if err != nil { - c.logError(fmt.Errorf("could not get user information.\n%w", err)) - return 1 + return fmt.Errorf("could not get user information.\n%w", err) } var chosenTeam client.Team @@ -126,8 +154,7 @@ func (c *LinkCommand) Run(args []string) int { })) if chosenTeamName == "" { - c.Ui.Info("Canceled. Turborepo not set up.") - return 1 + return errUserCancelled } else if (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username) { chosenTeam = client.Team{ ID: userResponse.User.ID, @@ -143,30 +170,28 @@ func (c *LinkCommand) Run(args []string) int { } } fs.EnsureDir(filepath.Join(".turbo", "config.json")) - fsErr := config.WriteRepoConfigFile(&config.TurborepoConfig{ + err = config.WriteRepoConfigFile(&config.TurborepoConfig{ TeamId: chosenTeam.ID, - ApiUrl: c.Config.ApiUrl, + ApiUrl: l.config.ApiUrl, }) - if fsErr != nil { - c.logError(fmt.Errorf("could not link current directory to team/user.\n%w", fsErr)) - return 1 + if err != nil { + return fmt.Errorf("could not link current directory to team/user.\n%w", err) } - if !dontModifyGitIgnore { + if l.modifyGitIgnore { fs.EnsureDir(".gitignore") _, gitIgnoreErr := exec.Command("sh", "-c", "grep -qxF '.turbo' .gitignore || echo '.turbo' >> .gitignore").CombinedOutput() if err != nil { - c.logError(fmt.Errorf("could find or update .gitignore.\n%w", gitIgnoreErr)) - return 1 + return fmt.Errorf("could find or update .gitignore.\n%w", gitIgnoreErr) } } - c.Ui.Info("") - c.Ui.Info(util.Sprintf("%s${RESET} Turborepo CLI authorized for ${BOLD}%s${RESET}", ui.Rainbow(">>> Success!"), chosenTeamName)) - c.Ui.Info("") - c.Ui.Info(util.Sprintf("${GREY}To disable Remote Caching, run `npx turbo unlink`${RESET}")) - c.Ui.Info("") - return 0 + l.ui.Info("") + l.ui.Info(util.Sprintf("%s${RESET} Turborepo CLI authorized for ${BOLD}%s${RESET}", ui.Rainbow(">>> Success!"), chosenTeamName)) + l.ui.Info("") + l.ui.Info(util.Sprintf("${GREY}To disable Remote Caching, run `npx turbo unlink`${RESET}")) + l.ui.Info("") + return nil } // logError logs an error and outputs it to the UI. diff --git a/cli/internal/util/cmd.go b/cli/internal/util/cmd.go new file mode 100644 index 0000000000000..1e33c6730068b --- /dev/null +++ b/cli/internal/util/cmd.go @@ -0,0 +1,17 @@ +package util + +import ( + "bytes" + + "github.com/spf13/cobra" +) + +// HelpForCobraCmd returns the help string for a given command +// Note that this overwrites the output for the command +func HelpForCobraCmd(cmd *cobra.Command) string { + f := cmd.HelpFunc() + buf := bytes.NewBufferString("") + cmd.SetOut(buf) + f(cmd, []string{}) + return buf.String() +} From f008635b425202cde009ec208d1bd4e2707b572f Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Tue, 19 Apr 2022 16:38:25 -0700 Subject: [PATCH 2/5] Parametrize parts of link --- cli/internal/client/client.go | 8 +++ cli/internal/login/link.go | 109 ++++++++++++++++++++++------------ 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/cli/internal/client/client.go b/cli/internal/client/client.go index 8e5e08343a908..30a80413d34e8 100644 --- a/cli/internal/client/client.go +++ b/cli/internal/client/client.go @@ -68,6 +68,14 @@ func NewClient(baseURL string, logger hclog.Logger, turboVersion string, teamID return client } +func (c *ApiClient) IsLoggedIn() bool { + return c.Token != "" +} + +func (c *ApiClient) SetTeamID(teamID string) { + c.teamID = teamID +} + func (c *ApiClient) retryCachePolicy(resp *http.Response, err error) (bool, error) { if err != nil { if errors.As(err, &x509.UnknownAuthorityError{}) { diff --git a/cli/internal/login/link.go b/cli/internal/login/link.go index 4010d60fb0248..0adca4fa7e034 100644 --- a/cli/internal/login/link.go +++ b/cli/internal/login/link.go @@ -27,9 +27,19 @@ type LinkCommand struct { } type link struct { - config *config.Config ui cli.Ui modifyGitIgnore bool + apiURL string + apiClient linkAPIClient + promptSetup func(location string) (bool, error) + promptTeam func(teams []string) (string, error) +} + +type linkAPIClient interface { + IsLoggedIn() bool + GetTeams() (*client.TeamsResponse, error) + GetUser() (*client.UserResponse, error) + SetTeamID(teamID string) } func getCmd(config *config.Config, ui cli.Ui) *cobra.Command { @@ -39,9 +49,12 @@ func getCmd(config *config.Config, ui cli.Ui) *cobra.Command { Short: "Link your local directory to a Vercel organization and enable remote caching.", RunE: func(cmd *cobra.Command, args []string) error { link := &link{ - config: config, ui: ui, modifyGitIgnore: !dontModifyGitIgnore, + apiURL: config.ApiUrl, + apiClient: config.ApiClient, + promptSetup: promptSetup, + promptTeam: promptTeam, } return link.run() }, @@ -81,7 +94,6 @@ func (c *LinkCommand) Run(args []string) int { var errUserCancelled = errors.New("cancelled") func (l *link) run() error { - shouldSetup := true dir, err := homedir.Dir() if err != nil { return fmt.Errorf("could not find home directory.\n%w", err) @@ -98,64 +110,46 @@ func (l *link) run() error { if err != nil { return fmt.Errorf("could figure out file path.\n%w", err) } - - survey.AskOne( - &survey.Confirm{ - Default: true, - Message: util.Sprintf("Would you like to enable Remote Caching for ${CYAN}${BOLD}\"%s\"${RESET}?", strings.Replace(currentDir, dir, "~", 1)), - }, - &shouldSetup, survey.WithValidator(survey.Required), - survey.WithIcons(func(icons *survey.IconSet) { - // for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format - icons.Question.Format = "gray+hb" - })) + repoLocation := strings.Replace(currentDir, dir, "~", 1) + shouldSetup, err := l.promptSetup(repoLocation) if !shouldSetup { return errUserCancelled } - if l.config.Token == "" { + if !l.apiClient.IsLoggedIn() { return fmt.Errorf(util.Sprintf("User not found. Please login to Turborepo first by running ${BOLD}`npx turbo login`${RESET}.")) } - teamsResponse, err := l.config.ApiClient.GetTeams() + teamsResponse, err := l.apiClient.GetTeams() if err != nil { return fmt.Errorf("could not get team information.\n%w", err) } - userResponse, err := l.config.ApiClient.GetUser() + userResponse, err := l.apiClient.GetUser() if err != nil { return fmt.Errorf("could not get user information.\n%w", err) } - var chosenTeam client.Team - - teamOptions := make([]string, len(teamsResponse.Teams)) - // Gather team options - for i, team := range teamsResponse.Teams { - teamOptions[i] = team.Name - } - - var chosenTeamName string + teamOptions := make([]string, len(teamsResponse.Teams)+1) nameWithFallback := userResponse.User.Name if nameWithFallback == "" { nameWithFallback = userResponse.User.Username } - survey.AskOne( - &survey.Select{ - Message: "Which Vercel scope (and Remote Cache) do you want associate with this Turborepo? ", - Options: append([]string{nameWithFallback}, teamOptions...), - }, - &chosenTeamName, - survey.WithValidator(survey.Required), - survey.WithIcons(func(icons *survey.IconSet) { - // for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format - icons.Question.Format = "gray+hb" - })) + teamOptions[0] = nameWithFallback + for i, team := range teamsResponse.Teams { + teamOptions[i+1] = team.Name + } + chosenTeamName, err := l.promptTeam(teamOptions) + if err != nil { + return err + } if chosenTeamName == "" { return errUserCancelled - } else if (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username) { + } + var chosenTeam client.Team + if (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username) { chosenTeam = client.Team{ ID: userResponse.User.ID, Name: userResponse.User.Name, @@ -172,7 +166,7 @@ func (l *link) run() error { fs.EnsureDir(filepath.Join(".turbo", "config.json")) err = config.WriteRepoConfigFile(&config.TurborepoConfig{ TeamId: chosenTeam.ID, - ApiUrl: l.config.ApiUrl, + ApiUrl: l.apiURL, }) if err != nil { return fmt.Errorf("could not link current directory to team/user.\n%w", err) @@ -199,3 +193,40 @@ func (c *LinkCommand) logError(err error) { c.Config.Logger.Error("error", err) c.Ui.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err))) } + +func promptSetup(location string) (bool, error) { + shouldSetup := true + err := survey.AskOne( + &survey.Confirm{ + Default: true, + Message: util.Sprintf("Would you like to enable Remote Caching for ${CYAN}${BOLD}\"%s\"${RESET}?", location), + }, + &shouldSetup, survey.WithValidator(survey.Required), + survey.WithIcons(func(icons *survey.IconSet) { + // for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format + icons.Question.Format = "gray+hb" + })) + if err != nil { + return false, err + } + return shouldSetup, nil +} + +func promptTeam(teams []string) (string, error) { + chosenTeamName := "" + err := survey.AskOne( + &survey.Select{ + Message: "Which Vercel scope (and Remote Cache) do you want associate with this Turborepo? ", + Options: teams, + }, + &chosenTeamName, + survey.WithValidator(survey.Required), + survey.WithIcons(func(icons *survey.IconSet) { + // for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format + icons.Question.Format = "gray+hb" + })) + if err != nil { + return "", err + } + return chosenTeamName, nil +} From 3cf093616419c2dfdd84acad182f7bb32d07f15a Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Tue, 19 Apr 2022 16:43:40 -0700 Subject: [PATCH 3/5] Cancelled -> Canceled --- cli/internal/login/link.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/internal/login/link.go b/cli/internal/login/link.go index 0adca4fa7e034..5d34be940eef4 100644 --- a/cli/internal/login/link.go +++ b/cli/internal/login/link.go @@ -81,8 +81,8 @@ func (c *LinkCommand) Run(args []string) int { cmd.SetArgs(args) err := cmd.Execute() if err != nil { - if errors.Is(err, errUserCancelled) { - c.Ui.Info("Cancelled. Turborepo not set up.") + if errors.Is(err, errUserCanceled) { + c.Ui.Info("Canceled. Turborepo not set up.") } else { c.logError(err) } @@ -91,7 +91,7 @@ func (c *LinkCommand) Run(args []string) int { return 0 } -var errUserCancelled = errors.New("cancelled") +var errUserCanceled = errors.New("Canceled") func (l *link) run() error { dir, err := homedir.Dir() @@ -114,7 +114,7 @@ func (l *link) run() error { shouldSetup, err := l.promptSetup(repoLocation) if !shouldSetup { - return errUserCancelled + return errUserCanceled } if !l.apiClient.IsLoggedIn() { @@ -146,7 +146,7 @@ func (l *link) run() error { return err } if chosenTeamName == "" { - return errUserCancelled + return errUserCanceled } var chosenTeam client.Team if (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username) { From 9e77ee4677e6bec8618cda5467139645169eb9f3 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Tue, 19 Apr 2022 16:55:41 -0700 Subject: [PATCH 4/5] lint --- cli/internal/client/client.go | 2 ++ cli/internal/login/link.go | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/internal/client/client.go b/cli/internal/client/client.go index 30a80413d34e8..835c1ed17071a 100644 --- a/cli/internal/client/client.go +++ b/cli/internal/client/client.go @@ -68,10 +68,12 @@ func NewClient(baseURL string, logger hclog.Logger, turboVersion string, teamID return client } +// IsLoggedIn returns true if this ApiClient has a credential (token) func (c *ApiClient) IsLoggedIn() bool { return c.Token != "" } +// SetTeamID sets the team parameter used on all requests by this client func (c *ApiClient) SetTeamID(teamID string) { c.teamID = teamID } diff --git a/cli/internal/login/link.go b/cli/internal/login/link.go index 5d34be940eef4..25432490d01be 100644 --- a/cli/internal/login/link.go +++ b/cli/internal/login/link.go @@ -112,7 +112,9 @@ func (l *link) run() error { } repoLocation := strings.Replace(currentDir, dir, "~", 1) shouldSetup, err := l.promptSetup(repoLocation) - + if err != nil { + return err + } if !shouldSetup { return errUserCanceled } From 0869fac8ba97e13087d611f7b7a8e0210e643c0f Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Wed, 20 Apr 2022 12:01:11 -0700 Subject: [PATCH 5/5] cobra command now responsible for logging errors from link --- cli/internal/login/link.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/cli/internal/login/link.go b/cli/internal/login/link.go index 25432490d01be..09ba37fdf9960 100644 --- a/cli/internal/login/link.go +++ b/cli/internal/login/link.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/hashicorp/go-hclog" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/vercel/turborepo/cli/internal/client" @@ -28,6 +29,7 @@ type LinkCommand struct { type link struct { ui cli.Ui + logger hclog.Logger modifyGitIgnore bool apiURL string apiClient linkAPIClient @@ -50,13 +52,23 @@ func getCmd(config *config.Config, ui cli.Ui) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { link := &link{ ui: ui, + logger: config.Logger, modifyGitIgnore: !dontModifyGitIgnore, apiURL: config.ApiUrl, apiClient: config.ApiClient, promptSetup: promptSetup, promptTeam: promptTeam, } - return link.run() + err := link.run() + if err != nil { + if errors.Is(err, errUserCanceled) { + ui.Info("Canceled. Turborepo not set up.") + } else { + link.logError(err) + } + return err + } + return nil }, } cmd.Flags().BoolVar(&dontModifyGitIgnore, "no-gitignore", false, "Do not create or modify .gitignore (default false)") @@ -81,11 +93,6 @@ func (c *LinkCommand) Run(args []string) int { cmd.SetArgs(args) err := cmd.Execute() if err != nil { - if errors.Is(err, errUserCanceled) { - c.Ui.Info("Canceled. Turborepo not set up.") - } else { - c.logError(err) - } return 1 } return 0 @@ -191,9 +198,9 @@ func (l *link) run() error { } // logError logs an error and outputs it to the UI. -func (c *LinkCommand) logError(err error) { - c.Config.Logger.Error("error", err) - c.Ui.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err))) +func (l *link) logError(err error) { + l.logger.Error("error", err) + l.ui.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err))) } func promptSetup(location string) (bool, error) {