Skip to content

Commit

Permalink
Various config install improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
pjcdawkins committed Jan 3, 2025
1 parent 637a421 commit 8de3af6
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 104 deletions.
45 changes: 26 additions & 19 deletions commands/config_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ import (

var configInstallCommand = &cobra.Command{
Use: "config:install [flags] [url]",
Short: "Installs a new CLI instance from a configuration URL",
Short: "Installs an alternative CLI, downloading new configuration from a URL",
Args: cobra.ExactArgs(1),
RunE: runConfigInstall,
}

func runConfigInstall(cmd *cobra.Command, args []string) error {
cmd.PrintErrln("Downloading alternative CLI configuration.")
cmd.PrintErrln("Downloading and validating new CLI configuration...")

newCnfNode, newCnfStruct, err := alt.FetchConfig(cmd.Context(), args[0])
urlStr := args[0]
if !strings.Contains(urlStr, "://") {
urlStr = "https://" + urlStr
}
newCnfNode, newCnfStruct, err := alt.FetchConfig(cmd.Context(), urlStr)
if err != nil {
return err
}
Expand All @@ -37,7 +41,7 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
}

// Find the directory and file paths.
binDir, err := alt.FindBinDir()
executableDir, err := alt.FindBinDir()
if err != nil {
return err
}
Expand All @@ -46,9 +50,9 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
return err
}
configFilePath := filepath.Join(configDir, newExecutable+".yaml")
binFilePath := filepath.Join(binDir, newExecutable)
executableFilePath := filepath.Join(executableDir, newExecutable)

