From b47dc1f3f05f9f68d03e8cbbb3ce9557c141301d Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 02:26:43 -0700 Subject: [PATCH 1/2] X-Smart-Branch-Parent: main From 4c2776bd48addbba327210b015c2036d7642743f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 02:36:53 -0700 Subject: [PATCH 2/2] refactor: remove test cmd, rename gateway.New, YAML resources, verbose mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four items from robby.todo.md: 1. Remove harness test subcommand — use test-flow.sh or make validate. 2. Rename gateway.NewCLI → gateway.New — callers need not know impl. 3. Extract RBAC/Route to deploy/rbac.yaml and deploy/route.yaml. Namespace removed so kubectl -n handles it dynamically. 4. Add --verbose / -v global flag. Shows every kubectl/helm/oc/openshell command before execution (stderr). Validated: unit 92, bats 29+29, Go+podman 19/19. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/connect.go | 2 +- cmd/deploy.go | 52 +++++------------------------------- cmd/deploy_test.go | 15 +++++------ cmd/new.go | 2 +- cmd/preflight.go | 2 +- cmd/providers.go | 2 +- cmd/teardown.go | 2 +- cmd/test.go | 26 ------------------ deploy/rbac.yaml | 26 ++++++++++++++++++ deploy/route.yaml | 13 +++++++++ internal/gateway/cli.go | 7 ++++- internal/gateway/cli_test.go | 50 +++++++++++++++++----------------- internal/k8s/kubectl.go | 6 +++++ internal/status/status.go | 18 ++++++++++++- main.go | 9 ++++++- 15 files changed, 118 insertions(+), 114 deletions(-) delete mode 100644 cmd/test.go create mode 100644 deploy/rbac.yaml create mode 100644 deploy/route.yaml diff --git a/cmd/connect.go b/cmd/connect.go index b5595bb..facd52c 100644 --- a/cmd/connect.go +++ b/cmd/connect.go @@ -11,7 +11,7 @@ func NewConnectCmd(cli string) *cobra.Command { Short: "Reconnect to a running sandbox", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - gw := gateway.NewCLI(cli) + gw := gateway.New(cli) name := "" if len(args) > 0 { name = args[0] diff --git a/cmd/deploy.go b/cmd/deploy.go index 1bdd450..e371f1c 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -28,13 +28,13 @@ func NewDeployCmd(harnessDir, cli string) *cobra.Command { Short: "Deploy or verify the gateway", RunE: func(cmd *cobra.Command, args []string) error { if remote { - gw := gateway.NewCLI(cli) + gw := gateway.New(cli) kc := k8s.New(kubeconfig, k8s.DefaultNamespace()) clusterRunner := k8s.New(kubeconfig, "") return deployRemote(harnessDir, gw, kc, clusterRunner) } if local { - gw := gateway.NewCLI(cli) + gw := gateway.New(cli) return deployLocal(gw) } return fmt.Errorf("specify --local or --remote") @@ -97,39 +97,8 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R "--clusterrole=cluster-admin", "--serviceaccount=agent-sandbox-system:agent-sandbox-controller") - // RBAC for launcher - if err := kc.ApplyYAML(ctx, - map[string]any{ - "apiVersion": "v1", - "kind": "ServiceAccount", - "metadata": map[string]any{"name": "openshell-launcher", "namespace": namespace}, - }, - map[string]any{ - "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", - "metadata": map[string]any{"name": "openshell-launcher", "namespace": namespace}, - "rules": []map[string]any{{ - "apiGroups": []string{""}, - "resources": []string{"configmaps", "secrets"}, - "verbs": []string{"get", "list"}, - }}, - }, - map[string]any{ - "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "RoleBinding", - "metadata": map[string]any{"name": "openshell-launcher", "namespace": namespace}, - "subjects": []map[string]any{{ - "kind": "ServiceAccount", - "name": "openshell-launcher", - "namespace": namespace, - }}, - "roleRef": map[string]any{ - "kind": "Role", - "name": "openshell-launcher", - "apiGroup": "rbac.authorization.k8s.io", - }, - }, - ); err != nil { + // RBAC for launcher (from deploy/rbac.yaml) + if err := kc.RunKubectlPassthrough(ctx, "apply", "-f", filepath.Join(harnessDir, "deploy", "rbac.yaml")); err != nil { return fmt.Errorf("applying launcher RBAC: %w", err) } @@ -169,19 +138,10 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R return fmt.Errorf("gateway rollout failed: %w", err) } - // Step 5: Route + // Step 5: Route (from deploy/route.yaml) status.Step(5, "Creating OpenShift route") if err := kc.RunKubectlQuiet(ctx, "get", "route", "gateway"); err != nil { - kc.ApplyYAML(ctx, map[string]any{ - "apiVersion": "route.openshift.io/v1", - "kind": "Route", - "metadata": map[string]any{"name": "gateway", "namespace": namespace}, - "spec": map[string]any{ - "tls": map[string]any{"termination": "passthrough"}, - "to": map[string]any{"kind": "Service", "name": "openshell"}, - "port": map[string]any{"targetPort": "grpc"}, - }, - }) + kc.RunKubectlPassthrough(ctx, "apply", "-f", filepath.Join(harnessDir, "deploy", "route.yaml")) } fmt.Printf(" Route: %s\n", routeHost) diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 91e5cae..390b6c9 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -51,12 +51,9 @@ func TestDeployRemote_Success(t *testing.T) { t.Errorf("missing CRD apply, calls: %v", clusterRunner.Calls) } - // Verify RBAC applied - if !nsRunner.HasCall("apply-yaml ServiceAccount") { - t.Errorf("missing RBAC ServiceAccount, calls: %v", nsRunner.Calls) - } - if !nsRunner.HasCall("apply-yaml Role") { - t.Errorf("missing RBAC Role, calls: %v", nsRunner.Calls) + // Verify RBAC applied (via kubectl apply -f) + if !nsRunner.HasCall("apply -f") { + t.Errorf("missing RBAC apply, calls: %v", nsRunner.Calls) } // Verify Helm install @@ -69,9 +66,9 @@ func TestDeployRemote_Success(t *testing.T) { t.Errorf("missing rollout status, calls: %v", nsRunner.Calls) } - // Verify Route applied - if !nsRunner.HasCall("apply-yaml Route") { - t.Errorf("missing Route apply, calls: %v", nsRunner.Calls) + // Verify Route applied (via kubectl apply -f) + if nsRunner.CallCount("apply -f") < 2 { + t.Errorf("expected 2 apply -f calls (RBAC + Route), got %d: %v", nsRunner.CallCount("apply -f"), nsRunner.Calls) } } diff --git a/cmd/new.go b/cmd/new.go index d3b406d..082c2fa 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -35,7 +35,7 @@ func NewNewCmd(harnessDir, cli string) *cobra.Command { sandboxName = args[0] } - gw := gateway.NewCLI(cli) + gw := gateway.New(cli) if remote { return newRemote(harnessDir, gw, profileName, sandboxName) diff --git a/cmd/preflight.go b/cmd/preflight.go index f3070f2..354ff79 100644 --- a/cmd/preflight.go +++ b/cmd/preflight.go @@ -22,7 +22,7 @@ func NewPreflightCmd(harnessDir, cli string) *cobra.Command { return preflight.RunNames(harnessDir) } } - gw := gateway.NewCLI(cli) + gw := gateway.New(cli) return preflight.RunCheck(harnessDir, gw, strict) }, } diff --git a/cmd/providers.go b/cmd/providers.go index 1062f2e..fe80bb9 100644 --- a/cmd/providers.go +++ b/cmd/providers.go @@ -21,7 +21,7 @@ func NewProvidersCmd(harnessDir, cli string) *cobra.Command { Use: "providers", Short: "Register providers with the gateway", RunE: func(cmd *cobra.Command, args []string) error { - gw := gateway.NewCLI(cli) + gw := gateway.New(cli) return registerProviders(harnessDir, gw, force) }, } diff --git a/cmd/teardown.go b/cmd/teardown.go index a8405c9..6b97a93 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -29,7 +29,7 @@ func NewTeardownCmd(harnessDir, cli string) *cobra.Command { k8sFlag = true } - gw := gateway.NewCLI(cli) + gw := gateway.New(cli) activeGW := gw.ActiveGateway() if activeGW != "" { diff --git a/cmd/test.go b/cmd/test.go deleted file mode 100644 index c679819..0000000 --- a/cmd/test.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "os" - "os/exec" - "path/filepath" - - "github.com/spf13/cobra" -) - -func NewTestCmd(harnessDir string) *cobra.Command { - return &cobra.Command{ - Use: "test [podman|ocp|all] [--full]", - Short: "End-to-end validation", - DisableFlagParsing: true, - RunE: func(cmd *cobra.Command, args []string) error { - path := filepath.Join(harnessDir, "test", "test-flow.sh") - c := exec.Command("bash", append([]string{path}, args...)...) - c.Stdin = os.Stdin - c.Stdout = os.Stdout - c.Stderr = os.Stderr - c.Dir = harnessDir - return c.Run() - }, - } -} diff --git a/deploy/rbac.yaml b/deploy/rbac.yaml new file mode 100644 index 0000000..78199dd --- /dev/null +++ b/deploy/rbac.yaml @@ -0,0 +1,26 @@ +# Launcher RBAC — namespace set by kubectl -n flag. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: openshell-launcher +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: openshell-launcher +rules: + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: openshell-launcher +subjects: + - kind: ServiceAccount + name: openshell-launcher +roleRef: + kind: Role + name: openshell-launcher + apiGroup: rbac.authorization.k8s.io diff --git a/deploy/route.yaml b/deploy/route.yaml new file mode 100644 index 0000000..9ee1f4c --- /dev/null +++ b/deploy/route.yaml @@ -0,0 +1,13 @@ +# OpenShift route — namespace set by kubectl -n flag. +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: gateway +spec: + tls: + termination: passthrough + to: + kind: Service + name: openshell + port: + targetPort: grpc diff --git a/internal/gateway/cli.go b/internal/gateway/cli.go index 9c4d2e5..4c00fbe 100644 --- a/internal/gateway/cli.go +++ b/internal/gateway/cli.go @@ -7,6 +7,8 @@ import ( "regexp" "strings" "syscall" + + "github.com/robbycochran/harness-openshell/internal/status" ) var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`) @@ -16,7 +18,7 @@ type CLI struct { bin string // path or name of the openshell binary } -func NewCLI(bin string) *CLI { +func New(bin string) *CLI { return &CLI{bin: bin} } @@ -253,6 +255,7 @@ func (c *CLI) SandboxExec(name string, command ...string) error { // passthrough runs the CLI with stdin/stdout/stderr connected. func (c *CLI) passthrough(args ...string) error { + status.Cmd(c.bin, args...) cmd := exec.Command(c.bin, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -262,6 +265,7 @@ func (c *CLI) passthrough(args ...string) error { // silent runs the CLI with all output discarded. func (c *CLI) silent(args ...string) error { + status.Cmd(c.bin, args...) cmd := exec.Command(c.bin, args...) cmd.Stdout = io.Discard cmd.Stderr = io.Discard @@ -270,6 +274,7 @@ func (c *CLI) silent(args ...string) error { // output runs the CLI and returns stdout. func (c *CLI) output(args ...string) ([]byte, error) { + status.Cmd(c.bin, args...) cmd := exec.Command(c.bin, args...) cmd.Stderr = io.Discard return cmd.Output() diff --git a/internal/gateway/cli_test.go b/internal/gateway/cli_test.go index 9c293b1..f171e67 100644 --- a/internal/gateway/cli_test.go +++ b/internal/gateway/cli_test.go @@ -24,7 +24,7 @@ printf "github\tgithub\tactive\n" printf "vertex-local\tgoogle-vertex-ai\tactive\n" printf "atlassian\tatlassian\tactive\n" `) - gw := NewCLI(bin) + gw := New(bin) names, err := gw.ProviderList() if err != nil { t.Fatalf("ProviderList: %v", err) @@ -41,7 +41,7 @@ func TestProviderList_Empty(t *testing.T) { bin := writeStub(t, `#!/bin/bash printf "NAME\tTYPE\tSTATUS\n" `) - gw := NewCLI(bin) + gw := New(bin) names, err := gw.ProviderList() if err != nil { t.Fatalf("ProviderList: %v", err) @@ -52,7 +52,7 @@ printf "NAME\tTYPE\tSTATUS\n" } func TestProviderList_CLINotFound(t *testing.T) { - gw := NewCLI("/nonexistent/openshell") + gw := New("/nonexistent/openshell") _, err := gw.ProviderList() if err == nil { t.Error("expected error for missing CLI") @@ -64,7 +64,7 @@ func TestProviderGet_Exists(t *testing.T) { [[ "$3" == "github" ]] && exit 0 exit 1 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.ProviderGet("github"); err != nil { t.Errorf("ProviderGet(github) = %v, want nil", err) } @@ -74,7 +74,7 @@ func TestProviderGet_Missing(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 1 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.ProviderGet("nonexistent"); err == nil { t.Error("ProviderGet(nonexistent) = nil, want error") } @@ -84,7 +84,7 @@ func TestInferenceGet_Active(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 0 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.InferenceGet(); err != nil { t.Errorf("InferenceGet = %v, want nil", err) } @@ -94,7 +94,7 @@ func TestInferenceGet_NoGateway(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 1 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.InferenceGet(); err == nil { t.Error("InferenceGet = nil, want error") } @@ -104,7 +104,7 @@ func TestSandboxCreate_ArgsMinimal(t *testing.T) { bin := writeStub(t, `#!/bin/bash echo "$@" > /tmp/test-create-args `) - gw := NewCLI(bin) + gw := New(bin) gw.SandboxCreate(SandboxCreateOpts{ Name: "test", TTY: false, @@ -133,7 +133,7 @@ func TestSandboxCreate_ArgsFull(t *testing.T) { bin := writeStub(t, `#!/bin/bash echo "$@" > `+argsFile+` `) - gw := NewCLI(bin) + gw := New(bin) gw.SandboxCreate(SandboxCreateOpts{ Name: "my-agent", Image: "quay.io/test:latest", @@ -168,7 +168,7 @@ func TestSandboxDelete_Silent(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 0 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.SandboxDelete("test"); err != nil { t.Errorf("SandboxDelete = %v", err) } @@ -178,7 +178,7 @@ func TestSandboxDelete_NotFound(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 1 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.SandboxDelete("missing"); err == nil { t.Error("SandboxDelete = nil, want error") } @@ -190,7 +190,7 @@ printf "NAME\tENDPOINT\tTYPE\tAUTH\n" printf " openshell\thttps://127.0.0.1:17670\tlocal\tmtls\n" printf "* openshell-remote-ocp\thttps://gw.example.com:443\tlocal\tmtls\n" `) - gw := NewCLI(bin) + gw := New(bin) gateways, err := gw.GatewayList() if err != nil { t.Fatalf("GatewayList: %v", err) @@ -215,7 +215,7 @@ printf "NAME\tPHASE\n" printf "\033[32magent\033[0m\tReady\n" printf "\033[32mtest-agent\033[0m\tReady\n" `) - gw := NewCLI(bin) + gw := New(bin) names, err := gw.SandboxList() if err != nil { t.Fatalf("SandboxList: %v", err) @@ -232,7 +232,7 @@ func TestSandboxList_Empty(t *testing.T) { bin := writeStub(t, `#!/bin/bash printf "NAME\tPHASE\n" `) - gw := NewCLI(bin) + gw := New(bin) names, err := gw.SandboxList() if err != nil { t.Fatalf("SandboxList: %v", err) @@ -248,7 +248,7 @@ printf "NAME\tENDPOINT\n" printf " local\thttps://127.0.0.1:17670\n" printf "* remote\thttps://gw.example.com\n" `) - gw := NewCLI(bin) + gw := New(bin) active := gw.ActiveGateway() if active != "remote" { t.Errorf("ActiveGateway = %q, want remote", active) @@ -260,7 +260,7 @@ func TestActiveGateway_None(t *testing.T) { printf "NAME\tENDPOINT\n" printf " local\thttps://127.0.0.1:17670\n" `) - gw := NewCLI(bin) + gw := New(bin) active := gw.ActiveGateway() if active != "" { t.Errorf("ActiveGateway = %q, want empty", active) @@ -272,7 +272,7 @@ func TestInferenceModel_ParsesModel(t *testing.T) { echo "Provider: vertex-local" echo "Model: claude-sonnet-4-6" `) - gw := NewCLI(bin) + gw := New(bin) model := gw.InferenceModel() if model != "claude-sonnet-4-6" { t.Errorf("InferenceModel = %q, want claude-sonnet-4-6", model) @@ -283,7 +283,7 @@ func TestCLIVersion(t *testing.T) { bin := writeStub(t, `#!/bin/bash echo "openshell v0.0.55" `) - gw := NewCLI(bin) + gw := New(bin) ver := gw.CLIVersion() if ver != "openshell v0.0.55" { t.Errorf("CLIVersion = %q", ver) @@ -294,7 +294,7 @@ func TestCLIPath(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 0 `) - gw := NewCLI(bin) + gw := New(bin) path := gw.CLIPath() if path == "" { t.Error("CLIPath = empty, want non-empty") @@ -302,7 +302,7 @@ exit 0 } func TestCLIPath_NotFound(t *testing.T) { - gw := NewCLI("/nonexistent/openshell") + gw := New("/nonexistent/openshell") path := gw.CLIPath() if path != "" { t.Errorf("CLIPath = %q, want empty", path) @@ -315,7 +315,7 @@ func TestProviderCreate_Args(t *testing.T) { bin := writeStub(t, `#!/bin/bash printf '%s\n' "$*" > `+argsFile+` `) - gw := NewCLI(bin) + gw := New(bin) gw.ProviderCreate("vertex-local", "google-vertex-ai", ProviderCreateOpts{ FromADC: true, Credentials: []string{"TOKEN=abc"}, @@ -343,7 +343,7 @@ func TestInferenceSet_Args(t *testing.T) { bin := writeStub(t, `#!/bin/bash printf '%s\n' "$*" > `+argsFile+` `) - gw := NewCLI(bin) + gw := New(bin) gw.InferenceSet("vertex-local", "claude-sonnet-4-6") data, _ := os.ReadFile(argsFile) args := strings.TrimSpace(string(data)) @@ -365,7 +365,7 @@ func TestGatewayAdd_Args(t *testing.T) { bin := writeStub(t, `#!/bin/bash printf '%s\n' "$*" > `+argsFile+` `) - gw := NewCLI(bin) + gw := New(bin) gw.GatewayAdd("https://gw.example.com:443", "my-ocp", true) data, _ := os.ReadFile(argsFile) args := strings.TrimSpace(string(data)) @@ -385,7 +385,7 @@ func TestGatewayRemove(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 0 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.GatewayRemove("old-gw"); err != nil { t.Errorf("GatewayRemove: %v", err) } @@ -395,7 +395,7 @@ func TestProviderProfileDelete(t *testing.T) { bin := writeStub(t, `#!/bin/bash exit 0 `) - gw := NewCLI(bin) + gw := New(bin) if err := gw.ProviderProfileDelete("profile-123"); err != nil { t.Errorf("ProviderProfileDelete: %v", err) } diff --git a/internal/k8s/kubectl.go b/internal/k8s/kubectl.go index d1534ed..0f8ffeb 100644 --- a/internal/k8s/kubectl.go +++ b/internal/k8s/kubectl.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/robbycochran/harness-openshell/internal/status" "gopkg.in/yaml.v3" ) @@ -68,6 +69,8 @@ func (c *Client) RunKubectlOpts(ctx context.Context, opts KubectlOpts) (string, args = append([]string{"--kubeconfig", c.kubeconfig}, args...) } + status.Cmd("kubectl", args...) + var lastErr error for attempt := range 3 { cmd := exec.CommandContext(ctx, "kubectl", args...) @@ -119,6 +122,7 @@ func (c *Client) RunKubectlPassthrough(ctx context.Context, args ...string) erro if c.kubeconfig != "" { args = append([]string{"--kubeconfig", c.kubeconfig}, args...) } + status.Cmd("kubectl", args...) cmd := exec.CommandContext(ctx, "kubectl", args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -133,6 +137,7 @@ func (c *Client) RunHelm(ctx context.Context, args ...string) (string, error) { if c.kubeconfig != "" { args = append(args, "--kubeconfig", c.kubeconfig) } + status.Cmd("helm", args...) cmd := exec.CommandContext(ctx, "helm", args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -144,6 +149,7 @@ func (c *Client) RunOC(ctx context.Context, args ...string) error { if c.kubeconfig != "" { args = append(args, "--kubeconfig", c.kubeconfig) } + status.Cmd("oc", args...) cmd := exec.CommandContext(ctx, "oc", args...) cmd.Stdout = io.Discard cmd.Stderr = io.Discard diff --git a/internal/status/status.go b/internal/status/status.go index 3f715b0..945d4fe 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -1,6 +1,22 @@ package status -import "fmt" +import ( + "fmt" + "os" +) + +var Verbose bool + +func Cmd(name string, args ...string) { + if !Verbose { + return + } + fmt.Fprintf(os.Stderr, " $ %s", name) + for _, a := range args { + fmt.Fprintf(os.Stderr, " %s", a) + } + fmt.Fprintln(os.Stderr) +} func OK(msg string) { fmt.Println(" ✓ " + msg) } func OKf(format string, a ...any) { fmt.Printf(" ✓ "+format+"\n", a...) } diff --git a/main.go b/main.go index d80e046..ff312a1 100644 --- a/main.go +++ b/main.go @@ -6,19 +6,27 @@ import ( "path/filepath" "github.com/robbycochran/harness-openshell/cmd" + "github.com/robbycochran/harness-openshell/internal/status" "github.com/spf13/cobra" ) func main() { harnessDir := detectHarnessDir() + var verbose bool + root := &cobra.Command{ Use: "harness", Short: "OpenShell Harness — deploy and manage AI agent sandboxes", SilenceErrors: true, SilenceUsage: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + status.Verbose = verbose + }, } + root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show kubectl/helm/openshell commands") + cli := os.Getenv("OPENSHELL_CLI") if cli == "" { cli = "openshell" @@ -31,7 +39,6 @@ func main() { cmd.NewTeardownCmd(harnessDir, cli), cmd.NewPreflightCmd(harnessDir, cli), cmd.NewProvidersCmd(harnessDir, cli), - cmd.NewTestCmd(harnessDir), ) if err := root.Execute(); err != nil {