refactor: OO pattern

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
diff --git a/BUILD.bazel b/BUILD.bazel
index f2c95ba..6cf9e68 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -10,6 +10,7 @@
     args = [
         "-from_file=go.mod",
         "-to_macro=go.bzl%deps",
+        "-prune",
     ],
     command = "update-repos",
 )
diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel
deleted file mode 100644
index 966283f..0000000
--- a/bazel/BUILD.bazel
+++ /dev/null
@@ -1,15 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-
-go_library(
-    name = "bazel",
-    srcs = [
-        "flags.go",
-        "spawn.go",
-    ],
-    importpath = "aspect.build/cli/bazel",
-    visibility = ["//visibility:public"],
-    deps = [
-        "@com_github_bazelbuild_bazelisk//core:go_default_library",
-        "@com_github_bazelbuild_bazelisk//repositories:go_default_library",
-    ],
-)
diff --git a/bazel/flags.go b/bazel/flags.go
deleted file mode 100644
index 93122b6..0000000
--- a/bazel/flags.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package bazel
-
-import ()
-
diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go
index 01ab16b..2e1d339 100644
--- a/buildinfo/buildinfo.go
+++ b/buildinfo/buildinfo.go
@@ -1,8 +1,13 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
 // Variables in this file will be replaced by the linker when Bazel is run with --stamp
 // The time should be in format '2018-12-12 12:30:00 UTC'
 // The GitStatus should be either "clean" or "dirty"
 // Release will be a comma-separated string representation of any tags.
-
 package buildinfo
 
 // BuildTime is a string representation of when this binary was built.
diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel
deleted file mode 100644
index fe8413e..0000000
--- a/cmd/BUILD.bazel
+++ /dev/null
@@ -1,30 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-
-go_library(
-    name = "cmd",
-    srcs = [
-        "root.go",
-        "version.go",
-    ],
-    importpath = "aspect.build/cli/cmd",
-    visibility = ["//visibility:public"],
-    deps = [
-        "//bazel",
-        "//buildinfo",
-        "@com_github_fatih_color//:color",
-        "@com_github_mattn_go_isatty//:go-isatty",
-        "@com_github_mitchellh_go_homedir//:go-homedir",
-        "@com_github_spf13_cobra//:cobra",
-        "@com_github_spf13_viper//:viper",
-    ],
-)
-
-go_test(
-    name = "cmd_test",
-    srcs = ["version_test.go"],
-    embed = [":cmd"],
-    deps = [
-        "@com_github_spf13_cobra//:cobra",
-        "@com_github_stretchr_testify//assert",
-    ],
-)
diff --git a/cmd/aspect/BUILD.bazel b/cmd/aspect/BUILD.bazel
index ce6196f..4e8b33d 100644
--- a/cmd/aspect/BUILD.bazel
+++ b/cmd/aspect/BUILD.bazel
@@ -2,10 +2,21 @@
 
 go_library(
     name = "aspect_lib",
-    srcs = ["main.go"],
+    srcs = [
+        "main.go",
+        "root.go",
+    ],
     importpath = "aspect.build/cli/cmd/aspect",
     visibility = ["//visibility:private"],
-    deps = ["//cmd"],
+    deps = [
+        "//cmd/aspect/version",
+        "//pkg/ioutils",
+        "@com_github_fatih_color//:color",
+        "@com_github_mattn_go_isatty//:go-isatty",
+        "@com_github_mitchellh_go_homedir//:go-homedir",
+        "@com_github_spf13_cobra//:cobra",
+        "@com_github_spf13_viper//:viper",
+    ],
 )
 
 go_binary(
diff --git a/cmd/aspect/main.go b/cmd/aspect/main.go
index 837f711..8ee6f4d 100644
--- a/cmd/aspect/main.go
+++ b/cmd/aspect/main.go
@@ -1,6 +1,12 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
 package main
 
-import "aspect.build/cli/cmd"
+import "github.com/spf13/cobra"
 
 func main() {
 	// Detect whether we are being run as a tools/bazel wrapper (look for BAZEL_REAL in the environment)
@@ -14,5 +20,6 @@
 	//     ask the user if they want to install for all users of the workspace, if so
 	//         - tools/bazel file and put our bootstrap code in there
 	//
-	cmd.Execute()
+	cmd := NewDefaultRootCmd()
+	cobra.CheckErr(cmd.Execute())
 }
diff --git a/cmd/aspect/root.go b/cmd/aspect/root.go
new file mode 100644
index 0000000..8bfa033
--- /dev/null
+++ b/cmd/aspect/root.go
@@ -0,0 +1,63 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/fatih/color"
+	"github.com/mattn/go-isatty"
+	"github.com/mitchellh/go-homedir"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+
+	"aspect.build/cli/cmd/aspect/version"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+func NewDefaultRootCmd() *cobra.Command {
+	defaultInteractive := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
+	return NewRootCmd(ioutils.DefaultStreams, defaultInteractive)
+}
+
+func NewRootCmd(streams ioutils.Streams, defaultInteractive bool) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "aspect",
+		Short: "Aspect.build bazel wrapper",
+		Long:  color.New(color.FgBlue).SprintFunc()(`Aspect CLI`) + ` is a better frontend for running bazel`,
+	}
+
+	// ### Flags
+	var cfgFile string
+	var interactive bool
+	cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.aspect.yaml)")
+	cmd.PersistentFlags().BoolVar(&interactive, "interactive", defaultInteractive, "Interactive mode (e.g. prompts for user input)")
+
+	// ### Viper
+	if cfgFile != "" {
+		// Use config file from the flag.
+		viper.SetConfigFile(cfgFile)
+	} else {
+		// Find home directory.
+		home, err := homedir.Dir()
+		cobra.CheckErr(err)
+
+		// Search config in home directory with name ".aspect" (without extension).
+		viper.AddConfigPath(home)
+		viper.SetConfigName(".aspect")
+	}
+	viper.AutomaticEnv()
+	if err := viper.ReadInConfig(); err == nil {
+		fmt.Fprintln(streams.Stderr, "Using config file:", viper.ConfigFileUsed())
+	}
+
+	// ### Child commands
+	cmd.AddCommand(version.NewDefaultVersionCmd())
+
+	return cmd
+}
diff --git a/cmd/aspect/version/BUILD.bazel b/cmd/aspect/version/BUILD.bazel
new file mode 100644
index 0000000..d58d38f
--- /dev/null
+++ b/cmd/aspect/version/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "version",
+    srcs = ["version.go"],
+    importpath = "aspect.build/cli/cmd/aspect/version",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//buildinfo",
+        "//pkg/aspect/version",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
diff --git a/cmd/aspect/version/version.go b/cmd/aspect/version/version.go
new file mode 100644
index 0000000..21553c9
--- /dev/null
+++ b/cmd/aspect/version/version.go
@@ -0,0 +1,37 @@
+/*
+Copyright © 2021 Aspect Build Systems
+
+Not licensed for re-use
+*/
+
+package version
+
+import (
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/buildinfo"
+	"aspect.build/cli/pkg/aspect/version"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+func NewDefaultVersionCmd() *cobra.Command {
+	return NewVersionCmd(ioutils.DefaultStreams)
+}
+
+func NewVersionCmd(streams ioutils.Streams) *cobra.Command {
+	v := version.New(streams)
+
+	v.BuildinfoRelease = buildinfo.Release
+	v.BuildinfoGitStatus = buildinfo.GitStatus
+
+	cmd := &cobra.Command{
+		Use:   "version",
+		Short: "Print the version of aspect CLI as well as tools it invokes",
+		Long:  `Prints version info on colon-separated lines, just like bazel does`,
+		RunE:  v.Run,
+	}
+
+	cmd.PersistentFlags().BoolVarP(&v.GNUFormat, "gnu_format", "", false, "format help following GNU convention")
+
+	return cmd
+}
diff --git a/cmd/root.go b/cmd/root.go
deleted file mode 100644
index cad93c4..0000000
--- a/cmd/root.go
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
-Copyright © 2021 Aspect Build Systems Inc
-
-Not licensed for re-use.
-*/
-package cmd
-
-import (
-	"fmt"
-	"github.com/spf13/cobra"
-	"os"
-
-	"github.com/fatih/color"
-	"github.com/mattn/go-isatty"
-	homedir "github.com/mitchellh/go-homedir"
-	"github.com/spf13/viper"
-)
-
-var (
-	cfgFile string
-	interactive bool
-
-	varInitFncs []func()
-	cmdInitFncs []func()	
-) 
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "aspect",
-	Short: "Aspect.build bazel wrapper",
-	Long:  color.New(color.FgBlue).SprintFunc()(`Aspect CLI`) + ` is a better frontend for running bazel`,
-	// Uncomment the following line if your bare application
-	// has an action associated with it:
-	// Run: func(cmd *cobra.Command, args []string) { },
-}
-
-func RegisterCommandVar(c func()) bool {
-	varInitFncs = append(varInitFncs, c)
-
-	return true
-}
-
-func RegisterCommandInit(c func()) bool {
-	cmdInitFncs = append(cmdInitFncs, c)
-	return true
-}
-
-func Main() error {
-	// Setup all variables.
-	// Setting up all the variables first will allow px
-	// to initialize the init functions in any order
-	for _, v := range varInitFncs {
-		v()
-	}
-
-	// Call all plugin inits
-	for _, f := range cmdInitFncs {
-		f()
-	}
-	return rootCmd.Execute()
-}
-
-// Execute adds all child commands to the root command and sets flags appropriately.
-// This is called by main.main(). It only needs to happen once to the rootCmd.
-func Execute() {
-	cobra.CheckErr(Main())
-}
-
-func init() {
-	cobra.OnInitialize(initConfig)
-
-	// Here you will define your flags and configuration settings.
-	// Cobra supports persistent flags, which, if defined here,
-	// will be global for your application.
-
-	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.aspect.yaml)")
-
-	interactive_default := false
-	if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
-		interactive_default = true
-	}
-	rootCmd.PersistentFlags().BoolVar(&interactive, "interactive", interactive_default, "Interactive mode (e.g. prompts for user input)")
-}
-
-// initConfig reads in config file and ENV variables if set.
-func initConfig() {
-	if cfgFile != "" {
-		// Use config file from the flag.
-		viper.SetConfigFile(cfgFile)
-	} else {
-		// Find home directory.
-		home, err := homedir.Dir()
-		cobra.CheckErr(err)
-
-		// Search config in home directory with name ".aspect" (without extension).
-		viper.AddConfigPath(home)
-		viper.SetConfigName(".aspect")
-	}
-
-	viper.AutomaticEnv() // read in environment variables that match
-
-	// If a config file is found, read it in.
-	if err := viper.ReadInConfig(); err == nil {
-		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
-	}
-}
diff --git a/cmd/version.go b/cmd/version.go
deleted file mode 100644
index 1396f3d..0000000
--- a/cmd/version.go
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
-Copyright © 2021 Aspect Build Systems
-
-Not licensed for re-use
-*/
-package cmd
-
-import (
-	"aspect.build/cli/bazel"
-	"aspect.build/cli/buildinfo"
-	"fmt"
-	"github.com/spf13/cobra"
-)
-
-var gnuFormat bool
-var versionCmd *cobra.Command
-
-var _ = RegisterCommandVar(func() {
-	// versionCmd represents the version command
-	versionCmd = &cobra.Command{
-		Use:   "version",
-		Short: "Print the version of aspect CLI as well as tools it invokes",
-		Long:  `Prints version info on colon-separated lines, just like bazel does`,
-		Run: versionExec,
-	}
-})
-
-var _ = RegisterCommandInit(func() {
-	versionCmd.PersistentFlags().BoolVarP(&gnuFormat, "gnu_format", "", false, "format help following GNU convention")
-	rootCmd.AddCommand(versionCmd)
-})
-
-func versionExec(cmd *cobra.Command, args []string) {
-	var version string
-	if !buildinfo.IsStamped() {
-		version = "unknown [not built with --stamp]"
-	} else {
-		version := buildinfo.Release
-		if buildinfo.GitStatus != "clean" {
-			version += " (with local changes)"
-		}
-	}
-	// Check if the --gnu_format flag is set, if that is the case,
-	// the version is printed differently
-	bazelCmd := []string{"version"}
-	if gnuFormat {
-		fmt.Printf("Aspect %s\n", version)
-		// Propagate the flag
-		bazelCmd = append(bazelCmd, "--gnu_format")
-	} else {
-		fmt.Printf("Aspect version: %s\n", version)
-	}
-	bazel.Spawn(bazelCmd)
-}
diff --git a/cmd/version_test.go b/cmd/version_test.go
deleted file mode 100644
index 45f00eb..0000000
--- a/cmd/version_test.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"io"
-	"os"
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-var (
-	// Stdout points to the output buffer to send screen output
-	Stdout io.Writer = os.Stdout
-	// Stderr points to the output buffer to send errors to the screen
-	Stderr io.Writer = os.Stderr
-)
-
-// Returns a buffer for stdout, stderr, and a function.
-// The function should be used as a defer to restore the state
-func testSetupCli(args string) (*bytes.Buffer, *bytes.Buffer, func()) {
-	// Save
-	oldargs := os.Args
-	oldStdout := Stdout
-	oldStderr := Stderr
-
-	// Create new buffers
-	stdout := new(bytes.Buffer)
-	stderr := new(bytes.Buffer)
-
-	// Set buffers
-	Stdout = stdout
-	Stderr = stderr
-	os.Args = strings.Split(args, " ")
-
-	return stdout, stderr, func() {
-		os.Args = oldargs
-		Stdout = oldStdout
-		Stderr = oldStderr
-	}
-}
-
-// Execute cli and return the buffers of standard out, standard error,
-// and error.
-// Callers must managage global variables using Patch from pkg/tests
-func executeCliRaw(cli string) (*bytes.Buffer, *bytes.Buffer, error) {
-	so, se, r := testSetupCli(cli)
-
-	// Defer to cleanup state
-	defer r()
-
-	// Start the CLI
-	err := Main()
-
-	return so, se, err
-}
-
-func executeCli(cli string) ([]string, []string, error) {
-	so, se, err := executeCliRaw(cli)
-
-	return strings.Split(so.String(), "\n"),
-		strings.Split(se.String(), "\n"),
-		err
-}
-
-func TestVersion(t *testing.T) {
-    input := "aspect version"
-    _, stderr, err := executeCli(input)
-    assert.NoError(t, err)
-    assert.Contains(t, stderr, "version")
-}
-
-func TestVersionGnuFormat(t *testing.T) {
-    t.Skip()
-    input := "aspect version --gnu-format"
-    lines, _, err := executeCli(input)
-    assert.NoError(t, err)
-    assert.Contains(t, lines, "version")
-}
diff --git a/go.bzl b/go.bzl
index db64214..054f6cd 100644
--- a/go.bzl
+++ b/go.bzl
@@ -53,7 +53,6 @@
         sum = "h1:fNtx0dJpG5ENGdMj3/GICoi/7z+ixB3IIW5rERTzOgM=",
         version = "v0.28.0",
     )
