Repository: tetratelabs/getenvoy (go)

Issue:
command overhaul: 3 commands!
Hopefully, the last cleanup in pursuit of simplest path (before we release stuff!)

goals:
* intuitive as possible (little jargon)
* zero steps (no "run this before that")
* auto-completion friendly (avoid commands that share same first character)

end state:
* `getenvoy versions` (lists installed versions, -a includes remote ones not yet installed)
* `getenvoy use <version>` (assign current version, installing as necessary)
* `getenvoy run -c envoy.yaml` (run current version, installing as necessary)
current version is $ENVOY_VERSION, $PWD/.envoy-version or $ENVOY_HOME/version, which defaults to envoy-versions.json/latestVersion

current state (still not released):
* `run <version> -c envoy.yaml` (run installing on demand as necessary)
* `versions` (lists all versions)
* `install <version>` (install if not already)
* `installed` (lists all installed versions)
current version requires looking at the versions list

action 1: remove `<version>` from run, and read $ENVOY_VERSION $PWD/.envoy-version or $ENVOY_HOME/version
* simplifies `run` syntax and the need to know what version you'd want
* removes all questions about args to envoy: everything after `run` is interpreted by envoy
* $ENVOY_VERSION makes runs in CI for a non-latest version single-step
* on first run $ENVOY_HOME/version is populated from envoy-versions.json/latestVersion

action 2: repurpose `install -> use <version>`
* if we introduce a command to assign a version, there's a chance it isn't there, ends up the same as install
* rename install to use, and upon `use <version>` install it as necessary
* new change is to update $PWD/.envoy-version or $ENVOY_HOME/version if present

action 3: don't make a flag to write .envoy-version
* `getenvoy use 1.18.3 --local` is only 1 more char than `echo 1.18.3 > .envoy-version`
* by waiting to define a flag, we don't need to choose between bad jargon choices ex "local" or "cwd"
* by waiting, we can get more input from end users

action 4: fold `installed` into `versions -a/--all`
* `getenvoy install` collides on auto-complete with `getenvoy installed` which is annoying
* `getenvoy installed` output is different than `getenvoy versions` mainly because we didn't have release date locally
* during install, we can persist the "releaseDate" field into the $ENVOY_HOME/versions/$version directory create date
* at that point `getenvoy versions` shows the installed ones and `getenvoy versions -a` shows all including remote, both knowing the release date


Pull request:
Repurposes install -> use <version>
This removes `getenvoy install` for `getenvoy use`. The most essential change is that this assigns the version `getenvoy run` uses upon invocation. Specifically, the new change is it updates $PWD/.envoy-version or $ENVOY_HOME/version if present.

Notes:
* This intentionally doesn't crash `getenvoy list` when a version is targeted with a version file, but not present yet
* `getenvoy run` continues to install on-demand as it did before
* For edge cases that need to pre-install multiple versions, simply run `getenvoy use` in a loop, with the intended default version last.

Changes to `getenvoy versions` output:
* this indicates what version is current and why similar to `pyenv`
* this removes header and error when no versions are present for parsing

Usage example:
```bash
$ getenvoy versions
* 1.18.3 2021-05-11 (set by $GETENVOY_HOME/version)
$ getenvoy versions -a
* 1.18.3 2021-05-11 (set by $GETENVOY_HOME/version)
  1.17.3 2021-05-11
  1.16.4 2021-05-11
  1.15.5 2021-05-11
  1.14.7 2021-04-15
$ getenvoy use 1.14.7
downloading https://dl.getenvoy.io/public/raw/files/getenvoy-envoy-1.14.7.p0.gbc15868-1p74.gbb8060d-darwin-release-x86_64.tar.xz
$ getenvoy versions
  1.18.3 2021-05-11
* 1.14.7 2021-04-15 (set by $GETENVOY_HOME/version)
$ echo 1.15.5 > .envoy-version
$ getenvoy versions
  1.18.3 2021-05-11
  1.14.7 2021-04-15
$ getenvoy run
downloading https://dl.getenvoy.io/public/raw/files/getenvoy-envoy-1.15.5.p0.g623479e-1p74.gbb8060d-darwin-release-x86_64.tar.xz
--snip--
$ getenvoy versions
  1.18.3 2021-05-11
* 1.15.5 2021-05-11 (set by $PWD/.envoy-version)
  1.14.7 2021-04-15
$ ENVOY_VERSION=1.18.3 getenvoy versions
* 1.18.3 2021-05-11 (set by $ENVOY_VERSION)
  1.15.5 2021-05-11
  1.14.7 2021-04-15
```

Fixes #248

Patch:
diff --git a/internal/cmd/app.go b/internal/cmd/app.go
index 749673f..3588460 100644
--- a/internal/cmd/app.go
+++ b/internal/cmd/app.go
@@ -69,7 +69,7 @@ func NewApp(o *globals.GlobalOpts) *cli.App {
 		helpCommand,
 		NewRunCmd(o),
 		NewVersionsCmd(o),
-		NewInstallCmd(o),
+		NewUseCmd(o),
 		NewDocCmd(),
 	}
 	return app