if path, err := exec.LookPath(newExecutable); err == nil && path != binFilePath {
if path, err := exec.LookPath(newExecutable); err == nil && path != executableFilePath {
return fmt.Errorf(
"cannot install config: an executable with the same name already exists at another location: %s",
path,
Expand All @@ -75,13 +79,13 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
if err := os.MkdirAll(configDir, 0o755); err != nil {
return err
}
if err := os.MkdirAll(binDir, 0o755); err != nil {
if err := os.MkdirAll(executableDir, 0o755); err != nil {
return err
}
if err := os.WriteFile(configFilePath, configContent, 0o600); err != nil {
return err
}
if err := os.WriteFile(binFilePath, []byte(executableContent), 0o700); err != nil { //nolint:gosec
if err := os.WriteFile(executableFilePath, []byte(executableContent), 0o700); err != nil { //nolint:gosec
return err
}

Expand All @@ -96,20 +100,23 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
}
}()

cmd.PrintErrln()
cmd.PrintErrf(
"The following files have been created:\n"+
"\nThe following files have been created:\n"+
" - Configuration: %s\n"+
" - Executable: %s\n",
color.CyanString(replaceHomeDir(configFilePath)),
color.CyanString(replaceHomeDir(binFilePath)),
color.CyanString(replaceHomeDir(executableFilePath)),
)
cmd.PrintErrln()
cmd.PrintErrln("The configuration file will be auto-updated periodically in the background.")

isInPath, err := alt.InPath(binDir)
if newCnfStruct.Updates.Check {
cmd.PrintErrln("\nThe configuration file will be auto-updated periodically in the background.")
} else {
cmd.PrintErrln("\nEnable automatic updates to keep the configuration file up to date.")
}

isInPath, err := alt.InPath(executableDir)
if err != nil {
return fmt.Errorf("could not determine if the bin directory is in the PATH: %w", err)
return fmt.Errorf("could not determine if the executable directory is in the PATH: %w", err)
}

envVarName := "PATH"
Expand All @@ -120,17 +127,17 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
cmd.PrintErrln()

if isInPath {
cmd.PrintErrln("Run the new executable with:", color.GreenString(filepath.Base(binFilePath)))
cmd.PrintErrln("Run the new CLI with:", color.GreenString(filepath.Base(executableFilePath)))
} else {
cmd.PrintErrf(
"Add the following directory to your %s: %s\n",
envVarName,
color.YellowString(replaceHomeDir(binDir)),
color.YellowString(replaceHomeDir(executableDir)),
)
cmd.PrintErrln()
cmd.PrintErrln(
"Then you will be able to run the new executable with:",
color.YellowString(filepath.Base(binFilePath)),
"Then you will be able to run the new CLI with:",
color.YellowString(filepath.Base(executableFilePath)),
)
}

Expand Down
40 changes: 20 additions & 20 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
if viper.GetBool("quiet") && !viper.GetBool("debug") && !viper.GetBool("verbose") {
viper.Set("no-interaction", true)
cmd.SetErr(io.Discard)
} else {
cmd.SetErr(color.Error)
}
if viper.GetBool("version") {
versionCommand.Run(cmd, []string{})
Expand All @@ -67,10 +69,10 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
updateMessageChan <- rel
}()
}
if alt.ShouldAutoUpdate(cnf) {
if alt.ShouldUpdate(cnf) {
go func() {
if err := alt.AutoUpdate(cmd.Context(), cnf, debugLog); err != nil {
cmd.PrintErrln("Error auto-updating config:", err)
if err := alt.Update(cmd.Context(), cnf, debugLog); err != nil {
cmd.PrintErrln("Error updating config:", color.RedString(err.Error()))
}
}()
}
Expand All @@ -88,13 +90,13 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
Stdin: cmd.InOrStdin(),
}
if err := c.Init(); err != nil {
_, _ = fmt.Fprint(color.Error, color.RedString(err.Error()))
cmd.PrintErrf("%s\n", color.RedString(err.Error()))
os.Exit(1)
return
}

if err := c.Exec(cmd.Context(), os.Args[1:]...); err != nil {
_, _ = fmt.Fprint(color.Error, color.RedString(err.Error()))
cmd.PrintErrf("%s\n", color.RedString(err.Error()))
exitCode := 1
var execErr *exec.ExitError
if errors.As(err, &execErr) {
Expand All @@ -103,11 +105,11 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
os.Exit(exitCode)
}
},
PersistentPostRun: func(_ *cobra.Command, _ []string) {
checkShellConfigLeftovers(cnf)
PersistentPostRun: func(cmd *cobra.Command, _ []string) {
checkShellConfigLeftovers(cnf, cmd.ErrOrStderr())
select {
case rel := <-updateMessageChan:
printUpdateMessage(rel, cnf)
printUpdateMessage(rel, cnf, cmd.ErrOrStderr())
default:
}
},
Expand Down Expand Up @@ -169,7 +171,7 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
}

// checkShellConfigLeftovers checks .zshrc and .bashrc for any leftovers from the legacy CLI
func checkShellConfigLeftovers(cnf *config.Config) {
func checkShellConfigLeftovers(cnf *config.Config, stdErr io.Writer) {
start := fmt.Sprintf("# BEGIN SNIPPET: %s configuration", cnf.Application.Name)
end := "# END SNIPPET"
shellConfigSnippet := regexp.MustCompile(regexp.QuoteMeta(start) + "(?s).+?" + regexp.QuoteMeta(end))
Expand All @@ -195,44 +197,42 @@ func checkShellConfigLeftovers(cnf *config.Config) {
}

if shellConfigSnippet.Match(shellConfig) {
fmt.Fprintf(color.Error, "%s Your %s file contains code that is no longer needed for the New %s\n",
fmt.Fprintf(stdErr, "%s Your %s file contains code that is no longer needed for the New %s\n",
color.YellowString("Warning:"),
shellConfigFile,
cnf.Application.Name,
)
fmt.Fprintf(color.Error, "%s %s\n", color.YellowString("Please remove the following lines from:"), shellConfigFile)
fmt.Fprintf(color.Error, "\t%s\n", strings.ReplaceAll(string(shellConfigSnippet.Find(shellConfig)), "\n", "\n\t"))
fmt.Fprintf(stdErr, "%s %s\n", color.YellowString("Please remove the following lines from:"), shellConfigFile)
fmt.Fprintf(stdErr, "\t%s\n", strings.ReplaceAll(string(shellConfigSnippet.Find(shellConfig)), "\n", "\n\t"))
}
}
}

func printUpdateMessage(newRelease *internal.ReleaseInfo, cnf *config.Config) {
func printUpdateMessage(newRelease *internal.ReleaseInfo, cnf *config.Config, stdErr io.Writer) {
if newRelease == nil {
return
}

fmt.Fprintf(color.Error, "\n\n%s %s → %s\n",
fmt.Fprintf(stdErr, "\n\n%s %s → %s\n",
color.YellowString(fmt.Sprintf("A new release of the %s is available:", cnf.Application.Name)),
color.CyanString(config.Version),
color.CyanString(newRelease.Version),
)

executable, err := os.Executable()
if err == nil && cnf.Wrapper.HomebrewTap != "" && isUnderHomebrew(executable) {
fmt.Fprintf(
color.Error,
fmt.Fprintf(stdErr,
"To upgrade, run: brew update && brew upgrade %s\n",
color.YellowString(cnf.Wrapper.HomebrewTap),
)
} else if cnf.Wrapper.GitHubRepo != "" {
fmt.Fprintf(
color.Error,
fmt.Fprintf(stdErr,
"To upgrade, follow the instructions at: https://github.com/%s#upgrade\n",
cnf.Wrapper.GitHubRepo,
)
}

fmt.Fprintf(color.Error, "%s\n\n", color.YellowString(newRelease.URL))
fmt.Fprintf(stdErr, "%s\n\n", color.YellowString(newRelease.URL))
}

func isUnderHomebrew(binary string) bool {
Expand All @@ -256,5 +256,5 @@ func debugLog(format string, v ...any) {
}

prefix := color.New(color.ReverseVideo).Sprintf("DEBUG")
_, _ = fmt.Fprintf(color.Error, prefix+" "+strings.TrimSpace(format)+"\n", v...)
fmt.Fprintf(color.Error, prefix+" "+strings.TrimSpace(format)+"\n", v...)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/crypto v0.31.0
golang.org/x/mod v0.18.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
Expand Down
15 changes: 8 additions & 7 deletions internal/config/alt/alt.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"

"gopkg.in/yaml.v3"
Expand All @@ -19,9 +18,6 @@ import (
// A comment and some metadata are added to the cnfNode.
// A "cnfStruct" is also returned to allow reading some keys.
func FetchConfig(ctx context.Context, urlStr string) (cnfNode *yaml.Node, cnfStruct *config.Config, err error) {
if !strings.Contains(urlStr, "://") {
urlStr = "https://" + urlStr
}
resp, err := fetch(ctx, urlStr)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch config: %w", err)
Expand Down Expand Up @@ -82,9 +78,10 @@ func processConfig(b []byte, urlStr string) (cnfNode *yaml.Node, cnfStruct *conf

// Add a comment to the document.
cnfNode.HeadComment = fmt.Sprintf(
"CLI configuration.\n"+
"This file will be updated automatically.\n"+
"%s configuration.\n"+
"Do not edit this file, as it will be replaced with updated versions automatically.\n"+
"Source URL: %s\nDownloaded at: %s\n",
cnfStruct.Application.Name,
urlStr,
metadata.DownloadedAt.Format(time.RFC3339),
)
Expand Down Expand Up @@ -130,9 +127,13 @@ func deleteDocumentKey(doc *yaml.Node, toDelete string) error {
}

func fetch(ctx context.Context, urlStr string) (*http.Response, error) {
if _, err := url.Parse(urlStr); err != nil {
u, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
if u.Scheme != "https" && u.Hostname() != "127.0.0.1" {
return nil, fmt.Errorf("invalid config URL scheme %s (https required): %s", u.Scheme, urlStr)
}
httpClient := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, http.NoBody)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/config/alt/fs.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package alt manages instances of alternate CLI configurations.
// Package alt manages instances of alternative CLI configurations.
package alt

import (
Expand Down
Loading

0 comments on commit 8de3af6

Please sign in to comment.