-
     go_repository(
         name = "com_github_beorn7_perks",
         importpath = "github.com/beorn7/perks",
@@ -193,6 +192,12 @@
         version = "v1.8.0",
     )
     go_repository(
+        name = "com_github_go_task_slim_sprig",
+        importpath = "github.com/go-task/slim-sprig",
+        sum = "h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=",
+        version = "v0.0.0-20210107165309-348f09dbbbc0",
+    )
+    go_repository(
         name = "com_github_gogo_protobuf",
         importpath = "github.com/gogo/protobuf",
         sum = "h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=",
@@ -219,8 +224,8 @@
     go_repository(
         name = "com_github_golang_protobuf",
         importpath = "github.com/golang/protobuf",
-        sum = "h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=",
-        version = "v1.3.2",
+        sum = "h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=",
+        version = "v1.5.2",
     )
     go_repository(
         name = "com_github_google_btree",
@@ -231,8 +236,8 @@
     go_repository(
         name = "com_github_google_go_cmp",
         importpath = "github.com/google/go-cmp",
-        sum = "h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=",
-        version = "v0.3.0",
+        sum = "h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=",
+        version = "v0.5.5",
     )
     go_repository(
         name = "com_github_google_martian",
@@ -366,7 +371,6 @@
         sum = "h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=",
         version = "v1.3.0",
     )
-
     go_repository(
         name = "com_github_hashicorp_golang_lru",
         importpath = "github.com/hashicorp/golang-lru",
@@ -404,6 +408,12 @@
         version = "v0.8.2",
     )
     go_repository(
+        name = "com_github_hpcloud_tail",
+        importpath = "github.com/hpcloud/tail",
+        sum = "h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=",
+        version = "v1.0.0",
+    )
+    go_repository(
         name = "com_github_inconshreveable_mousetrap",
         importpath = "github.com/inconshreveable/mousetrap",
         sum = "h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=",
@@ -572,6 +582,12 @@
         version = "v0.0.0-20161129095857-cc309e4a2223",
     )
     go_repository(
+        name = "com_github_nxadm_tail",
+        importpath = "github.com/nxadm/tail",
+        sum = "h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=",
+        version = "v1.4.8",
+    )
+    go_repository(
         name = "com_github_oklog_ulid",
         importpath = "github.com/oklog/ulid",
         sum = "h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=",
@@ -584,6 +600,18 @@
         version = "v1.2.2",
     )
     go_repository(
+        name = "com_github_onsi_ginkgo",
+        importpath = "github.com/onsi/ginkgo",
+        sum = "h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=",
+        version = "v1.16.4",
+    )
+    go_repository(
+        name = "com_github_onsi_gomega",
+        importpath = "github.com/onsi/gomega",
+        sum = "h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=",
+        version = "v1.16.0",
+    )
+    go_repository(
         name = "com_github_pascaldekloe_goe",
         importpath = "github.com/pascaldekloe/goe",
         sum = "h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=",
@@ -760,8 +788,8 @@
     go_repository(
         name = "com_github_stretchr_testify",
         importpath = "github.com/stretchr/testify",
-        sum = "h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=",
-        version = "v1.4.0",
+        sum = "h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=",
+        version = "v1.5.1",
     )
     go_repository(
         name = "com_github_subosito_gotenv",
@@ -782,6 +810,12 @@
         version = "v0.0.0-20190116061207-43a291ad63a2",
     )
     go_repository(
+        name = "com_github_yuin_goldmark",
+        importpath = "github.com/yuin/goldmark",
+        sum = "h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=",
+        version = "v1.2.1",
+    )
+    go_repository(
         name = "com_google_cloud_go",
         importpath = "cloud.google.com/go",
         sum = "h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y=",
@@ -842,6 +876,12 @@
         version = "v2.1.0",
     )
     go_repository(
+        name = "in_gopkg_fsnotify_v1",
+        importpath = "gopkg.in/fsnotify.v1",
+        sum = "h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=",
+        version = "v1.4.7",
+    )
+    go_repository(
         name = "in_gopkg_ini_v1",
         importpath = "gopkg.in/ini.v1",
         sum = "h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=",
@@ -854,6 +894,12 @@
         version = "v1.12.0",
     )
     go_repository(
+        name = "in_gopkg_tomb_v1",
+        importpath = "gopkg.in/tomb.v1",
+        sum = "h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=",
+        version = "v1.0.0-20141024135613-dd632973f1e7",
+    )
+    go_repository(
         name = "in_gopkg_yaml_v2",
         importpath = "gopkg.in/yaml.v2",
         sum = "h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=",
@@ -902,10 +948,16 @@
         version = "v1.21.1",
     )
     go_repository(
+        name = "org_golang_google_protobuf",
+        importpath = "google.golang.org/protobuf",
+        sum = "h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=",
+        version = "v1.26.0",
+    )
+    go_repository(
         name = "org_golang_x_crypto",
         importpath = "golang.org/x/crypto",
-        sum = "h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=",
-        version = "v0.0.0-20190820162420-60c769a6c586",
+        sum = "h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=",
+        version = "v0.0.0-20200622213623-75b288015ac9",
     )
     go_repository(
         name = "org_golang_x_exp",
@@ -934,14 +986,14 @@
     go_repository(
         name = "org_golang_x_mod",
         importpath = "golang.org/x/mod",
-        sum = "h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=",
-        version = "v0.1.0",
+        sum = "h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=",
+        version = "v0.3.0",
     )
     go_repository(
         name = "org_golang_x_net",
         importpath = "golang.org/x/net",
-        sum = "h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=",
-        version = "v0.0.0-20190620200207-3b0461eec859",
+        sum = "h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=",
+        version = "v0.0.0-20210428140749-89ef3d95e781",
     )
     go_repository(
         name = "org_golang_x_oauth2",
@@ -952,8 +1004,8 @@
     go_repository(
         name = "org_golang_x_sync",
         importpath = "golang.org/x/sync",
-        sum = "h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=",
-        version = "v0.0.0-20190423024810-112230192c58",
+        sum = "h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=",
+        version = "v0.0.0-20201020160332-67f06af15bc9",
     )
     go_repository(
         name = "org_golang_x_sys",
@@ -962,6 +1014,12 @@
         version = "v0.0.0-20210611083646-a4fc73990273",
     )
     go_repository(
+        name = "org_golang_x_term",
+        importpath = "golang.org/x/term",
+        sum = "h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=",
+        version = "v0.0.0-20201126162022-7de9c90e9dd1",
+    )
+    go_repository(
         name = "org_golang_x_text",
         importpath = "golang.org/x/text",
         sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=",
@@ -976,14 +1034,14 @@
     go_repository(
         name = "org_golang_x_tools",
         importpath = "golang.org/x/tools",
-        sum = "h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=",
-        version = "v0.0.0-20191112195655-aa38f8e97acc",
+        sum = "h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=",
+        version = "v0.0.0-20201224043029-2b0845dc783e",
     )
     go_repository(
         name = "org_golang_x_xerrors",
         importpath = "golang.org/x/xerrors",
-        sum = "h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=",
-        version = "v0.0.0-20190717185122-a985d3407aa7",
+        sum = "h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=",
+        version = "v0.0.0-20200804184101-5ec99f83aff1",
     )
     go_repository(
         name = "org_uber_go_atomic",
diff --git a/go.mod b/go.mod
index f99aa83..b32cf0c 100644
--- a/go.mod
+++ b/go.mod
@@ -5,19 +5,17 @@
 require (
 	github.com/bazelbuild/bazelisk v1.10.1
 	github.com/fatih/color v1.12.0
-	github.com/fsnotify/fsnotify v1.4.9 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
 	github.com/mattn/go-isatty v0.0.13
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/onsi/gomega v1.16.0
 	github.com/pelletier/go-toml v1.9.2 // indirect
 	github.com/spf13/afero v1.6.0 // indirect
 	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/cobra v1.1.3
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/viper v1.7.1
-	github.com/stretchr/testify v1.4.0
 	golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect
-	golang.org/x/text v0.3.6 // indirect
 	gopkg.in/ini.v1 v1.62.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 24e6076..b962aee 100644
--- a/go.sum
+++ b/go.sum
@@ -85,6 +85,8 @@
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
@@ -98,14 +100,25 @@
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -166,6 +179,8 @@
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
@@ -229,8 +244,19 @@
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
+github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -303,14 +329,17 @@
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -327,8 +356,10 @@
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -350,10 +381,12 @@
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -365,8 +398,11 @@
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -375,11 +411,13 @@
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -390,11 +428,20 @@
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
 golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -420,10 +467,15 @@
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=
 golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -449,6 +501,15 @@
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -456,15 +517,20 @@
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/pkg/aspect/version/BUILD.bazel b/pkg/aspect/version/BUILD.bazel
new file mode 100644
index 0000000..dde5ee3
--- /dev/null
+++ b/pkg/aspect/version/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "version",
+    srcs = ["version.go"],
+    importpath = "aspect.build/cli/pkg/aspect/version",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/bazel",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
+
+go_test(
+    name = "version_test",
+    srcs = ["version_test.go"],
+    deps = [
+        ":version",
+        "//pkg/ioutils",
+        "@com_github_onsi_gomega//:gomega",
+    ],
+)
diff --git a/pkg/aspect/version/version.go b/pkg/aspect/version/version.go
new file mode 100644
index 0000000..a1ce21e
--- /dev/null
+++ b/pkg/aspect/version/version.go
@@ -0,0 +1,58 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package version
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/pkg/bazel"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+type Version struct {
+	ioutils.Streams
+
+	BuildinfoRelease   string
+	BuildinfoGitStatus string
+	GNUFormat          bool
+}
+
+func New(streams ioutils.Streams) *Version {
+	return &Version{
+		Streams: streams,
+	}
+}
+
+func (v *Version) Run(_ *cobra.Command, _ []string) error {
+	var versionBuilder strings.Builder
+	if v.BuildinfoRelease != "" {
+		versionBuilder.WriteString(v.BuildinfoRelease)
+		if v.BuildinfoGitStatus != "clean" {
+			versionBuilder.WriteString(" (with local changes)")
+		}
+	} else {
+		versionBuilder.WriteString("unknown [not built with --stamp]")
+	}
+	version := versionBuilder.String()
+	// Check if the --gnu_format flag is set, if that is the case,
+	// the version is printed differently
+	bazelCmd := []string{"version"}
+	if v.GNUFormat {
+		fmt.Fprintf(v.Stdout, "Aspect %s\n", version)
+		// Propagate the flag
+		bazelCmd = append(bazelCmd, "--gnu_format")
+	} else {
+		fmt.Fprintf(v.Stdout, "Aspect version: %s\n", version)
+	}
+	bzl := bazel.New()
+	bzl.Spawn(bazelCmd)
+
+	return nil
+}
diff --git a/pkg/aspect/version/version_test.go b/pkg/aspect/version/version_test.go
new file mode 100644
index 0000000..4d91d8d
--- /dev/null
+++ b/pkg/aspect/version/version_test.go
@@ -0,0 +1,68 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package version_test
+
+import (
+	"strings"
+	"testing"
+
+	. "github.com/onsi/gomega"
+
+	"aspect.build/cli/pkg/aspect/version"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+func TestVersion(t *testing.T) {
+	t.Run("without release build info", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		var stdout strings.Builder
+		streams := ioutils.Streams{Stdout: &stdout}
+		v := version.New(streams)
+		err := v.Run(nil, nil)
+		g.Expect(err).To(BeNil())
+		g.Expect(stdout.String()).To(Equal("Aspect version: unknown [not built with --stamp]\n"))
+	})
+
+	t.Run("with release build info", func(t *testing.T) {
+		t.Run("git is clean", func(t *testing.T) {
+			g := NewGomegaWithT(t)
+			var stdout strings.Builder
+			streams := ioutils.Streams{Stdout: &stdout}
+			v := version.New(streams)
+			v.BuildinfoRelease = "1.2.3"
+			v.BuildinfoGitStatus = "clean"
+			err := v.Run(nil, nil)
+			g.Expect(err).To(BeNil())
+			g.Expect(stdout.String()).To(Equal("Aspect version: 1.2.3\n"))
+		})
+
+		t.Run("git is dirty", func(t *testing.T) {
+			g := NewGomegaWithT(t)
+			var stdout strings.Builder
+			streams := ioutils.Streams{Stdout: &stdout}
+			v := version.New(streams)
+			v.BuildinfoRelease = "1.2.3"
+			v.BuildinfoGitStatus = ""
+			err := v.Run(nil, nil)
+			g.Expect(err).To(BeNil())
+			g.Expect(stdout.String()).To(Equal("Aspect version: 1.2.3 (with local changes)\n"))
+		})
+	})
+
+	t.Run("with --gnu_format", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		var stdout strings.Builder
+		streams := ioutils.Streams{Stdout: &stdout}
+		v := version.New(streams)
+		v.GNUFormat = true
+		v.BuildinfoRelease = "1.2.3"
+		v.BuildinfoGitStatus = "clean"
+		err := v.Run(nil, nil)
+		g.Expect(err).To(BeNil())
+		g.Expect(stdout.String()).To(Equal("Aspect 1.2.3\n"))
+	})
+}
diff --git a/pkg/bazel/BUILD.bazel b/pkg/bazel/BUILD.bazel
new file mode 100644
index 0000000..d85e11f
--- /dev/null
+++ b/pkg/bazel/BUILD.bazel
@@ -0,0 +1,24 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "bazel",
+    srcs = [
+        "bazel.go",
+        "flags.go",
+    ],
+    importpath = "aspect.build/cli/pkg/bazel",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_bazelbuild_bazelisk//core:go_default_library",
+        "@com_github_bazelbuild_bazelisk//repositories:go_default_library",
+    ],
+)
+
+go_test(
+    name = "bazel_test",
+    srcs = ["bazel_test.go"],
+    deps = [
+        ":bazel",
+        "@com_github_onsi_gomega//:gomega",
+    ],
+)
diff --git a/bazel/spawn.go b/pkg/bazel/bazel.go
similarity index 73%
rename from bazel/spawn.go
rename to pkg/bazel/bazel.go
index cc3781e..b45948c 100644
--- a/bazel/spawn.go
+++ b/pkg/bazel/bazel.go
@@ -1,3 +1,9 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
 package bazel
 
 import (
@@ -5,9 +11,20 @@
 	"github.com/bazelbuild/bazelisk/repositories"
 )
 
+type Spawner interface {
+	Spawn(command []string) (int, error)
+}
+
+type Bazel struct {
+}
+
+func New() *Bazel {
+	return &Bazel{}
+}
+
 // Spawn is similar to the main() function of bazelisk
 // see https://github.com/bazelbuild/bazelisk/blob/7c3d9d5/bazelisk.go
-func Spawn(command []string) (int, error) {
+func (*Bazel) Spawn(command []string) (int, error) {
 	gcs := &repositories.GCSRepo{}
 	gitHub := repositories.CreateGitHubRepo(core.GetEnvOrConfig("BAZELISK_GITHUB_TOKEN"))
 	// Fetch LTS releases, release candidates and Bazel-at-commits from GCS, forks and rolling releases from GitHub.
diff --git a/pkg/bazel/bazel_test.go b/pkg/bazel/bazel_test.go
new file mode 100644
index 0000000..8fec5c2
--- /dev/null
+++ b/pkg/bazel/bazel_test.go
@@ -0,0 +1,23 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package bazel_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+
+	"aspect.build/cli/pkg/bazel"
+)
+
+func TestBazel(t *testing.T) {
+	t.Run("satisfies the Spawner interface", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		var bzl bazel.Spawner = bazel.New()
+		g.Expect(bzl).To(Not(BeNil()))
+	})
+}
diff --git a/pkg/bazel/flags.go b/pkg/bazel/flags.go
new file mode 100644
index 0000000..4ff1cfc
--- /dev/null
+++ b/pkg/bazel/flags.go
@@ -0,0 +1,7 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package bazel
diff --git a/pkg/ioutils/BUILD.bazel b/pkg/ioutils/BUILD.bazel
new file mode 100644
index 0000000..3cdce77
--- /dev/null
+++ b/pkg/ioutils/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "ioutils",
+    srcs = ["streams.go"],
+    importpath = "aspect.build/cli/pkg/ioutils",
+    visibility = ["//visibility:public"],
+)
diff --git a/pkg/ioutils/streams.go b/pkg/ioutils/streams.go
new file mode 100644
index 0000000..8f0202c
--- /dev/null
+++ b/pkg/ioutils/streams.go
@@ -0,0 +1,24 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package ioutils
+
+import (
+	"io"
+	"os"
+)
+
+type Streams struct {
+	Stdin  io.Reader
+	Stdout io.Writer
+	Stderr io.Writer
+}
+
+var DefaultStreams = Streams{
+	Stdin:  os.Stdin,
+	Stdout: os.Stdout,
+	Stderr: os.Stderr,
+}