diff --git a/internal/cmd/run.go b/internal/cmd/run.go
index c5f1060..430e949 100644
--- a/internal/cmd/run.go
+++ b/internal/cmd/run.go
@@ -36,8 +36,11 @@ func NewRunCmd(o *globals.GlobalOpts) *cli.Command {
 		Usage:           "Run Envoy with the given [arguments...], collecting process state on termination",
 		ArgsUsage:       "[arguments...]",
 		SkipFlagParsing: true,
-		Description: fmt.Sprintf(`The version of Envoy run is chosen from %s
-The '[arguments...]' are interpreted by Envoy.
+		Description: fmt.Sprintf(`
+Envoy interprets the '[arguments...]'.
+
+The "use" command primarily controls the version. The first version in %s is
+run, installing on-demand as needed.
 
 Envoy uses $GETENVOY_HOME/runs/$epochtime as the working directory.
 Upon termination, this is archived as $GETENVOY_HOME/runs/$epochtime.tar.gz.
diff --git a/internal/cmd/install.go b/internal/cmd/use.go
similarity index 65%
rename from internal/cmd/install.go
rename to internal/cmd/use.go
index df9c0b8..94eb74d 100644
--- a/internal/cmd/install.go
+++ b/internal/cmd/use.go
@@ -24,21 +24,28 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
-// NewInstallCmd create a command responsible for downloading and extracting Envoy
-func NewInstallCmd(o *globals.GlobalOpts) *cli.Command {
+// NewUseCmd create a command responsible for downloading and extracting Envoy
+func NewUseCmd(o *globals.GlobalOpts) *cli.Command {
 	return &cli.Command{
-		Name:      "install",
-		Usage:     "Download and install a [version] of Envoy",
+		Name:      "use",
+		Usage:     `Sets the current [version] used by the "run" command, installing as necessary`,
 		ArgsUsage: "[version]",
-		Description: fmt.Sprintf(`The '[version]' is from the "versions" command.
-The Envoy [version] will be installed into $GETENVOY_HOME/versions/[version]
+		Description: fmt.Sprintf(`The '[version]' is from the "versions -a" command.
+The Envoy [version] installs on-demand into $GETENVOY_HOME/versions/[version]
+if needed.
+
+This updates %s or %s with [version],
+depending on which is present.
 
 Example:
-$ getenvoy install %s`, version.LastKnownEnvoy),
+$ getenvoy use %s`, envoy.CurrentVersionWorkingDirFile, envoy.CurrentVersionHomeDirFile, version.LastKnownEnvoy),
 		Before: validateVersionArg,
 		Action: func(c *cli.Context) error {
-			_, err := envoy.InstallIfNeeded(o, globals.CurrentPlatform, c.Args().First())
-			return err
+			v := c.Args().First()
+			if _, err := envoy.InstallIfNeeded(o, globals.CurrentPlatform, v); err != nil {
+				return err
+			}
+			return envoy.WriteCurrentVersion(v, o.HomeDir)
 		},
 	}
 }
diff --git a/internal/cmd/versions.go b/internal/cmd/versions.go
index e758183..7653106 100644
--- a/internal/cmd/versions.go
+++ b/internal/cmd/versions.go
@@ -19,6 +19,7 @@ import (
 	"os"
 	"path/filepath"
 	"sort"
+	"text/tabwriter"
 	"time"
 
 	"github.com/urfave/cli/v2"
@@ -44,6 +45,10 @@ func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command {
 			if err != nil {
 				return err
 			}
+
+			// tolerate errors determining current version, as that can be due to initial or out-of-band setup
+			currentVersion, currentVersionSource, _ := envoy.CurrentVersion(o.HomeDir)
+
 			if c.Bool("all") {
 				if ev, err := envoy.GetEnvoyVersions(o.EnvoyVersionsURL, o.UserAgent); err != nil {
 					return err
@@ -52,11 +57,6 @@ func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command {
 				}
 			}
 
-			if len(rows) == 0 {
-				fmt.Fprintln(c.App.Writer, "No Envoy versions, yet") //nolint
-				return nil
-			}
-
 			// Sort so that new release dates appear first and on conflict choosing the higher version
 			sort.Slice(rows, func(i, j int) bool {
 				if rows[i].releaseDate == rows[j].releaseDate {
@@ -65,12 +65,16 @@ func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command {
 				return rows[i].releaseDate > rows[j].releaseDate
 			})
 
-			// This doesn't use tabwriter because the columns are likely to remain the same width.
-			fmt.Fprintln(c.App.Writer, "VERSION\tRELEASE_DATE") //nolint
-			for _, vr := range rows {                           //nolint:gocritic
-				fmt.Fprintf(c.App.Writer, "%s\t%s\n", vr.version, vr.releaseDate) //nolint
+			// We use a tab writer to ensure we can format the current version
+			w := tabwriter.NewWriter(c.App.Writer, 0, 0, 1, ' ', tabwriter.AlignRight)
+			for _, vr := range rows { //nolint:gocritic
+				if vr.version == currentVersion {
+					fmt.Fprintf(w, "* %s %s (set by %s)\n", vr.version, vr.releaseDate, currentVersionSource) //nolint
+				} else {
+					fmt.Fprintf(w, "  %s %s\n", vr.version, vr.releaseDate) //nolint
+				}
 			}
-			return nil
+			return w.Flush()
 		},
 	}
 }
diff --git a/internal/envoy/version.go b/internal/envoy/version.go
index 8473bb3..2f29571 100644
--- a/internal/envoy/version.go
+++ b/internal/envoy/version.go
@@ -25,9 +25,11 @@ import (
 )
 
 var (
-	currentVersionVar            = "$ENVOY_VERSION"
-	currentVersionWorkingDirFile = filepath.Join("$PWD", ".envoy-version")
-	currentVersionHomeDirFile    = filepath.Join("$GETENVOY_HOME", "version")
+	currentVersionVar = "$ENVOY_VERSION"
+	// CurrentVersionWorkingDirFile is used for stable "versions" and "help" output
+	CurrentVersionWorkingDirFile = filepath.Join("$PWD", ".envoy-version")
+	// CurrentVersionHomeDirFile is used for stable "versions" and "help" output
+	CurrentVersionHomeDirFile = filepath.Join("$GETENVOY_HOME", "version")
 )
 
 // GetHomeVersion returns the default version in the "homeDir" and path to to it (homeVersionFile). When "v" is empty,
@@ -37,10 +39,21 @@ func GetHomeVersion(homeDir string) (v, homeVersionFile string, err error) {
 	if err == nil && v == "" { // no home version, yet
 		return
 	}
-	err = verifyVersion(v, currentVersionHomeDirFile, err)
+	err = verifyVersion(v, CurrentVersionHomeDirFile, err)
 	return
 }
 
+// WriteCurrentVersion writes the version to CurrentVersionWorkingDirFile or CurrentVersionHomeDirFile depending on
+// if the former is present.
+func WriteCurrentVersion(v, homeDir string) error {
+	if _, err := os.Stat(".envoy-version"); os.IsNotExist(err) {
+		return os.WriteFile(filepath.Join(homeDir, "version"), []byte(v), 0600)
+	} else if err != nil {
+		return err
+	}
+	return os.WriteFile(".envoy-version", []byte(v), 0600)
+}
+
 // CurrentVersion returns the first version in priority of VersionUsageList and its source or an error. The "source"
 // and error messages returned include unexpanded variables to clarify the intended context.
 func CurrentVersion(homeDir string) (v, source string, err error) {
@@ -68,13 +81,13 @@ func getCurrentVersion(homeDir string) (v, source string, err error) {
 	// Priority 2: $PWD/.envoy-version
 	data, err := os.ReadFile(".envoy-version")
 	if err == nil {
-		return string(data), currentVersionWorkingDirFile, nil
+		return trimNewline(data), CurrentVersionWorkingDirFile, nil
 	} else if !os.IsNotExist(err) {
-		return "", currentVersionWorkingDirFile, err
+		return "", CurrentVersionWorkingDirFile, err
 	}
 
 	// Priority 3: $GETENVOY_HOME/version
-	source = currentVersionHomeDirFile
+	source = CurrentVersionHomeDirFile
 	v, _, err = getHomeVersion(homeDir)
 	return
 }
@@ -83,7 +96,7 @@ func getHomeVersion(homeDir string) (v, homeVersionFile string, err error) {
 	homeVersionFile = filepath.Join(homeDir, "version")
 	var data []byte
 	if data, err = os.ReadFile(homeVersionFile); err == nil {
-		v = string(data)
+		v = trimNewline(data)
 	} else if os.IsNotExist(err) {
 		err = nil // ok on file-not-found
 	}
@@ -93,5 +106,10 @@ func getHomeVersion(homeDir string) (v, homeVersionFile string, err error) {
 // VersionUsageList is the priority order of Envoy version sources.
 // This includes unresolved variables as it is both used statically for markdown generation, and also at runtime.
 func VersionUsageList() string {
-	return strings.Join([]string{currentVersionVar, currentVersionWorkingDirFile, currentVersionHomeDirFile}, ", ")
+	return strings.Join([]string{currentVersionVar, CurrentVersionWorkingDirFile, CurrentVersionHomeDirFile}, ", ")
+}
+
+// trimNewline allows assignments like `echo 1.15.5 > $GETENVOY_HOME/version`
+func trimNewline(data []byte) string {
+	return strings.TrimSuffix(string(data), "\n")
 }


Test patch:
diff --git a/e2e/getenvoy_install_test.go b/e2e/getenvoy_use_test.go
similarity index 71%
rename from e2e/getenvoy_install_test.go
rename to e2e/getenvoy_use_test.go
index 5fa7608..417f0e8 100644
--- a/e2e/getenvoy_install_test.go
+++ b/e2e/getenvoy_use_test.go
@@ -16,6 +16,7 @@ package e2e
 
 import (
 	"fmt"
+	"os"
 	"path/filepath"
 	"runtime"
 	"testing"
@@ -26,23 +27,29 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
-// TestGetEnvoyInstall needs to always execute, so we run it in a separate home directory
-func TestGetEnvoyInstall(t *testing.T) {
+// TestGetEnvoyUse needs to always execute, so we run it in a separate home directory
+func TestGetEnvoyUse(t *testing.T) {
 	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
 	defer removeHomeDir()
 
 	t.Run("not yet installed", func(t *testing.T) {
-		stdout, stderr, err := getEnvoy("--home-dir", homeDir, "install", version.LastKnownEnvoy).exec()
+		stdout, stderr, err := getEnvoy("--home-dir", homeDir, "use", version.LastKnownEnvoy).exec()
 
 		require.NoError(t, err)
 		require.Regexp(t, `^downloading https:.*tar.*z\n$`, stdout)
 		require.Empty(t, stderr)
 
+		// The binary was installed
 		require.FileExists(t, filepath.Join(homeDir, "versions", version.LastKnownEnvoy, "bin", "envoy"))
+
+		// The current version was written
+		f, err := os.ReadFile(filepath.Join(homeDir, "version"))
+		require.NoError(t, err)
+		require.Equal(t, version.LastKnownEnvoy, string(f))
 	})
 
 	t.Run("already installed", func(t *testing.T) {
-		stdout, stderr, err := getEnvoy("--home-dir", homeDir, "install", version.LastKnownEnvoy).exec()
+		stdout, stderr, err := getEnvoy("--home-dir", homeDir, "use", version.LastKnownEnvoy).exec()
 
 		require.NoError(t, err)
 		require.Equal(t, version.LastKnownEnvoy+" is already downloaded\n", stdout)
@@ -50,8 +57,8 @@ func TestGetEnvoyInstall(t *testing.T) {
 	})
 }
 
-func TestGetEnvoyInstall_UnknownVersion(t *testing.T) {
-	stdout, stderr, err := getEnvoy("install", "1.1.1").exec()
+func TestGetEnvoyUse_UnknownVersion(t *testing.T) {
+	stdout, stderr, err := getEnvoy("use", "1.1.1").exec()
 
 	require.EqualError(t, err, "exit status 1")
 	require.Empty(t, stdout)
diff --git a/e2e/getenvoy_versions_test.go b/e2e/getenvoy_versions_test.go
index e1287e7..b836c1d 100644
--- a/e2e/getenvoy_versions_test.go
+++ b/e2e/getenvoy_versions_test.go
@@ -33,8 +33,7 @@ func TestGetEnvoyVersions_NothingYet(t *testing.T) {
 	stdout, stderr, err := getEnvoy("--home-dir", homeDir, "versions").exec()
 
 	require.NoError(t, err)
-	require.Equal(t, `No Envoy versions, yet
-`, stdout)
+	require.Empty(t, stdout)
 	require.Empty(t, stderr)
 }
 
@@ -43,8 +42,7 @@ func TestGetEnvoyVersions(t *testing.T) {
 
 	stdout, stderr, err := getEnvoy("versions").exec()
 
-	require.Regexp(t, "^VERSION\tRELEASE_DATE\n", stdout)
-	require.Regexp(t, fmt.Sprintf("%s\t202[1-9]-[01][0-9]-[0-3][0-9]\n", version.LastKnownEnvoy), stdout)
+	require.Regexp(t, fmt.Sprintf("[ *] %s 202[1-9]-[01][0-9]-[0-3][0-9].*\n", version.LastKnownEnvoy), stdout)
 	require.Empty(t, stderr)
 	require.NoError(t, err)
 }
@@ -54,8 +52,7 @@ func TestGetEnvoyVersions_All(t *testing.T) {
 
 	stdout, stderr, err := getEnvoy("versions", "-a").exec()
 
-	require.Regexp(t, "^VERSION\tRELEASE_DATE\n", stdout)
-	require.Regexp(t, fmt.Sprintf("%s\t202[1-9]-[01][0-9]-[0-3][0-9]\n", version.LastKnownEnvoy), stdout)
+	require.Regexp(t, fmt.Sprintf("[ *] %s 202[1-9]-[01][0-9]-[0-3][0-9].*\n", version.LastKnownEnvoy), stdout)
 	require.Empty(t, stderr)
 	require.NoError(t, err)
 }
diff --git a/internal/cmd/app_test.go b/internal/cmd/app_test.go
index 315a317..530a439 100644
--- a/internal/cmd/app_test.go
+++ b/internal/cmd/app_test.go
@@ -212,6 +212,7 @@ func newApp(o *globals.GlobalOpts) (c *cli.App, stdout, stderr *bytes.Buffer) {
 	c.Name = "getenvoy"
 	c.Writer = stdout
 	c.ErrWriter = stderr
+	o.Out = stdout
 	return
 }
 
diff --git a/internal/cmd/help_test.go b/internal/cmd/help_test.go
index 4216499..a5462aa 100644
--- a/internal/cmd/help_test.go
+++ b/internal/cmd/help_test.go
@@ -28,7 +28,7 @@ import (
 )
 
 func TestGetEnvoyHelp(t *testing.T) {
-	for _, command := range []string{"", "install", "versions", "run"} {
+	for _, command := range []string{"", "use", "versions", "run"} {
 		command := command
 		t.Run(command, func(t *testing.T) {
 			c, stdout, _ := newApp(&globals.GlobalOpts{})
diff --git a/internal/cmd/run_test.go b/internal/cmd/run_test.go
index a494cb7..20f518a 100644
--- a/internal/cmd/run_test.go
+++ b/internal/cmd/run_test.go
@@ -17,6 +17,7 @@ package cmd_test
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"os"
 	"path/filepath"
 	"testing"
@@ -53,6 +54,7 @@ func TestGetEnvoyRun(t *testing.T) {
 			defer cleanup()
 
 			c, stdout, stderr := newApp(o)
+			o.Out = io.Discard // don't verify logging
 			err := c.Run(test.args)
 
 			// Verify the command invoked, passing the correct default commandline
@@ -124,20 +126,15 @@ func TestGetEnvoyRun_ValidatesHomeVersion(t *testing.T) {
 func TestGetEnvoyRun_ValidatesWorkingVersion(t *testing.T) {
 	o, cleanup := setupTest(t)
 	o.Out = new(bytes.Buffer)
-	defer cleanup()
-
 	o.EnvoyVersion = ""
-	wd, err := os.Getwd()
-	require.NoError(t, err)
-	defer os.Chdir(wd) //nolint
+	defer cleanup()
 
-	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
-	defer removeTempDir()
-	require.NoError(t, os.Chdir(tempDir))
+	revertTempWd := morerequire.RequireChdirIntoTemp(t)
+	defer revertTempWd()
 	require.NoError(t, os.WriteFile(".envoy-version", []byte("b.b.b"), 0600))
 
 	c, _, _ := newApp(o)
-	err = c.Run([]string{"getenvoy", "run"})
+	err := c.Run([]string{"getenvoy", "run"})
 
 	// Verify the command failed with the expected error
 	require.EqualError(t, err, fmt.Sprintf(`invalid version in "$PWD/.envoy-version": "b.b.b" should look like "%s"`, version.LastKnownEnvoy))
diff --git a/internal/cmd/testdata/getenvoy.md b/internal/cmd/testdata/getenvoy.md
index d23b687..95d0a25 100644
--- a/internal/cmd/testdata/getenvoy.md
+++ b/internal/cmd/testdata/getenvoy.md
@@ -43,7 +43,7 @@ List Envoy versions
 
 **--all, -a**: Show all versions including ones not yet installed
 
-## install
+## use
 
-Download and install a [version] of Envoy
+Sets the current [version] used by the "run" command, installing as necessary
 
diff --git a/internal/cmd/testdata/getenvoy_help.txt b/internal/cmd/testdata/getenvoy_help.txt
index 6d76288..4050913 100644
--- a/internal/cmd/testdata/getenvoy_help.txt
+++ b/internal/cmd/testdata/getenvoy_help.txt
@@ -11,7 +11,7 @@ COMMANDS:
    help      Shows how to use a [command]
    run       Run Envoy with the given [arguments...], collecting process state on termination
    versions  List Envoy versions
-   install   Download and install a [version] of Envoy
+   use       Sets the current [version] used by the "run" command, installing as necessary
 
 GLOBAL OPTIONS:
    --home-dir value            GetEnvoy home directory (location of installed versions and run archives) (default: ${HOME}/.getenvoy) [$GETENVOY_HOME]
diff --git a/internal/cmd/testdata/getenvoy_install_help.txt b/internal/cmd/testdata/getenvoy_install_help.txt
deleted file mode 100644
index a068178..0000000
--- a/internal/cmd/testdata/getenvoy_install_help.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-NAME:
-   getenvoy install - Download and install a [version] of Envoy
-
-USAGE:
-   getenvoy install [version]
-
-DESCRIPTION:
-   The '[version]' is from the "versions" command.
-   The Envoy [version] will be installed into $GETENVOY_HOME/versions/[version]
-   
-   Example:
-   $ getenvoy install 1.18.3
diff --git a/internal/cmd/testdata/getenvoy_run_help.txt b/internal/cmd/testdata/getenvoy_run_help.txt
index caefb8d..1387f82 100644
--- a/internal/cmd/testdata/getenvoy_run_help.txt
+++ b/internal/cmd/testdata/getenvoy_run_help.txt
@@ -5,8 +5,10 @@ USAGE:
    getenvoy run [arguments...]
 
 DESCRIPTION:
-   The version of Envoy run is chosen from $ENVOY_VERSION, $PWD/.envoy-version, $GETENVOY_HOME/version
-   The '[arguments...]' are interpreted by Envoy.
+   Envoy interprets the '[arguments...]'.
+   
+   The "use" command primarily controls the version. The first version in $ENVOY_VERSION, $PWD/.envoy-version, $GETENVOY_HOME/version is
+   run, installing on-demand as needed.
    
    Envoy uses $GETENVOY_HOME/runs/$epochtime as the working directory.
    Upon termination, this is archived as $GETENVOY_HOME/runs/$epochtime.tar.gz.
diff --git a/internal/cmd/testdata/getenvoy_use_help.txt b/internal/cmd/testdata/getenvoy_use_help.txt
new file mode 100644
index 0000000..e12b3e3
--- /dev/null
+++ b/internal/cmd/testdata/getenvoy_use_help.txt
@@ -0,0 +1,16 @@
+NAME:
+   getenvoy use - Sets the current [version] used by the "run" command, installing as necessary
+
+USAGE:
+   getenvoy use [version]
+
+DESCRIPTION:
+   The '[version]' is from the "versions -a" command.
+   The Envoy [version] installs on-demand into $GETENVOY_HOME/versions/[version]
+   if needed.
+   
+   This updates $PWD/.envoy-version or $GETENVOY_HOME/version with [version],
+   depending on which is present.
+   
+   Example:
+   $ getenvoy use {ENVOY_VERSION}
diff --git a/internal/cmd/install_test.go b/internal/cmd/use_test.go
similarity index 73%
rename from internal/cmd/install_test.go
rename to internal/cmd/use_test.go
index a39dddd..7166c87 100644
--- a/internal/cmd/install_test.go
+++ b/internal/cmd/use_test.go
@@ -22,11 +22,10 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/tetratelabs/getenvoy/internal/test"
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
-func TestGetEnvoyInstall_VersionValidates(t *testing.T) {
+func TestGetEnvoyUse_VersionValidates(t *testing.T) {
 	o, cleanup := setupTest(t)
 	defer cleanup()
 
@@ -47,7 +46,7 @@ func TestGetEnvoyInstall_VersionValidates(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			c, stdout, stderr := newApp(o)
-			err := c.Run([]string{"getenvoy", "install", tc.version})
+			err := c.Run([]string{"getenvoy", "use", tc.version})
 
 			// Verify the command failed with the expected error
 			require.EqualError(t, err, tc.expectedErr)
@@ -58,19 +57,18 @@ func TestGetEnvoyInstall_VersionValidates(t *testing.T) {
 	}
 }
 
-func TestGetEnvoyInstall_PreservesReleaseDate(t *testing.T) {
+func TestGetEnvoyUse_InstallsAndWritesHomeVersion(t *testing.T) {
 	o, cleanup := setupTest(t)
 	defer cleanup()
 
 	c, _, _ := newApp(o)
-	require.NoError(t, c.Run([]string{"getenvoy", "install", o.EnvoyVersion}))
+	require.NoError(t, c.Run([]string{"getenvoy", "use", o.EnvoyVersion}))
 
-	// The directory was created
-	versionDir := filepath.Join(o.HomeDir, "versions", o.EnvoyVersion)
-	require.DirExists(t, versionDir)
+	// The binary was installed
+	require.FileExists(t, filepath.Join(o.HomeDir, "versions", o.EnvoyVersion, "bin", "envoy"))
 
-	// The directory timestamp matches the fake release date, not the current time
-	f, err := os.Stat(versionDir)
+	// The current version was written
+	f, err := os.ReadFile(filepath.Join(o.HomeDir, "version"))
 	require.NoError(t, err)
-	require.Equal(t, f.ModTime().Format("2006-01-02"), test.FakeReleaseDate)
+	require.Equal(t, o.EnvoyVersion, string(f))
 }
diff --git a/internal/cmd/versions_cmd_test.go b/internal/cmd/versions_cmd_test.go
index 237c0ae..00b5d47 100644
--- a/internal/cmd/versions_cmd_test.go
+++ b/internal/cmd/versions_cmd_test.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 
+	"github.com/tetratelabs/getenvoy/internal/globals"
 	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
@@ -31,76 +32,160 @@ func TestGetEnvoyVersions_NothingYet(t *testing.T) {
 	defer cleanup()
 
 	c, stdout, stderr := newApp(o)
-	o.Out = stdout
 	err := c.Run([]string{"getenvoy", "versions"})
 
 	require.NoError(t, err)
-	require.Equal(t, "No Envoy versions, yet\n", stdout.String())
+	require.Empty(t, stdout) // allows consistent parsing even when nothing yet installed
 	require.Empty(t, stderr)
 }
 
-func TestGetEnvoyVersions_Sorted(t *testing.T) {
-	o, cleanup := setupTest(t)
+func TestGetEnvoyVersions_NoCurrentVersion(t *testing.T) {
+	o, cleanup := setupTestVersions(t)
 	defer cleanup()
+	require.NoError(t, os.Remove(filepath.Join(o.HomeDir, "version")))
 
-	oneOneTwo := filepath.Join(o.HomeDir, "versions", "1.1.2")
-	require.NoError(t, os.MkdirAll(oneOneTwo, 0700))
-	morerequire.RequireSetMtime(t, oneOneTwo, "2020-12-31")
+	c, stdout, stderr := newApp(o)
+	err := c.Run([]string{"getenvoy", "versions"})
 
-	oneTwoOne := filepath.Join(o.HomeDir, "versions", "1.2.1")
-	require.NoError(t, os.MkdirAll(oneTwoOne, 0700))
-	morerequire.RequireSetMtime(t, oneTwoOne, "2020-12-30")
+	require.NoError(t, err)
+	require.Equal(t, `  1.2.2 2021-01-31
+  1.1.2 2021-01-31
+  1.2.1 2021-01-30
+`, stdout.String())
+	require.Empty(t, stderr)
+}
 
-	oneTwoTwo := filepath.Join(o.HomeDir, "versions", "1.2.2")
-	require.NoError(t, os.MkdirAll(oneTwoTwo, 0700))
-	morerequire.RequireSetMtime(t, oneTwoTwo, "2020-12-31")
+// TestGetEnvoyVersions_CurrentVersion tests depend on prior state, so execute sequentially. This doesn't use a matrix
+// to improve readability
+func TestGetEnvoyVersions_CurrentVersion(t *testing.T) {
+	o, cleanup := setupTestVersions(t)
+	defer cleanup()
+
+	t.Run("no current version", func(t *testing.T) {
+		require.NoError(t, os.Remove(filepath.Join(o.HomeDir, "version")))
+
+		c, stdout, _ := newApp(o)
+		require.NoError(t, c.Run([]string{"getenvoy", "versions"}))
+		require.Equal(t, `  1.2.2 2021-01-31
+  1.1.2 2021-01-31
+  1.2.1 2021-01-30
+`, stdout.String())
+	})
+
+	t.Run("set by $GETENVOY_HOME/version", func(t *testing.T) {
+		require.NoError(t, os.WriteFile(filepath.Join(o.HomeDir, "version"), []byte("1.1.2"), 0600))
+
+		c, stdout, _ := newApp(o)
+		require.NoError(t, c.Run([]string{"getenvoy", "versions"}))
+		require.Equal(t, `  1.2.2 2021-01-31
+* 1.1.2 2021-01-31 (set by $GETENVOY_HOME/version)
+  1.2.1 2021-01-30
+`, stdout.String())
+	})
+
+	t.Run("set by $PWD/.envoy-version", func(t *testing.T) {
+		revertTempWd := morerequire.RequireChdirIntoTemp(t)
+		defer revertTempWd()
+		require.NoError(t, os.WriteFile(".envoy-version", []byte("1.2.2"), 0600))
+
+		c, stdout, _ := newApp(o)
+		require.NoError(t, c.Run([]string{"getenvoy", "versions"}))
+		require.Equal(t, `* 1.2.2 2021-01-31 (set by $PWD/.envoy-version)
+  1.1.2 2021-01-31
+  1.2.1 2021-01-30
+`, stdout.String())
+	})
+
+	t.Run("set by $ENVOY_VERSION", func(t *testing.T) {
+		revertEnv := morerequire.RequireSetenv(t, "ENVOY_VERSION", "1.2.1")
+		defer revertEnv()
+
+		c, stdout, _ := newApp(o)
+		require.NoError(t, c.Run([]string{"getenvoy", "versions"}))
+		require.Equal(t, `  1.2.2 2021-01-31
+  1.1.2 2021-01-31
+* 1.2.1 2021-01-30 (set by $ENVOY_VERSION)
+`, stdout.String())
+	})
+}
+
+func TestGetEnvoyVersions_Sorted(t *testing.T) {
+	o, cleanup := setupTestVersions(t)
+	defer cleanup()
 
 	c, stdout, stderr := newApp(o)
-	o.Out = stdout
 	err := c.Run([]string{"getenvoy", "versions"})
 
 	require.NoError(t, err)
-	require.Equal(t, `VERSION	RELEASE_DATE
-1.2.2	2020-12-31
-1.1.2	2020-12-31
-1.2.1	2020-12-30
+	require.Equal(t, `  1.2.2 2021-01-31
+  1.1.2 2021-01-31
+* 1.2.1 2021-01-30 (set by $GETENVOY_HOME/version)
 `, stdout.String())
 	require.Empty(t, stderr)
 }
 
-func TestGetEnvoyVersions_All_IncludesRemote(t *testing.T) {
+func TestGetEnvoyVersions_All_OnlyRemote(t *testing.T) {
 	o, cleanup := setupTest(t)
 	defer cleanup()
 
-	// Run "getenvoy versions"
 	c, stdout, stderr := newApp(o)
-	o.Out = stdout
 	err := c.Run([]string{"getenvoy", "versions", "-a"})
 
 	require.NoError(t, err)
-	require.Equal(t, fmt.Sprintf(`VERSION	RELEASE_DATE
-%s	2020-12-31
-`, version.LastKnownEnvoy), stdout.String())
+	require.Equal(t, fmt.Sprintf("  %s 2020-12-31\n", version.LastKnownEnvoy), stdout.String())
 	require.Empty(t, stderr)
 }
 
-func TestGetEnvoyVersions_All_Mixed(t *testing.T) {
+func TestGetEnvoyVersions_All_RemoteIsCurrent(t *testing.T) {
 	o, cleanup := setupTest(t)
 	defer cleanup()
 
-	oneOneTwo := filepath.Join(o.HomeDir, "versions", "1.1.2")
-	require.NoError(t, os.MkdirAll(oneOneTwo, 0700))
-	morerequire.RequireSetMtime(t, oneOneTwo, "2020-12-31")
+	versionDir := filepath.Join(o.HomeDir, "versions", version.LastKnownEnvoy)
+	require.NoError(t, os.MkdirAll(versionDir, 0700))
+	morerequire.RequireSetMtime(t, versionDir, "2020-12-31")
+	require.NoError(t, os.WriteFile(filepath.Join(o.HomeDir, "version"), []byte(version.LastKnownEnvoy), 0600))
+
+	expected := fmt.Sprintf("* %s 2020-12-31 (set by $GETENVOY_HOME/version)\n", version.LastKnownEnvoy)
 
-	// Run "getenvoy versions"
 	c, stdout, stderr := newApp(o)
-	o.Out = stdout
 	err := c.Run([]string{"getenvoy", "versions", "-a"})
 
 	require.NoError(t, err)
-	require.Equal(t, fmt.Sprintf(`VERSION	RELEASE_DATE
-%s	2020-12-31
-1.1.2	2020-12-31
+	require.Equal(t, expected, stdout.String())
+	require.Empty(t, stderr)
+}
+
+func TestGetEnvoyVersions_All_Mixed(t *testing.T) {
+	o, cleanup := setupTestVersions(t)
+	defer cleanup()
+
+	c, stdout, stderr := newApp(o)
+	err := c.Run([]string{"getenvoy", "versions", "-a"})
+
+	require.NoError(t, err)
+	require.Equal(t, fmt.Sprintf(`  1.2.2 2021-01-31
+  1.1.2 2021-01-31
+* 1.2.1 2021-01-30 (set by $GETENVOY_HOME/version)
+  %s 2020-12-31
 `, version.LastKnownEnvoy), stdout.String())
 	require.Empty(t, stderr)
 }
+
+func setupTestVersions(t *testing.T) (o *globals.GlobalOpts, cleanup func()) {
+	o, cleanup = setupTest(t)
+
+	oneOneTwo := filepath.Join(o.HomeDir, "versions", "1.1.2")
+	require.NoError(t, os.MkdirAll(oneOneTwo, 0700))
+	morerequire.RequireSetMtime(t, oneOneTwo, "2021-01-31")
+
+	// Set the middle version current
+	oneTwoOne := filepath.Join(o.HomeDir, "versions", "1.2.1")
+	require.NoError(t, os.MkdirAll(oneTwoOne, 0700))
+	morerequire.RequireSetMtime(t, oneTwoOne, "2021-01-30")
+	require.NoError(t, os.WriteFile(filepath.Join(o.HomeDir, "version"), []byte("1.2.1"), 0600))
+
+	oneTwoTwo := filepath.Join(o.HomeDir, "versions", "1.2.2")
+	require.NoError(t, os.MkdirAll(oneTwoTwo, 0700))
+	morerequire.RequireSetMtime(t, oneTwoTwo, "2021-01-31")
+	return
+}
diff --git a/internal/envoy/install_test.go b/internal/envoy/install_test.go
index 4497913..2d49d34 100644
--- a/internal/envoy/install_test.go
+++ b/internal/envoy/install_test.go
@@ -23,6 +23,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -134,6 +135,12 @@ func TestInstallIfNeeded(t *testing.T) {
 	require.Equal(t, o.EnvoyPath, envoyPath)
 	require.FileExists(t, envoyPath)
 
+	// The version directory timestamp matches the fake release date, not the current time
+	versionDir := strings.Replace(envoyPath, binEnvoy, "", 1)
+	f, err := os.Stat(versionDir)
+	require.NoError(t, err)
+	require.Equal(t, f.ModTime().Format("2006-01-02"), test.FakeReleaseDate)
+
 	require.Equal(t, fmt.Sprintln("downloading", o.tarballURL), out.String())
 }
 
diff --git a/internal/envoy/version_test.go b/internal/envoy/version_test.go
index b68f564..63d6d18 100644
--- a/internal/envoy/version_test.go
+++ b/internal/envoy/version_test.go
@@ -30,51 +30,110 @@ func TestVersionUsageList(t *testing.T) {
 	require.Equal(t, "$ENVOY_VERSION, $PWD/.envoy-version, $GETENVOY_HOME/version", VersionUsageList())
 }
 
+func TestGetHomeVersion_Empty(t *testing.T) {
+	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
+	defer removeHomeDir()
+
+	homeVersionFile := filepath.Join(homeDir, "version")
+
+	for _, tt := range []struct {
+		name  string
+		setup func()
+	}{
+		{"empty home version file", func() {
+			require.NoError(t, os.WriteFile(homeVersionFile, []byte(""), 0600))
+		}},
+		{"missing home version file", func() {
+			require.NoError(t, os.Remove(homeVersionFile))
+		}},
+		{"missing home dir", removeHomeDir},
+	} {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			v, hvf, err := GetHomeVersion(homeDir)
+			require.Empty(t, v)
+			require.Equal(t, homeVersionFile, hvf)
+			require.NoError(t, err)
+		})
+	}
+}
+
+func TestTrimNewline(t *testing.T) {
+	require.Equal(t, "", trimNewline([]byte{}))
+	require.Equal(t, "", trimNewline([]byte("\n")))
+	require.Equal(t, "1.15.5", trimNewline([]byte("1.15.5")))
+	require.Equal(t, "1.15.5", trimNewline([]byte("1.15.5\n")))
+}
+
 func TestGetHomeVersion(t *testing.T) {
 	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
-	removeHomeDir() // delete the home directory
+	defer removeHomeDir()
 
-	t.Run("doesn't error on no home dir", func(t *testing.T) {
-		v, homeVersionFile, err := GetHomeVersion(homeDir)
-		require.Empty(t, v)
-		require.Equal(t, filepath.Join(homeDir, "version"), homeVersionFile)
-		require.NoError(t, err)
-	})
+	homeVersionFile := filepath.Join(homeDir, "version")
+	require.NoError(t, os.WriteFile(homeVersionFile, []byte(version.LastKnownEnvoy), 0600))
+
+	v, hvf, err := GetHomeVersion(homeDir)
+	require.Equal(t, version.LastKnownEnvoy, v)
+	require.Equal(t, homeVersionFile, hvf)
+	require.NoError(t, err)
+}
 
-	homeDir, removeHomeDir = morerequire.RequireNewTempDir(t)
+func TestGetHomeVersion_Validates(t *testing.T) {
+	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
 	defer removeHomeDir()
 
-	t.Run("doesn't error on no home version file", func(t *testing.T) {
-		v, homeVersionFile, err := GetHomeVersion(homeDir)
-		require.Empty(t, v)
-		require.Equal(t, filepath.Join(homeDir, "version"), homeVersionFile)
-		require.NoError(t, err)
-	})
+	homeVersionFile := filepath.Join(homeDir, "version")
+	require.NoError(t, os.WriteFile(homeVersionFile, []byte("a.a.a"), 0600))
+
+	_, _, err := GetHomeVersion(homeDir)
+	require.EqualError(t, err, fmt.Sprintf(`invalid version in "$GETENVOY_HOME/version": "a.a.a" should look like "%s"`, version.LastKnownEnvoy))
+}
+
+func TestWriteCurrentVersion_HomeDir(t *testing.T) {
+	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
+	defer removeHomeDir()
+
+	for _, tt := range []struct{ name, v string }{
+		{"writes initial home version", "1.1.1"},
+		{"overwrites home version", "2.2.2"},
+	} {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			require.NoError(t, WriteCurrentVersion(tc.v, homeDir))
+			v, src, err := getCurrentVersion(homeDir)
+			require.NoError(t, err)
+			require.Equal(t, tc.v, v)
+			require.Equal(t, CurrentVersionHomeDirFile, src)
+			require.NoFileExists(t, ".envoy-version")
+		})
+	}
+}
+
+func TestWriteCurrentVersion_OverwritesWorkingDirVersion(t *testing.T) {
+	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
+	defer removeHomeDir()
 
 	homeVersionFile := filepath.Join(homeDir, "version")
-	require.NoError(t, os.WriteFile(homeVersionFile, []byte(""), 0600))
+	require.NoError(t, os.WriteFile(homeVersionFile, []byte("1.1.1"), 0600))
 
-	// This could be considered unexpected, but it is less to code to overwrite on this edge-case
-	t.Run("doesn't error on no empty home version file", func(t *testing.T) {
-		v, _, err := GetHomeVersion(homeDir)
-		require.Empty(t, v)
-		require.NoError(t, err)
-	})
+	revertTempWd := morerequire.RequireChdirIntoTemp(t)
+	defer revertTempWd()
+	require.NoError(t, os.WriteFile(".envoy-version", []byte("2.2.2"), 0600))
 
-	require.NoError(t, os.WriteFile(homeVersionFile, []byte(version.LastKnownEnvoy), 0600))
-	t.Run("reads valid home version file", func(t *testing.T) {
-		v, _, err := GetHomeVersion(homeDir)
-		require.Equal(t, version.LastKnownEnvoy, v)
-		require.NoError(t, err)
-	})
+	require.NoError(t, WriteCurrentVersion("3.3.3", homeDir))
+	v, src, err := getCurrentVersion(homeDir)
+	require.NoError(t, err)
+	require.Equal(t, "3.3.3", v)
+	require.Equal(t, CurrentVersionWorkingDirFile, src)
 
-	require.NoError(t, os.WriteFile(homeVersionFile, []byte("a.a.a"), 0600))
-	t.Run("errors on invalid home version file", func(t *testing.T) {
-		_, _, err := GetHomeVersion(homeDir)
-		require.EqualError(t, err, fmt.Sprintf(`invalid version in "$GETENVOY_HOME/version": "a.a.a" should look like "%s"`, version.LastKnownEnvoy))
-	})
+	// didn't overwrite the home version
+	v, _, err = getHomeVersion(homeDir)
+	require.NoError(t, err)
+	require.Equal(t, "1.1.1", v)
 }
 
+// TestCurrentVersion is intentionally written in priority order instead of via a matrix. This particularly helps with
+// test setup complexity required to ensure tiered priority (ex layering overridden PWD with an ENV)
 func TestCurrentVersion(t *testing.T) {
 	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
 	defer removeHomeDir()
@@ -87,13 +146,8 @@ func TestCurrentVersion(t *testing.T) {
 		require.NoError(t, err)
 	})
 
-	wd, err := os.Getwd()
-	require.NoError(t, err)
-	defer os.Chdir(wd) //nolint
-
-	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
-	defer removeTempDir()
-	require.NoError(t, os.Chdir(tempDir))
+	revertTempWd := morerequire.RequireChdirIntoTemp(t)
+	defer revertTempWd()
 	require.NoError(t, os.WriteFile(".envoy-version", []byte("2.2.2"), 0600))
 
 	t.Run("prefers $PWD/.envoy-version over home version", func(t *testing.T) {
@@ -114,6 +168,7 @@ func TestCurrentVersion(t *testing.T) {
 	})
 }
 
+// TestCurrentVersion_Validates is intentionally written in priority order instead of via a matrix
 func TestCurrentVersion_Validates(t *testing.T) {
 	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
 	defer removeHomeDir()
@@ -124,13 +179,8 @@ func TestCurrentVersion_Validates(t *testing.T) {
 		require.EqualError(t, err, fmt.Sprintf(`invalid version in "$GETENVOY_HOME/version": "a.a.a" should look like "%s"`, version.LastKnownEnvoy))
 	})
 
-	wd, err := os.Getwd()
-	require.NoError(t, err)
-	defer os.Chdir(wd) //nolint
-
-	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
-	defer removeTempDir()
-	require.NoError(t, os.Chdir(tempDir))
+	revertTempWd := morerequire.RequireChdirIntoTemp(t)
+	defer revertTempWd()
 	require.NoError(t, os.WriteFile(".envoy-version", []byte("b.b.b"), 0600))
 
 	t.Run("validates $PWD/.envoy-version", func(t *testing.T) {
diff --git a/internal/test/morerequire/morerequire.go b/internal/test/morerequire/morerequire.go
index a79ce70..c5a833c 100644
--- a/internal/test/morerequire/morerequire.go
+++ b/internal/test/morerequire/morerequire.go
@@ -72,3 +72,25 @@ func RequireSetenv(t *testing.T, key, value string) func() {
 		require.NoError(t, err, `error reverting env variable %s=%s`, key, previous)
 	}
 }
+
+// RequireChdirIntoTemp creates a new temp directory and cleans it up on the returned function.
+func RequireChdirIntoTemp(t *testing.T) (cleanup func()) {
+	var cleanups []func()
+	cleanup = func() {
+		for i := len(cleanups) - 1; i >= 0; i-- {
+			cleanups[i]()
+		}
+	}
+
+	wd, err := os.Getwd()
+	require.NoError(t, err)
+	cleanups = append(cleanups, func() {
+		require.NoError(t, os.Chdir(wd))
+	})
+
+	tempDir, removeTempDir := RequireNewTempDir(t)
+	cleanups = append(cleanups, removeTempDir)
+
+	require.NoError(t, os.Chdir(tempDir))
+	return
+}