feat: build command (#24)

* feat: build command

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* fix: docs

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* fix: missing punctuation

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* refactor: move mock to its own package

This allows `go mod tidy` to run successfully while keeping Gazelle
happy.

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* fix: use proper arguments on tests

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* feat: propagate bazel exit code to aspect exit

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* fix: tests with ExitError

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* fix: command description

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
diff --git a/WORKSPACE b/WORKSPACE
index 78da1a6..63bdeee 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -41,3 +41,12 @@
 go_register_toolchains(version = "1.16")
 
 gazelle_dependencies()
+
+http_archive(
+    name = "bazel_gomock",
+    sha256 = "82a5fb946d2eb0fed80d3d70c2556784ec6cb5c35cd65a1b5e93e46f99681650",
+    strip_prefix = "bazel_gomock-1.3",
+    urls = [
+        "https://github.com/jmhodges/bazel_gomock/archive/refs/tags/v1.3.tar.gz",
+    ],
+)
diff --git a/cmd/aspect/BUILD.bazel b/cmd/aspect/BUILD.bazel
index 07b802d..9f25845 100644
--- a/cmd/aspect/BUILD.bazel
+++ b/cmd/aspect/BUILD.bazel
@@ -7,7 +7,7 @@
     visibility = ["//cmd:__subpackages__"],
     deps = [
         "//cmd/aspect/root",
-        "@com_github_spf13_cobra//:cobra",
+        "//pkg/aspecterrors",
     ],
 )
 
diff --git a/cmd/aspect/build/BUILD.bazel b/cmd/aspect/build/BUILD.bazel
new file mode 100644
index 0000000..4aa7eaa
--- /dev/null
+++ b/cmd/aspect/build/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "build",
+    srcs = ["build.go"],
+    importpath = "aspect.build/cli/cmd/aspect/build",
+    visibility = ["//cmd/aspect/root:__pkg__"],
+    deps = [
+        "//pkg/aspect/build",
+        "//pkg/bazel",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
diff --git a/cmd/aspect/build/build.go b/cmd/aspect/build/build.go
new file mode 100644
index 0000000..2c46fca
--- /dev/null
+++ b/cmd/aspect/build/build.go
@@ -0,0 +1,39 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package build
+
+import (
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/pkg/aspect/build"
+	"aspect.build/cli/pkg/bazel"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+// NewDefaultBuildCmd creates a new build cobra command with the default
+// dependencies.
+func NewDefaultBuildCmd() *cobra.Command {
+	return NewBuildCmd(ioutils.DefaultStreams, bazel.New())
+}
+
+// NewBuildCmd creates a new build cobra command.
+func NewBuildCmd(
+	streams ioutils.Streams,
+	bzl bazel.Spawner,
+) *cobra.Command {
+	b := build.New(streams, bzl)
+
+	cmd := &cobra.Command{
+		Use:   "build",
+		Short: "Builds the specified targets, using the options.",
+		Long: "Invokes bazel build on the specified targets. " +
+			"See 'bazel help target-syntax' for details and examples on how to specify targets to build.",
+		RunE: b.Run,
+	}
+
+	return cmd
+}
diff --git a/cmd/aspect/main.go b/cmd/aspect/main.go
index 12b014a..0e62da7 100644
--- a/cmd/aspect/main.go
+++ b/cmd/aspect/main.go
@@ -6,8 +6,14 @@
 
 package main
 
-import "aspect.build/cli/cmd/aspect/root"
-import "github.com/spf13/cobra"
+import (
+	"errors"
+	"fmt"
+	"os"
+
+	"aspect.build/cli/cmd/aspect/root"
+	"aspect.build/cli/pkg/aspecterrors"
+)
 
 func main() {
 	// Detect whether we are being run as a tools/bazel wrapper (look for BAZEL_REAL in the environment)
@@ -22,5 +28,16 @@
 	//         - tools/bazel file and put our bootstrap code in there
 	//
 	cmd := root.NewDefaultRootCmd()
-	cobra.CheckErr(cmd.Execute())
+	if err := cmd.Execute(); err != nil {
+		var exitErr *aspecterrors.ExitError
+		if errors.As(err, &exitErr) {
+			if exitErr.Err != nil {
+				fmt.Fprintln(os.Stderr, "Error:", err)
+			}
+			os.Exit(exitErr.ExitCode)
+		}
+
+		fmt.Fprintln(os.Stderr, "Error:", err)
+		os.Exit(1)
+	}
 }
diff --git a/cmd/aspect/root/BUILD.bazel b/cmd/aspect/root/BUILD.bazel
index 683cf6f..faa1504 100644
--- a/cmd/aspect/root/BUILD.bazel
+++ b/cmd/aspect/root/BUILD.bazel
@@ -4,8 +4,12 @@
     name = "root",
     srcs = ["root.go"],
     importpath = "aspect.build/cli/cmd/aspect/root",
-    visibility = ["//visibility:public"],
+    visibility = [
+        "//cmd/aspect:__pkg__",
+        "//cmd/docgen:__pkg__",
+    ],
     deps = [
+        "//cmd/aspect/build",
         "//cmd/aspect/docs",
         "//cmd/aspect/version",
         "//docs/help/topics",
diff --git a/cmd/aspect/root/root.go b/cmd/aspect/root/root.go
index c6c8e51..2345184 100644
--- a/cmd/aspect/root/root.go
+++ b/cmd/aspect/root/root.go
@@ -15,6 +15,7 @@
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
+	"aspect.build/cli/cmd/aspect/build"
 	"aspect.build/cli/cmd/aspect/docs"
 	"aspect.build/cli/cmd/aspect/version"
 	"aspect.build/cli/docs/help/topics"
@@ -33,9 +34,11 @@
 
 func NewRootCmd(streams ioutils.Streams, defaultInteractive bool) *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "aspect",
-		Short: "Aspect.build bazel wrapper",
-		Long:  boldCyan.Sprintf(`Aspect CLI`) + ` is a better frontend for running bazel`,
+		Use:           "aspect",
+		Short:         "Aspect.build bazel wrapper",
+		SilenceUsage:  true,
+		SilenceErrors: true,
+		Long:          boldCyan.Sprintf(`Aspect CLI`) + ` is a better frontend for running bazel`,
 	}
 
 	// ### Flags
@@ -64,6 +67,7 @@
 
 	// ### Child commands
 	// IMPORTANT: when adding a new command, also update the _DOCS list in /docs/BUILD.bazel
+	cmd.AddCommand(build.NewDefaultBuildCmd())
 	cmd.AddCommand(version.NewDefaultVersionCmd())
 	cmd.AddCommand(docs.NewDefaultDocsCmd())
 
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index d4cbfd4..4fd027d 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -4,6 +4,7 @@
 # This list must be updated when we add a new command
 _DOCS = [
     "aspect.md",
+    "aspect_build.md",
     "aspect_docs.md",
     "aspect_version.md",
 ]
diff --git a/docs/aspect.md b/docs/aspect.md
index c305917..24c1aa8 100644
--- a/docs/aspect.md
+++ b/docs/aspect.md
@@ -16,6 +16,7 @@
 
 ### SEE ALSO
 
+* [aspect build](aspect_build.md)	 - Builds the specified targets, using the options.
 * [aspect docs](aspect_docs.md)	 - Open documentation in the browser
 * [aspect version](aspect_version.md)	 - Print the version of aspect CLI as well as tools it invokes
 
diff --git a/docs/aspect_build.md b/docs/aspect_build.md
new file mode 100644
index 0000000..7dd414d
--- /dev/null
+++ b/docs/aspect_build.md
@@ -0,0 +1,30 @@
+## aspect build
+
+Builds the specified targets, using the options.
+
+### Synopsis
+
+Invokes bazel build on the specified targets. See 'bazel help target-syntax' for details and examples on how to specify targets to build.
+
+```
+aspect build [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for build
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.aspect.yaml)
+      --interactive     Interactive mode (e.g. prompts for user input)
+```
+
+### SEE ALSO
+
+* [aspect](aspect.md)	 - Aspect.build bazel wrapper
+
+###### Auto generated by spf13/cobra
diff --git a/go.bzl b/go.bzl
index 728a58f..bba5705 100644
--- a/go.bzl
+++ b/go.bzl
@@ -956,6 +956,7 @@
     )
     go_repository(
         name = "org_golang_google_protobuf",
+        build_directives = ["gazelle:exclude **/testdata/**/*"],
         importpath = "google.golang.org/protobuf",
         sum = "h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=",
         version = "v1.26.0",
@@ -999,8 +1000,8 @@
     go_repository(
         name = "org_golang_x_net",
         importpath = "golang.org/x/net",
-        sum = "h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=",
-        version = "v0.0.0-20210428140749-89ef3d95e781",
+        sum = "h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg=",
+        version = "v0.0.0-20210903162142-ad29c8ab022f",
     )
     go_repository(
         name = "org_golang_x_oauth2",
@@ -1017,8 +1018,8 @@
     go_repository(
         name = "org_golang_x_sys",
         importpath = "golang.org/x/sys",
-        sum = "h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk=",
-        version = "v0.0.0-20210616045830-e2b7044e8c71",
+        sum = "h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=",
+        version = "v0.0.0-20210903071746-97244b99971b",
     )
     go_repository(
         name = "org_golang_x_term",
@@ -1029,8 +1030,8 @@
     go_repository(
         name = "org_golang_x_text",
         importpath = "golang.org/x/text",
-        sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=",
-        version = "v0.3.6",
+        sum = "h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=",
+        version = "v0.3.7",
     )
     go_repository(
         name = "org_golang_x_time",
diff --git a/go.mod b/go.mod
index 96ca1e4..1f741ab 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@
 	github.com/bazelbuild/bazelisk v1.10.1
 	github.com/bazelbuild/rules_go v0.28.0
 	github.com/fatih/color v1.12.0
+	github.com/golang/mock v1.3.1
 	github.com/magiconair/properties v1.8.5 // indirect
 	github.com/mattn/go-isatty v0.0.13
 	github.com/mitchellh/go-homedir v1.1.0
@@ -18,5 +19,8 @@
 	github.com/spf13/cobra v1.1.3
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/viper v1.7.1
+	golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect
+	golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
+	golang.org/x/text v0.3.7 // indirect
 	gopkg.in/ini.v1 v1.62.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 4db7f4a..f599965 100644
--- a/go.sum
+++ b/go.sum
@@ -403,8 +403,9 @@
 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/net v0.0.0-20210903162142-ad29c8ab022f h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg=
+golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 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=
@@ -440,16 +441,18 @@
 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-20210616045830-e2b7044e8c71 h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk=
 golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
+golang.org/x/sys v0.0.0-20210903071746-97244b99971b/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=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/pkg/aspect/build/BUILD.bazel b/pkg/aspect/build/BUILD.bazel
new file mode 100644
index 0000000..e4df02f
--- /dev/null
+++ b/pkg/aspect/build/BUILD.bazel
@@ -0,0 +1,27 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "build",
+    srcs = ["build.go"],
+    importpath = "aspect.build/cli/pkg/aspect/build",
+    visibility = ["//cmd/aspect/build:__pkg__"],
+    deps = [
+        "//pkg/aspecterrors",
+        "//pkg/bazel",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
+
+go_test(
+    name = "build_test",
+    srcs = ["build_test.go"],
+    deps = [
+        ":build",
+        "//pkg/aspecterrors",
+        "//pkg/bazel/mock",
+        "//pkg/ioutils",
+        "@com_github_golang_mock//gomock",
+        "@com_github_onsi_gomega//:gomega",
+    ],
+)
diff --git a/pkg/aspect/build/build.go b/pkg/aspect/build/build.go
new file mode 100644
index 0000000..4e57ba9
--- /dev/null
+++ b/pkg/aspect/build/build.go
@@ -0,0 +1,46 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package build
+
+import (
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/pkg/aspecterrors"
+	"aspect.build/cli/pkg/bazel"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+// Build represents the aspect build command.
+type Build struct {
+	ioutils.Streams
+	bzl bazel.Spawner
+}
+
+// New creates a Build command.
+func New(
+	streams ioutils.Streams,
+	bzl bazel.Spawner,
+) *Build {
+	return &Build{
+		Streams: streams,
+		bzl:     bzl,
+	}
+}
+
+// Run runs the aspect build command.
+func (b *Build) Run(_ *cobra.Command, args []string) error {
+	cmd := append([]string{"build"}, args...)
+	if exitCode, err := b.bzl.Spawn(cmd); exitCode != 0 {
+		err = &aspecterrors.ExitError{
+			Err:      err,
+			ExitCode: exitCode,
+		}
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/aspect/build/build_test.go b/pkg/aspect/build/build_test.go
new file mode 100644
index 0000000..3dd2b06
--- /dev/null
+++ b/pkg/aspect/build/build_test.go
@@ -0,0 +1,65 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package build_test
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	. "github.com/onsi/gomega"
+
+	"aspect.build/cli/pkg/aspect/build"
+	"aspect.build/cli/pkg/aspecterrors"
+	"aspect.build/cli/pkg/bazel/mock"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+func TestBuild(t *testing.T) {
+	t.Run("when the bazel runner fails, the aspect build fails", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		var stdout strings.Builder
+		streams := ioutils.Streams{Stdout: &stdout}
+		spawner := mock.NewMockSpawner(ctrl)
+		expectErr := &aspecterrors.ExitError{
+			Err:      fmt.Errorf("failed to run bazel build"),
+			ExitCode: 5,
+		}
+		spawner.
+			EXPECT().
+			Spawn([]string{"build", "//..."}).
+			Return(expectErr.ExitCode, expectErr.Err)
+
+		b := build.New(streams, spawner)
+		err := b.Run(nil, []string{"//..."})
+
+		g.Expect(err).To(Equal(expectErr))
+	})
+
+	t.Run("when the bazel runner succeeds, the aspect build succeeds", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		var stdout strings.Builder
+		streams := ioutils.Streams{Stdout: &stdout}
+		spawner := mock.NewMockSpawner(ctrl)
+		spawner.
+			EXPECT().
+			Spawn([]string{"build", "//..."}).
+			Return(0, nil)
+
+		b := build.New(streams, spawner)
+		err := b.Run(nil, []string{"//..."})
+
+		g.Expect(err).To(BeNil())
+	})
+}
diff --git a/pkg/aspecterrors/BUILD.bazel b/pkg/aspecterrors/BUILD.bazel
new file mode 100644
index 0000000..49175ec
--- /dev/null
+++ b/pkg/aspecterrors/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "aspecterrors",
+    srcs = ["errors.go"],
+    importpath = "aspect.build/cli/pkg/aspecterrors",
+    visibility = ["//:__subpackages__"],
+)
diff --git a/pkg/aspecterrors/errors.go b/pkg/aspecterrors/errors.go
new file mode 100644
index 0000000..7388c7e
--- /dev/null
+++ b/pkg/aspecterrors/errors.go
@@ -0,0 +1,23 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package aspecterrors
+
+// ExitError encapsulates an upstream error and an exit code. It is used by the
+// aspect CLI main entrypoint to propagate meaningful exit error codes as the
+// aspect CLI exit code.
+type ExitError struct {
+	Err      error
+	ExitCode int
+}
+
+// Error returns the call to the encapsulated error.Error().
+func (err *ExitError) Error() string {
+	if err.Err != nil {
+		return err.Err.Error()
+	}
+	return ""
+}
diff --git a/pkg/bazel/BUILD.bazel b/pkg/bazel/BUILD.bazel
index d85e11f..f1ad4b9 100644
--- a/pkg/bazel/BUILD.bazel
+++ b/pkg/bazel/BUILD.bazel
@@ -7,7 +7,7 @@
         "flags.go",
     ],
     importpath = "aspect.build/cli/pkg/bazel",
-    visibility = ["//visibility:public"],
+    visibility = ["//:__subpackages__"],
     deps = [
         "@com_github_bazelbuild_bazelisk//core:go_default_library",
         "@com_github_bazelbuild_bazelisk//repositories:go_default_library",
diff --git a/pkg/bazel/mock/BUILD.bazel b/pkg/bazel/mock/BUILD.bazel
new file mode 100644
index 0000000..165b58e
--- /dev/null
+++ b/pkg/bazel/mock/BUILD.bazel
@@ -0,0 +1,26 @@
+load("@bazel_gomock//:gomock.bzl", "gomock")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+# gazelle:exclude mock_spawner_test.go
+
+gomock(
+    name = "mock_spawner_source",
+    out = "mock_spawner_test.go",
+    interfaces = ["Spawner"],
+    library = "//pkg/bazel",
+    package = "mock",
+    visibility = ["//visibility:private"],
+)
+
+go_library(
+    name = "mock",
+    srcs = [
+        "doc.go",
+        ":mock_spawner_source",  # keep
+    ],
+    importpath = "aspect.build/cli/pkg/bazel/mock",
+    visibility = ["//:__subpackages__"],
+    deps = [
+        "@com_github_golang_mock//gomock",  # keep
+    ],
+)
diff --git a/pkg/bazel/mock/doc.go b/pkg/bazel/mock/doc.go
new file mode 100644
index 0000000..747ba63
--- /dev/null
+++ b/pkg/bazel/mock/doc.go
@@ -0,0 +1,2 @@
+// Package mock contains generated files.
+package mock