From a9e392fd42627c04d9c44bd72bf43f319ffcd028 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Wed, 26 Aug 2026 14:55:02 -0700 Subject: [PATCH 1/7] refactor(cli): flatten CLI package layout Signed-off-by: Cody Hartsook --- go/core/cli/cmd/kagent/main.go | 24 +++++++++---------- go/core/cli/cmd/kagent/main_test.go | 2 +- .../{cli => commands}/agentinstance/get.go | 4 ++-- .../agentinstance/get_test.go | 2 +- .../{cli => commands}/agentinstance/invoke.go | 4 ++-- .../agentinstance/invoke_test.go | 2 +- .../agentinstance/lifecycle.go | 4 ++-- .../agentinstance/lifecycle_test.go | 2 +- .../{cli => commands}/agenttemplate/get.go | 2 +- .../agenttemplate/get_test.go | 2 +- .../{cli/agent => commands}/bug_report.go | 2 +- .../internal/{cli/agent => commands}/const.go | 2 +- .../{cli/agent => commands}/dashboard.go | 2 +- .../agent => commands}/dashboard_darwin.go | 2 +- .../{cli => commands}/envdoc/envdoc.go | 0 .../{cli/agent => commands}/install.go | 4 ++-- .../{cli => commands}/mcp/add_tool.go | 0 .../internal/{cli => commands}/mcp/build.go | 0 .../{cli => commands}/mcp/build_test.go | 0 .../internal/{cli => commands}/mcp/deploy.go | 0 .../internal/{cli => commands}/mcp/init.go | 0 .../{cli => commands}/mcp/init_test.go | 0 .../{cli => commands}/mcp/inspector.go | 0 .../{cli => commands}/mcp/integration_test.go | 0 .../internal/{cli => commands}/mcp/root.go | 0 .../cli/internal/{cli => commands}/mcp/run.go | 0 .../{cli => commands}/mcp/run_test.go | 0 .../internal/{cli => commands}/mcp/secrets.go | 0 .../{cli/agent => commands}/version.go | 2 +- .../{cli => }/connection/connection.go | 0 .../{cli => }/connection/connection_test.go | 0 .../cli/internal/{cli => }/output/output.go | 0 .../internal/{cli => }/output/output_test.go | 0 go/core/cli/internal/tui/workspace_test.go | 2 +- 34 files changed, 32 insertions(+), 32 deletions(-) rename go/core/cli/internal/{cli => commands}/agentinstance/get.go (96%) rename go/core/cli/internal/{cli => commands}/agentinstance/get_test.go (98%) rename go/core/cli/internal/{cli => commands}/agentinstance/invoke.go (98%) rename go/core/cli/internal/{cli => commands}/agentinstance/invoke_test.go (99%) rename go/core/cli/internal/{cli => commands}/agentinstance/lifecycle.go (96%) rename go/core/cli/internal/{cli => commands}/agentinstance/lifecycle_test.go (98%) rename go/core/cli/internal/{cli => commands}/agenttemplate/get.go (97%) rename go/core/cli/internal/{cli => commands}/agenttemplate/get_test.go (98%) rename go/core/cli/internal/{cli/agent => commands}/bug_report.go (99%) rename go/core/cli/internal/{cli/agent => commands}/const.go (99%) rename go/core/cli/internal/{cli/agent => commands}/dashboard.go (96%) rename go/core/cli/internal/{cli/agent => commands}/dashboard_darwin.go (98%) rename go/core/cli/internal/{cli => commands}/envdoc/envdoc.go (100%) rename go/core/cli/internal/{cli/agent => commands}/install.go (99%) rename go/core/cli/internal/{cli => commands}/mcp/add_tool.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/build.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/build_test.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/deploy.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/init.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/init_test.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/inspector.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/integration_test.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/root.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/run.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/run_test.go (100%) rename go/core/cli/internal/{cli => commands}/mcp/secrets.go (100%) rename go/core/cli/internal/{cli/agent => commands}/version.go (97%) rename go/core/cli/internal/{cli => }/connection/connection.go (100%) rename go/core/cli/internal/{cli => }/connection/connection_test.go (100%) rename go/core/cli/internal/{cli => }/output/output.go (100%) rename go/core/cli/internal/{cli => }/output/output_test.go (100%) diff --git a/go/core/cli/cmd/kagent/main.go b/go/core/cli/cmd/kagent/main.go index 1e1eb4339..bc0f9aa1e 100644 --- a/go/core/cli/cmd/kagent/main.go +++ b/go/core/cli/cmd/kagent/main.go @@ -10,12 +10,12 @@ import ( "syscall" "time" - cli "github.com/kagent-dev/kagent/go/core/cli/internal/cli/agent" - agentinstancecli "github.com/kagent-dev/kagent/go/core/cli/internal/cli/agentinstance" - agenttemplatecli "github.com/kagent-dev/kagent/go/core/cli/internal/cli/agenttemplate" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/connection" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/envdoc" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/mcp" + "github.com/kagent-dev/kagent/go/core/cli/internal/commands" + agentinstancecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agentinstance" + agenttemplatecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agenttemplate" + "github.com/kagent-dev/kagent/go/core/cli/internal/commands/envdoc" + "github.com/kagent-dev/kagent/go/core/cli/internal/commands/mcp" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" "github.com/kagent-dev/kagent/go/core/cli/internal/tui" dbcli "github.com/kagent-dev/kagent/go/core/pkg/cli/db" @@ -85,7 +85,7 @@ func newRootCommand(ctx context.Context, opts *rootOptions) *cobra.Command { rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "Verbose output") rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "Timeout") rootCmd.PersistentFlags().StringVar(&cfg.UserID, "user-id", cfg.UserID, "Caller identity used to select the server-side data partition") - installCfg := &cli.InstallCfg{ + installCfg := &commands.InstallCfg{ Connection: cfg, } @@ -94,7 +94,7 @@ func newRootCommand(ctx context.Context, opts *rootOptions) *cobra.Command { Short: "Install kagent", Long: `Install kagent`, Run: func(cmd *cobra.Command, args []string) { - cli.InstallCmd(cmd.Context(), installCfg) + commands.InstallCmd(cmd.Context(), installCfg) }, } installCmd.Flags().StringVar(&installCfg.Profile, "profile", "", "Installation profile (minimal|demo)") @@ -107,7 +107,7 @@ func newRootCommand(ctx context.Context, opts *rootOptions) *cobra.Command { Short: "Uninstall kagent", Long: `Uninstall kagent`, Run: func(cmd *cobra.Command, args []string) { - cli.UninstallCmd(cmd.Context(), cfg.Namespace) + commands.UninstallCmd(cmd.Context(), cfg.Namespace) }, } @@ -149,7 +149,7 @@ func newRootCommand(ctx context.Context, opts *rootOptions) *cobra.Command { if pf != nil { defer pf.Stop() } - cli.BugReportCmd(cfg.Namespace, cfg.Verbose) + commands.BugReportCmd(cfg.Namespace, cfg.Verbose) }, } @@ -162,7 +162,7 @@ func newRootCommand(ctx context.Context, opts *rootOptions) *cobra.Command { // versions unable to obtain from the remote kagent will be reported as "unknown" clientSet := cfg.Client() defer clientSet.Close() //nolint:errcheck - defer cli.VersionCmd(clientSet) + defer commands.VersionCmd(clientSet) if pf, _ := connection.Connect(cmd.Context(), cfg); pf != nil { defer pf.Stop() @@ -175,7 +175,7 @@ func newRootCommand(ctx context.Context, opts *rootOptions) *cobra.Command { Short: "Open the kagent dashboard", Long: `Open the kagent dashboard`, Run: func(cmd *cobra.Command, args []string) { - cli.DashboardCmd(cmd.Context(), cfg.Namespace) + commands.DashboardCmd(cmd.Context(), cfg.Namespace) }, } diff --git a/go/core/cli/cmd/kagent/main_test.go b/go/core/cli/cmd/kagent/main_test.go index 292d454c0..1836b4ea5 100644 --- a/go/core/cli/cmd/kagent/main_test.go +++ b/go/core/cli/cmd/kagent/main_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/connection" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/go/core/cli/internal/cli/agentinstance/get.go b/go/core/cli/internal/commands/agentinstance/get.go similarity index 96% rename from go/core/cli/internal/cli/agentinstance/get.go rename to go/core/cli/internal/commands/agentinstance/get.go index 7da3049e2..a88b85bef 100644 --- a/go/core/cli/internal/cli/agentinstance/get.go +++ b/go/core/cli/internal/commands/agentinstance/get.go @@ -12,8 +12,8 @@ import ( "github.com/google/uuid" "github.com/jedib0t/go-pretty/v6/table" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/connection" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/go/core/cli/internal/cli/agentinstance/get_test.go b/go/core/cli/internal/commands/agentinstance/get_test.go similarity index 98% rename from go/core/cli/internal/cli/agentinstance/get_test.go rename to go/core/cli/internal/commands/agentinstance/get_test.go index 187ff01f8..a45952f64 100644 --- a/go/core/cli/internal/cli/agentinstance/get_test.go +++ b/go/core/cli/internal/commands/agentinstance/get_test.go @@ -8,7 +8,7 @@ import ( "time" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/go/core/cli/internal/cli/agentinstance/invoke.go b/go/core/cli/internal/commands/agentinstance/invoke.go similarity index 98% rename from go/core/cli/internal/cli/agentinstance/invoke.go rename to go/core/cli/internal/commands/agentinstance/invoke.go index 9fc6d111c..7d8655284 100644 --- a/go/core/cli/internal/cli/agentinstance/invoke.go +++ b/go/core/cli/internal/commands/agentinstance/invoke.go @@ -14,8 +14,8 @@ import ( "github.com/a2aproject/a2a-go/v2/a2apb/v1/pbconv" "github.com/google/uuid" clia2a "github.com/kagent-dev/kagent/go/core/cli/internal/a2a" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/connection" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" ) var errTruncatedA2AStream = errors.New("a2a stream ended before returning a final result") diff --git a/go/core/cli/internal/cli/agentinstance/invoke_test.go b/go/core/cli/internal/commands/agentinstance/invoke_test.go similarity index 99% rename from go/core/cli/internal/cli/agentinstance/invoke_test.go rename to go/core/cli/internal/commands/agentinstance/invoke_test.go index 1ab4a371f..d2469e11b 100644 --- a/go/core/cli/internal/cli/agentinstance/invoke_test.go +++ b/go/core/cli/internal/commands/agentinstance/invoke_test.go @@ -13,7 +13,7 @@ import ( a2atype "github.com/a2aproject/a2a-go/v2/a2a" "github.com/a2aproject/a2a-go/v2/a2aclient" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/go/core/cli/internal/cli/agentinstance/lifecycle.go b/go/core/cli/internal/commands/agentinstance/lifecycle.go similarity index 96% rename from go/core/cli/internal/cli/agentinstance/lifecycle.go rename to go/core/cli/internal/commands/agentinstance/lifecycle.go index f1fce72c0..86f1d065a 100644 --- a/go/core/cli/internal/cli/agentinstance/lifecycle.go +++ b/go/core/cli/internal/commands/agentinstance/lifecycle.go @@ -8,8 +8,8 @@ import ( "github.com/google/uuid" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/connection" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" diff --git a/go/core/cli/internal/cli/agentinstance/lifecycle_test.go b/go/core/cli/internal/commands/agentinstance/lifecycle_test.go similarity index 98% rename from go/core/cli/internal/cli/agentinstance/lifecycle_test.go rename to go/core/cli/internal/commands/agentinstance/lifecycle_test.go index 9e51ebdf7..cf60f3bef 100644 --- a/go/core/cli/internal/cli/agentinstance/lifecycle_test.go +++ b/go/core/cli/internal/commands/agentinstance/lifecycle_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" diff --git a/go/core/cli/internal/cli/agenttemplate/get.go b/go/core/cli/internal/commands/agenttemplate/get.go similarity index 97% rename from go/core/cli/internal/cli/agenttemplate/get.go rename to go/core/cli/internal/commands/agenttemplate/get.go index 9fb294f2d..e64e23ae0 100644 --- a/go/core/cli/internal/cli/agenttemplate/get.go +++ b/go/core/cli/internal/commands/agenttemplate/get.go @@ -12,8 +12,8 @@ import ( "github.com/jedib0t/go-pretty/v6/table" typedapiv1alpha3 "github.com/kagent-dev/kagent/go/api/clientset/versioned/typed/api/v1alpha3" apiv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" commonk8s "github.com/kagent-dev/kagent/go/core/cli/internal/common/k8s" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/go/core/cli/internal/cli/agenttemplate/get_test.go b/go/core/cli/internal/commands/agenttemplate/get_test.go similarity index 98% rename from go/core/cli/internal/cli/agenttemplate/get_test.go rename to go/core/cli/internal/commands/agenttemplate/get_test.go index 743af6444..e3e4c19a9 100644 --- a/go/core/cli/internal/cli/agenttemplate/get_test.go +++ b/go/core/cli/internal/commands/agenttemplate/get_test.go @@ -8,7 +8,7 @@ import ( clientfake "github.com/kagent-dev/kagent/go/api/clientset/versioned/fake" apiv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" - clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/cli/output" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/go/core/cli/internal/cli/agent/bug_report.go b/go/core/cli/internal/commands/bug_report.go similarity index 99% rename from go/core/cli/internal/cli/agent/bug_report.go rename to go/core/cli/internal/commands/bug_report.go index 3c1083178..ef78934ba 100644 --- a/go/core/cli/internal/cli/agent/bug_report.go +++ b/go/core/cli/internal/commands/bug_report.go @@ -1,4 +1,4 @@ -package cli +package commands import ( "fmt" diff --git a/go/core/cli/internal/cli/agent/const.go b/go/core/cli/internal/commands/const.go similarity index 99% rename from go/core/cli/internal/cli/agent/const.go rename to go/core/cli/internal/commands/const.go index 08edd89a5..7aceb9efc 100644 --- a/go/core/cli/internal/cli/agent/const.go +++ b/go/core/cli/internal/commands/const.go @@ -1,4 +1,4 @@ -package cli +package commands import ( "os" diff --git a/go/core/cli/internal/cli/agent/dashboard.go b/go/core/cli/internal/commands/dashboard.go similarity index 96% rename from go/core/cli/internal/cli/agent/dashboard.go rename to go/core/cli/internal/commands/dashboard.go index 286ce1123..e42a5c43f 100644 --- a/go/core/cli/internal/cli/agent/dashboard.go +++ b/go/core/cli/internal/commands/dashboard.go @@ -1,6 +1,6 @@ //go:build !darwin -package cli +package commands import ( "context" diff --git a/go/core/cli/internal/cli/agent/dashboard_darwin.go b/go/core/cli/internal/commands/dashboard_darwin.go similarity index 98% rename from go/core/cli/internal/cli/agent/dashboard_darwin.go rename to go/core/cli/internal/commands/dashboard_darwin.go index eddf428f4..c679b9f6b 100644 --- a/go/core/cli/internal/cli/agent/dashboard_darwin.go +++ b/go/core/cli/internal/commands/dashboard_darwin.go @@ -1,6 +1,6 @@ //go:build darwin -package cli +package commands import ( "context" diff --git a/go/core/cli/internal/cli/envdoc/envdoc.go b/go/core/cli/internal/commands/envdoc/envdoc.go similarity index 100% rename from go/core/cli/internal/cli/envdoc/envdoc.go rename to go/core/cli/internal/commands/envdoc/envdoc.go diff --git a/go/core/cli/internal/cli/agent/install.go b/go/core/cli/internal/commands/install.go similarity index 99% rename from go/core/cli/internal/cli/agent/install.go rename to go/core/cli/internal/commands/install.go index 2702f163a..5bb9a7f06 100644 --- a/go/core/cli/internal/cli/agent/install.go +++ b/go/core/cli/internal/commands/install.go @@ -1,4 +1,4 @@ -package cli +package commands import ( "context" @@ -14,7 +14,7 @@ import ( "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/briandowns/spinner" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/connection" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" ) diff --git a/go/core/cli/internal/cli/mcp/add_tool.go b/go/core/cli/internal/commands/mcp/add_tool.go similarity index 100% rename from go/core/cli/internal/cli/mcp/add_tool.go rename to go/core/cli/internal/commands/mcp/add_tool.go diff --git a/go/core/cli/internal/cli/mcp/build.go b/go/core/cli/internal/commands/mcp/build.go similarity index 100% rename from go/core/cli/internal/cli/mcp/build.go rename to go/core/cli/internal/commands/mcp/build.go diff --git a/go/core/cli/internal/cli/mcp/build_test.go b/go/core/cli/internal/commands/mcp/build_test.go similarity index 100% rename from go/core/cli/internal/cli/mcp/build_test.go rename to go/core/cli/internal/commands/mcp/build_test.go diff --git a/go/core/cli/internal/cli/mcp/deploy.go b/go/core/cli/internal/commands/mcp/deploy.go similarity index 100% rename from go/core/cli/internal/cli/mcp/deploy.go rename to go/core/cli/internal/commands/mcp/deploy.go diff --git a/go/core/cli/internal/cli/mcp/init.go b/go/core/cli/internal/commands/mcp/init.go similarity index 100% rename from go/core/cli/internal/cli/mcp/init.go rename to go/core/cli/internal/commands/mcp/init.go diff --git a/go/core/cli/internal/cli/mcp/init_test.go b/go/core/cli/internal/commands/mcp/init_test.go similarity index 100% rename from go/core/cli/internal/cli/mcp/init_test.go rename to go/core/cli/internal/commands/mcp/init_test.go diff --git a/go/core/cli/internal/cli/mcp/inspector.go b/go/core/cli/internal/commands/mcp/inspector.go similarity index 100% rename from go/core/cli/internal/cli/mcp/inspector.go rename to go/core/cli/internal/commands/mcp/inspector.go diff --git a/go/core/cli/internal/cli/mcp/integration_test.go b/go/core/cli/internal/commands/mcp/integration_test.go similarity index 100% rename from go/core/cli/internal/cli/mcp/integration_test.go rename to go/core/cli/internal/commands/mcp/integration_test.go diff --git a/go/core/cli/internal/cli/mcp/root.go b/go/core/cli/internal/commands/mcp/root.go similarity index 100% rename from go/core/cli/internal/cli/mcp/root.go rename to go/core/cli/internal/commands/mcp/root.go diff --git a/go/core/cli/internal/cli/mcp/run.go b/go/core/cli/internal/commands/mcp/run.go similarity index 100% rename from go/core/cli/internal/cli/mcp/run.go rename to go/core/cli/internal/commands/mcp/run.go diff --git a/go/core/cli/internal/cli/mcp/run_test.go b/go/core/cli/internal/commands/mcp/run_test.go similarity index 100% rename from go/core/cli/internal/cli/mcp/run_test.go rename to go/core/cli/internal/commands/mcp/run_test.go diff --git a/go/core/cli/internal/cli/mcp/secrets.go b/go/core/cli/internal/commands/mcp/secrets.go similarity index 100% rename from go/core/cli/internal/cli/mcp/secrets.go rename to go/core/cli/internal/commands/mcp/secrets.go diff --git a/go/core/cli/internal/cli/agent/version.go b/go/core/cli/internal/commands/version.go similarity index 97% rename from go/core/cli/internal/cli/agent/version.go rename to go/core/cli/internal/commands/version.go index c9cd2adc9..b75d7a24f 100644 --- a/go/core/cli/internal/cli/agent/version.go +++ b/go/core/cli/internal/commands/version.go @@ -1,4 +1,4 @@ -package cli +package commands import ( "context" diff --git a/go/core/cli/internal/cli/connection/connection.go b/go/core/cli/internal/connection/connection.go similarity index 100% rename from go/core/cli/internal/cli/connection/connection.go rename to go/core/cli/internal/connection/connection.go diff --git a/go/core/cli/internal/cli/connection/connection_test.go b/go/core/cli/internal/connection/connection_test.go similarity index 100% rename from go/core/cli/internal/cli/connection/connection_test.go rename to go/core/cli/internal/connection/connection_test.go diff --git a/go/core/cli/internal/cli/output/output.go b/go/core/cli/internal/output/output.go similarity index 100% rename from go/core/cli/internal/cli/output/output.go rename to go/core/cli/internal/output/output.go diff --git a/go/core/cli/internal/cli/output/output_test.go b/go/core/cli/internal/output/output_test.go similarity index 100% rename from go/core/cli/internal/cli/output/output_test.go rename to go/core/cli/internal/output/output_test.go diff --git a/go/core/cli/internal/tui/workspace_test.go b/go/core/cli/internal/tui/workspace_test.go index 00f231b62..c1f84f8d6 100644 --- a/go/core/cli/internal/tui/workspace_test.go +++ b/go/core/cli/internal/tui/workspace_test.go @@ -10,7 +10,7 @@ import ( tea "github.com/charmbracelet/bubbletea" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" clia2a "github.com/kagent-dev/kagent/go/core/cli/internal/a2a" - "github.com/kagent-dev/kagent/go/core/cli/internal/cli/connection" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" From 9461143159f702348a9940fc1f75ecc391277cd8 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Wed, 26 Aug 2026 14:55:15 -0700 Subject: [PATCH 2/7] test(agentplugins): compare canonical materialization paths Signed-off-by: Cody Hartsook --- go/core/v2/agentplugins/materialize_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/go/core/v2/agentplugins/materialize_test.go b/go/core/v2/agentplugins/materialize_test.go index c76dbd927..52ac0e43a 100644 --- a/go/core/v2/agentplugins/materialize_test.go +++ b/go/core/v2/agentplugins/materialize_test.go @@ -70,8 +70,12 @@ func TestFetchSourceReusesExistingMaterialization(t *testing.T) { if err != nil { t.Fatalf("fetchSource() redownloaded existing materialization: %v", err) } - if root != destination { - t.Fatalf("fetchSource() root = %q, want %q", root, destination) + wantRoot, err := filepath.EvalSymlinks(destination) + if err != nil { + t.Fatal(err) + } + if root != wantRoot { + t.Fatalf("fetchSource() root = %q, want %q", root, wantRoot) } } From 43cdea6f2c5418c39292c5e503f3346751260c15 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Wed, 26 Aug 2026 15:33:07 -0700 Subject: [PATCH 3/7] refactor(cli): extract root command composition Signed-off-by: Cody Hartsook --- go/core/cli/cmd/kagent/main.go | 372 +----------------- go/core/cli/root.go | 353 +++++++++++++++++ go/core/cli/root_external_test.go | 14 + .../{cmd/kagent/main_test.go => root_test.go} | 112 +++--- 4 files changed, 422 insertions(+), 429 deletions(-) create mode 100644 go/core/cli/root.go create mode 100644 go/core/cli/root_external_test.go rename go/core/cli/{cmd/kagent/main_test.go => root_test.go} (56%) diff --git a/go/core/cli/cmd/kagent/main.go b/go/core/cli/cmd/kagent/main.go index bc0f9aa1e..09c3b57c2 100644 --- a/go/core/cli/cmd/kagent/main.go +++ b/go/core/cli/cmd/kagent/main.go @@ -2,386 +2,20 @@ package main import ( "context" - "errors" "fmt" "os" "os/signal" - "strconv" "syscall" - "time" - "github.com/kagent-dev/kagent/go/core/cli/internal/commands" - agentinstancecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agentinstance" - agenttemplatecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agenttemplate" - "github.com/kagent-dev/kagent/go/core/cli/internal/commands/envdoc" - "github.com/kagent-dev/kagent/go/core/cli/internal/commands/mcp" - "github.com/kagent-dev/kagent/go/core/cli/internal/connection" - "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" - "github.com/kagent-dev/kagent/go/core/cli/internal/tui" - dbcli "github.com/kagent-dev/kagent/go/core/pkg/cli/db" - dbmigrate "github.com/kagent-dev/kagent/go/core/pkg/cli/db/migrate" - "github.com/kagent-dev/kagent/go/core/pkg/migrations" - "github.com/spf13/cobra" - "golang.org/x/term" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/clientcmd" - "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/kagent-dev/kagent/go/core/cli" ) func main() { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() - // listen for signals to cancel the context throughout the application - done := make(chan os.Signal, 1) - signal.Notify(done, os.Interrupt, syscall.SIGTERM) - - go func() { - <-done - - fmt.Fprintf(os.Stderr, "kagent aborted.\n") - fmt.Fprintf(os.Stderr, "Exiting.\n") - - cancel() - }() - rootCmd := newRootCommand(ctx, defaultRootOptions()) - if err := rootCmd.ExecuteContext(ctx); err != nil { + if err := cli.Root().ExecuteContext(ctx); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) } } - -type rootOptions struct { - Connection connection.Options - OutputFormat string -} - -func defaultRootOptions() *rootOptions { - return &rootOptions{Connection: connection.DefaultOptions(), OutputFormat: "table"} -} - -func newRootCommand(ctx context.Context, opts *rootOptions) *cobra.Command { - cfg := &opts.Connection - rootCmd := &cobra.Command{ - Use: "kagent", - Short: "kagent is a CLI for kagent", - Long: "kagent is a CLI for kagent", - SilenceErrors: true, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return runInteractive(cmd, cfg) - }, - } - rootCmd.SetContext(ctx) - - rootCmd.PersistentFlags().StringVar(&cfg.KAgentURL, "kagent-url", cfg.KAgentURL, "KAgent REST URL") - rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCURL, "grpc-url", cfg.KAgentGRPCURL, "KAgent gRPC target") - rootCmd.PersistentFlags().BoolVar(&cfg.KAgentGRPCTLS, "grpc-tls", cfg.KAgentGRPCTLS, "Use TLS for KAgent gRPC") - rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCCAFile, "grpc-ca-file", cfg.KAgentGRPCCAFile, "CA certificate file for KAgent gRPC") - rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCServerName, "grpc-server-name", cfg.KAgentGRPCServerName, "TLS server name for KAgent gRPC") - rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", cfg.Namespace, "Namespace") - rootCmd.PersistentFlags().StringVarP(&opts.OutputFormat, "output-format", "o", opts.OutputFormat, "Output format") - rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "Verbose output") - rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "Timeout") - rootCmd.PersistentFlags().StringVar(&cfg.UserID, "user-id", cfg.UserID, "Caller identity used to select the server-side data partition") - installCfg := &commands.InstallCfg{ - Connection: cfg, - } - - installCmd := &cobra.Command{ - Use: "install", - Short: "Install kagent", - Long: `Install kagent`, - Run: func(cmd *cobra.Command, args []string) { - commands.InstallCmd(cmd.Context(), installCfg) - }, - } - installCmd.Flags().StringVar(&installCfg.Profile, "profile", "", "Installation profile (minimal|demo)") - _ = installCmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp - }) - - uninstallCmd := &cobra.Command{ - Use: "uninstall", - Short: "Uninstall kagent", - Long: `Uninstall kagent`, - Run: func(cmd *cobra.Command, args []string) { - commands.UninstallCmd(cmd.Context(), cfg.Namespace) - }, - } - - invokeCfg := &agentinstancecli.InvokeCfg{ - Connection: cfg, - } - - invokeCmd := &cobra.Command{ - Use: "invoke", - Short: "Invoke an AgentInstance", - Long: `Invoke an existing AgentInstance through the A2A API.`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - invokeCfg.OutputFormat = opts.OutputFormat - return agentinstancecli.InvokeCmd(cmd.Context(), invokeCfg, cmd.InOrStdin(), cmd.OutOrStdout()) - }, - Example: `kagent invoke --agent-instance 8bd650a8-9775-488f-8bc1-0d52bf7bdcab --task "Get all the pods"`, - } - - invokeCmd.Flags().StringVar(&invokeCfg.AgentInstance, "agent-instance", "", "AgentInstance ID") - invokeCmd.Flags().StringVarP(&invokeCfg.Task, "task", "t", "", "Task text") - invokeCmd.Flags().StringVarP(&invokeCfg.File, "file", "f", "", "Read task text from a file or - for stdin") - invokeCmd.Flags().BoolVarP(&invokeCfg.Stream, "stream", "S", false, "Stream the response") - invokeCmd.Flags().StringVar(&invokeCfg.Token, "token", "", "Model API key passed through as an A2A Bearer token") - _ = invokeCmd.MarkFlagRequired("agent-instance") - invokeCmd.MarkFlagsOneRequired("task", "file") - invokeCmd.MarkFlagsMutuallyExclusive("task", "file") - - bugReportCmd := &cobra.Command{ - Use: "bug-report", - Short: "Generate a bug report", - Long: `Generate a bug report`, - Run: func(cmd *cobra.Command, args []string) { - pf, err := connection.Connect(cmd.Context(), cfg) - if err != nil { - fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) - return - } - if pf != nil { - defer pf.Stop() - } - commands.BugReportCmd(cfg.Namespace, cfg.Verbose) - }, - } - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Print the kagent version", - Long: `Print the kagent version`, - Run: func(cmd *cobra.Command, args []string) { - // print out kagent CLI version regardless if a port-forward to kagent server succeeds - // versions unable to obtain from the remote kagent will be reported as "unknown" - clientSet := cfg.Client() - defer clientSet.Close() //nolint:errcheck - defer commands.VersionCmd(clientSet) - - if pf, _ := connection.Connect(cmd.Context(), cfg); pf != nil { - defer pf.Stop() - } - }, - } - - dashboardCmd := &cobra.Command{ - Use: "dashboard", - Short: "Open the kagent dashboard", - Long: `Open the kagent dashboard`, - Run: func(cmd *cobra.Command, args []string) { - commands.DashboardCmd(cmd.Context(), cfg.Namespace) - }, - } - - getCmd := &cobra.Command{ - Use: "get", - Short: "Get a kagent resource", - Long: `Get a kagent resource`, - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - return fmt.Errorf("resource type is required") - }, - } - agentInstanceGetCfg := &agentinstancecli.GetCfg{Connection: cfg} - getAgentInstanceCmd := &cobra.Command{ - Use: "agent-instance [ID]", - Short: "Get an AgentInstance or list your AgentInstances", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - agentInstanceGetCfg.OutputFormat = opts.OutputFormat - agentInstanceGetCfg.InstanceID = "" - if len(args) == 1 { - agentInstanceGetCfg.InstanceID = args[0] - } - return agentinstancecli.GetCmd(cmd.Context(), agentInstanceGetCfg, cmd.OutOrStdout()) - }, - } - getAgentInstanceCmd.Flags().Int32Var(&agentInstanceGetCfg.PageSize, "page-size", 0, "Number of AgentInstances to return (default 50, maximum 100)") - getAgentInstanceCmd.Flags().StringVar(&agentInstanceGetCfg.PageToken, "page-token", "", "Token returned by the previous page") - - agentTemplateGetCfg := &agenttemplatecli.GetCfg{} - getAgentTemplateCmd := &cobra.Command{ - Use: "agent-template [NAME]", - Short: "Get an AgentTemplate or list AgentTemplates", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - agentTemplateGetCfg.Namespace = cfg.Namespace - agentTemplateGetCfg.OutputFormat = opts.OutputFormat - agentTemplateGetCfg.Name = "" - if len(args) == 1 { - agentTemplateGetCfg.Name = args[0] - } - return agenttemplatecli.GetCmd(cmd.Context(), agentTemplateGetCfg, cmd.OutOrStdout()) - }, - } - getAgentTemplateCmd.Flags().Int64Var(&agentTemplateGetCfg.PageSize, "page-size", 0, "Number of AgentTemplates per page (0 uses 100; maximum 100)") - getAgentTemplateCmd.Flags().StringVar(&agentTemplateGetCfg.PageToken, "page-token", "", "Token returned by the previous page") - - getCmd.AddCommand(getAgentInstanceCmd, getAgentTemplateCmd) - - createCmd := &cobra.Command{ - Use: "create", - Short: "Create a kagent resource", - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - return fmt.Errorf("resource type is required") - }, - } - createAgentInstanceCfg := &agentinstancecli.CreateCfg{Connection: cfg} - createAgentInstanceCmd := &cobra.Command{ - Use: "agent-instance", - Short: "Create an AgentInstance", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - createAgentInstanceCfg.OutputFormat = opts.OutputFormat - return agentinstancecli.CreateCmd(cmd.Context(), createAgentInstanceCfg, cmd.OutOrStdout()) - }, - } - createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.Harness, "harness", "", "Harness name") - createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.AgentTemplate, "agent-template", "", "AgentTemplate name") - createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.RequestID, "request-id", "", "Idempotency key (generated when omitted)") - _ = createAgentInstanceCmd.MarkFlagRequired("harness") - _ = createAgentInstanceCmd.MarkFlagRequired("agent-template") - createCmd.AddCommand(createAgentInstanceCmd) - - deleteCmd := &cobra.Command{ - Use: "delete", - Short: "Delete a kagent resource", - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - return fmt.Errorf("resource type is required") - }, - } - deleteAgentInstanceCfg := &agentinstancecli.DeleteCfg{Connection: cfg} - deleteAgentInstanceCmd := &cobra.Command{ - Use: "agent-instance ID", - Short: "Delete an AgentInstance", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - deleteAgentInstanceCfg.OutputFormat = opts.OutputFormat - deleteAgentInstanceCfg.InstanceID = args[0] - return agentinstancecli.DeleteCmd(cmd.Context(), deleteAgentInstanceCfg, cmd.OutOrStdout()) - }, - } - deleteCmd.AddCommand(deleteAgentInstanceCmd) - - rootCmd.AddCommand(installCmd, uninstallCmd, invokeCmd, bugReportCmd, versionCmd, dashboardCmd, getCmd, createCmd, deleteCmd, mcp.NewMCPCmd(), envdoc.NewEnvCmd(), dbcli.NewCommandFromFunc(migrationSources(opts))) - - return rootCmd -} - -// vectorEnabledKey names two lookups that deliberately share it: the CLI's -// own DATABASE_VECTOR_ENABLED env var (a local operator override), and the -// controller-configmap key the chart renders — the value the controller pod -// itself consumes via envFrom. Same name, two different places. -const vectorEnabledKey = "DATABASE_VECTOR_ENABLED" - -// migrationSources resolves the built-in migration tracks when a db -// subcommand runs (never during command construction, so unrelated commands -// do no work and print no warnings). The vector track is gated, in order of -// precedence, on: the DATABASE_VECTOR_ENABLED env var in the CLI's own -// environment (explicit operator intent, works without a cluster), the -// controller's configmap on the live cluster (the same value the server -// reads), and finally the controller's default (enabled). -func migrationSources(opts *rootOptions) dbmigrate.SourcesFunc { - return func(ctx context.Context) ([]migrations.Source, error) { - vectorEnabled := true - if v := os.Getenv(vectorEnabledKey); v != "" { - b, err := strconv.ParseBool(v) - if err != nil { - fmt.Fprintf(os.Stderr, "warning: invalid %s=%q; assuming true\n", vectorEnabledKey, v) - } else { - vectorEnabled = b - } - } else if b, ok := clusterVectorEnabled(ctx, opts.Connection.Namespace); ok { - vectorEnabled = b - } - return migrations.BuiltinSources(vectorEnabled), nil - } -} - -// clusterVectorEnabled reads the vectorEnabledKey entry from the controller -// configmap in the given namespace (the same "kagent-controller" default -// naming the rest of the CLI assumes) — the cluster-side counterpart of the -// env-var override in migrationSources. When the value is used it says so on -// stderr, naming the kubeconfig context it was read from — the lookup follows -// the *current* context, so this is the operator's cue that the cluster and -// their --db-url had better be the same install. Best-effort: reports -// ok=false when no cluster is reachable, the configmap is absent, or the -// value doesn't parse — callers fall back to the default. -func clusterVectorEnabled(ctx context.Context, namespace string) (enabled, ok bool) { - restConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - clientcmd.NewDefaultClientConfigLoadingRules(), - &clientcmd.ConfigOverrides{}, - ).ClientConfig() - if err != nil { - return false, false - } - k8sClient, err := client.New(restConfig, client.Options{}) - if err != nil { - return false, false - } - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - var cm corev1.ConfigMap - if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "kagent-controller"}, &cm); err != nil { - return false, false - } - b, err := strconv.ParseBool(cm.Data[vectorEnabledKey]) - if err != nil { - return false, false - } - // Trailing blank line separates the notice from the command's stdout - // when both land on a terminal; piped stdout is unaffected. - fmt.Fprintf(os.Stderr, "resolved vector track from cluster context %q: configmap %s/kagent-controller has %s=%t (set %s to override)\n\n", - currentKubeContext(), namespace, vectorEnabledKey, b, vectorEnabledKey) - return b, true -} - -// currentKubeContext names the kubeconfig context the CLI's Kubernetes client -// dials, for operator-facing messages. Best-effort. -func currentKubeContext() string { - raw, err := clientcmd.NewDefaultClientConfigLoadingRules().Load() - if err != nil || raw.CurrentContext == "" { - return "(current kubeconfig context)" - } - return raw.CurrentContext -} - -// runInteractive launches the workspace; the TUI reads raw keys, so a redirected stream is an error. -func runInteractive(cmd *cobra.Command, cfg *connection.Options) (err error) { - if !isTerminal(cmd.InOrStdin()) || !isTerminal(cmd.OutOrStdout()) { - return errors.New("kagent requires a terminal; use `kagent get agent-instance` and `kagent invoke` for non-interactive use") - } - - client := cfg.Client() - defer func() { - err = errors.Join(err, client.Close()) - }() - - portForward, connectErr := connection.Connect(cmd.Context(), cfg) - if connectErr != nil { - return fmt.Errorf("connect to kagent: %w", connectErr) - } - if portForward != nil { - defer portForward.Stop() - } - - workspace := tui.Options{Namespace: cfg.Namespace} - if runErr := tui.RunWorkspace(cmd.Context(), workspace, client, cfg.Verbose); runErr != nil { - return fmt.Errorf("run kagent workspace: %w", runErr) - } - return nil -} - -// isTerminal reports whether a stream is backed by a TTY; a non-*os.File never is. -func isTerminal(stream any) bool { - file, ok := stream.(*os.File) - return ok && term.IsTerminal(int(file.Fd())) -} diff --git a/go/core/cli/root.go b/go/core/cli/root.go new file mode 100644 index 000000000..e6e7f8370 --- /dev/null +++ b/go/core/cli/root.go @@ -0,0 +1,353 @@ +package cli + +import ( + "context" + "errors" + "fmt" + "os" + "strconv" + "time" + + "github.com/kagent-dev/kagent/go/core/cli/internal/commands" + agentinstancecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agentinstance" + agenttemplatecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agenttemplate" + "github.com/kagent-dev/kagent/go/core/cli/internal/commands/envdoc" + "github.com/kagent-dev/kagent/go/core/cli/internal/commands/mcp" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" + "github.com/kagent-dev/kagent/go/core/cli/internal/tui" + dbcli "github.com/kagent-dev/kagent/go/core/pkg/cli/db" + dbmigrate "github.com/kagent-dev/kagent/go/core/pkg/cli/db/migrate" + "github.com/kagent-dev/kagent/go/core/pkg/migrations" + "github.com/spf13/cobra" + "golang.org/x/term" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Root creates a fresh kagent command tree. +func Root() *cobra.Command { + connectionOptions := connection.DefaultOptions() + cfg := &connectionOptions + outputFormat := "table" + rootCmd := &cobra.Command{ + Use: "kagent", + Short: "kagent is a CLI for kagent", + Long: "kagent is a CLI for kagent", + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return runInteractive(cmd, cfg) + }, + } + rootCmd.PersistentFlags().StringVar(&cfg.KAgentURL, "kagent-url", cfg.KAgentURL, "KAgent REST URL") + rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCURL, "kagent-grpc-url", cfg.KAgentGRPCURL, "KAgent gRPC target") + rootCmd.PersistentFlags().BoolVar(&cfg.KAgentGRPCTLS, "kagent-grpc-tls", cfg.KAgentGRPCTLS, "Use TLS for KAgent gRPC") + rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCCAFile, "kagent-grpc-ca-file", cfg.KAgentGRPCCAFile, "CA certificate file for KAgent gRPC") + rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCServerName, "kagent-grpc-server-name", cfg.KAgentGRPCServerName, "TLS server name for KAgent gRPC") + rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", cfg.Namespace, "Namespace") + rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", outputFormat, "Output format") + rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "Verbose output") + rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "Timeout") + rootCmd.PersistentFlags().StringVar(&cfg.UserID, "user-id", cfg.UserID, "Caller identity used to select the server-side data partition") + installCfg := &commands.InstallCfg{ + Connection: cfg, + } + + installCmd := &cobra.Command{ + Use: "install", + Short: "Install kagent", + Long: `Install kagent`, + Run: func(cmd *cobra.Command, args []string) { + commands.InstallCmd(cmd.Context(), installCfg) + }, + } + installCmd.Flags().StringVar(&installCfg.Profile, "profile", "", "Installation profile (minimal|demo)") + _ = installCmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp + }) + + uninstallCmd := &cobra.Command{ + Use: "uninstall", + Short: "Uninstall kagent", + Long: `Uninstall kagent`, + Run: func(cmd *cobra.Command, args []string) { + commands.UninstallCmd(cmd.Context(), cfg.Namespace) + }, + } + + invokeCfg := &agentinstancecli.InvokeCfg{ + Connection: cfg, + } + + invokeCmd := &cobra.Command{ + Use: "invoke", + Short: "Invoke an AgentInstance", + Long: `Invoke an existing AgentInstance through the A2A API.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + invokeCfg.OutputFormat = outputFormat + return agentinstancecli.InvokeCmd(cmd.Context(), invokeCfg, cmd.InOrStdin(), cmd.OutOrStdout()) + }, + Example: `kagent invoke --agent-instance 8bd650a8-9775-488f-8bc1-0d52bf7bdcab --task "Get all the pods"`, + } + + invokeCmd.Flags().StringVar(&invokeCfg.AgentInstance, "agent-instance", "", "AgentInstance ID") + invokeCmd.Flags().StringVarP(&invokeCfg.Task, "task", "t", "", "Task text") + invokeCmd.Flags().StringVarP(&invokeCfg.File, "file", "f", "", "Read task text from a file or - for stdin") + invokeCmd.Flags().BoolVarP(&invokeCfg.Stream, "stream", "S", false, "Stream the response") + invokeCmd.Flags().StringVar(&invokeCfg.Token, "token", "", "Model API key passed through as an A2A Bearer token") + _ = invokeCmd.MarkFlagRequired("agent-instance") + invokeCmd.MarkFlagsOneRequired("task", "file") + invokeCmd.MarkFlagsMutuallyExclusive("task", "file") + + bugReportCmd := &cobra.Command{ + Use: "bug-report", + Short: "Generate a bug report", + Long: `Generate a bug report`, + Run: func(cmd *cobra.Command, args []string) { + pf, err := connection.Connect(cmd.Context(), cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) + return + } + if pf != nil { + defer pf.Stop() + } + commands.BugReportCmd(cfg.Namespace, cfg.Verbose) + }, + } + + versionCmd := &cobra.Command{ + Use: "version", + Short: "Print the kagent version", + Long: `Print the kagent version`, + Run: func(cmd *cobra.Command, args []string) { + // print out kagent CLI version regardless if a port-forward to kagent server succeeds + // versions unable to obtain from the remote kagent will be reported as "unknown" + clientSet := cfg.Client() + defer clientSet.Close() //nolint:errcheck + defer commands.VersionCmd(clientSet) + + if pf, _ := connection.Connect(cmd.Context(), cfg); pf != nil { + defer pf.Stop() + } + }, + } + + dashboardCmd := &cobra.Command{ + Use: "dashboard", + Short: "Open the kagent dashboard", + Long: `Open the kagent dashboard`, + Run: func(cmd *cobra.Command, args []string) { + commands.DashboardCmd(cmd.Context(), cfg.Namespace) + }, + } + + getCmd := &cobra.Command{ + Use: "get", + Short: "Get a kagent resource", + Long: `Get a kagent resource`, + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + return fmt.Errorf("resource type is required") + }, + } + agentInstanceGetCfg := &agentinstancecli.GetCfg{Connection: cfg} + getAgentInstanceCmd := &cobra.Command{ + Use: "agent-instance [ID]", + Short: "Get an AgentInstance or list your AgentInstances", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + agentInstanceGetCfg.OutputFormat = outputFormat + agentInstanceGetCfg.InstanceID = "" + if len(args) == 1 { + agentInstanceGetCfg.InstanceID = args[0] + } + return agentinstancecli.GetCmd(cmd.Context(), agentInstanceGetCfg, cmd.OutOrStdout()) + }, + } + getAgentInstanceCmd.Flags().Int32Var(&agentInstanceGetCfg.PageSize, "page-size", 0, "Number of AgentInstances to return (default 50, maximum 100)") + getAgentInstanceCmd.Flags().StringVar(&agentInstanceGetCfg.PageToken, "page-token", "", "Token returned by the previous page") + + agentTemplateGetCfg := &agenttemplatecli.GetCfg{} + getAgentTemplateCmd := &cobra.Command{ + Use: "agent-template [NAME]", + Short: "Get an AgentTemplate or list AgentTemplates", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + agentTemplateGetCfg.Namespace = cfg.Namespace + agentTemplateGetCfg.OutputFormat = outputFormat + agentTemplateGetCfg.Name = "" + if len(args) == 1 { + agentTemplateGetCfg.Name = args[0] + } + return agenttemplatecli.GetCmd(cmd.Context(), agentTemplateGetCfg, cmd.OutOrStdout()) + }, + } + getAgentTemplateCmd.Flags().Int64Var(&agentTemplateGetCfg.PageSize, "page-size", 0, "Number of AgentTemplates per page (0 uses 100; maximum 100)") + getAgentTemplateCmd.Flags().StringVar(&agentTemplateGetCfg.PageToken, "page-token", "", "Token returned by the previous page") + + getCmd.AddCommand(getAgentInstanceCmd, getAgentTemplateCmd) + + createCmd := &cobra.Command{ + Use: "create", + Short: "Create a kagent resource", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + return fmt.Errorf("resource type is required") + }, + } + createAgentInstanceCfg := &agentinstancecli.CreateCfg{Connection: cfg} + createAgentInstanceCmd := &cobra.Command{ + Use: "agent-instance", + Short: "Create an AgentInstance", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + createAgentInstanceCfg.OutputFormat = outputFormat + return agentinstancecli.CreateCmd(cmd.Context(), createAgentInstanceCfg, cmd.OutOrStdout()) + }, + } + createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.Harness, "harness", "", "Harness name") + createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.AgentTemplate, "agent-template", "", "AgentTemplate name") + createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.RequestID, "request-id", "", "Idempotency key (generated when omitted)") + _ = createAgentInstanceCmd.MarkFlagRequired("harness") + _ = createAgentInstanceCmd.MarkFlagRequired("agent-template") + createCmd.AddCommand(createAgentInstanceCmd) + + deleteCmd := &cobra.Command{ + Use: "delete", + Short: "Delete a kagent resource", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + return fmt.Errorf("resource type is required") + }, + } + deleteAgentInstanceCfg := &agentinstancecli.DeleteCfg{Connection: cfg} + deleteAgentInstanceCmd := &cobra.Command{ + Use: "agent-instance ID", + Short: "Delete an AgentInstance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + deleteAgentInstanceCfg.OutputFormat = outputFormat + deleteAgentInstanceCfg.InstanceID = args[0] + return agentinstancecli.DeleteCmd(cmd.Context(), deleteAgentInstanceCfg, cmd.OutOrStdout()) + }, + } + deleteCmd.AddCommand(deleteAgentInstanceCmd) + + rootCmd.AddCommand(installCmd, uninstallCmd, invokeCmd, bugReportCmd, versionCmd, dashboardCmd, getCmd, createCmd, deleteCmd, mcp.NewMCPCmd(), envdoc.NewEnvCmd(), dbcli.NewCommandFromFunc(migrationSources(cfg))) + + return rootCmd +} + +// vectorEnabledKey names two lookups that deliberately share it: the CLI's +// own DATABASE_VECTOR_ENABLED env var (a local operator override), and the +// controller-configmap key the chart renders — the value the controller pod +// itself consumes via envFrom. Same name, two different places. +const vectorEnabledKey = "DATABASE_VECTOR_ENABLED" + +// migrationSources resolves the built-in migration tracks when a db +// subcommand runs (never during command construction, so unrelated commands +// do no work and print no warnings). The vector track is gated, in order of +// precedence, on: the DATABASE_VECTOR_ENABLED env var in the CLI's own +// environment (explicit operator intent, works without a cluster), the +// controller's configmap on the live cluster (the same value the server +// reads), and finally the controller's default (enabled). +func migrationSources(cfg *connection.Options) dbmigrate.SourcesFunc { + return func(ctx context.Context) ([]migrations.Source, error) { + vectorEnabled := true + if v := os.Getenv(vectorEnabledKey); v != "" { + b, err := strconv.ParseBool(v) + if err != nil { + fmt.Fprintf(os.Stderr, "warning: invalid %s=%q; assuming true\n", vectorEnabledKey, v) + } else { + vectorEnabled = b + } + } else if b, ok := clusterVectorEnabled(ctx, cfg.Namespace); ok { + vectorEnabled = b + } + return migrations.BuiltinSources(vectorEnabled), nil + } +} + +// clusterVectorEnabled reads the vectorEnabledKey entry from the controller +// configmap in the given namespace (the same "kagent-controller" default +// naming the rest of the CLI assumes) — the cluster-side counterpart of the +// env-var override in migrationSources. When the value is used it says so on +// stderr, naming the kubeconfig context it was read from — the lookup follows +// the *current* context, so this is the operator's cue that the cluster and +// their --db-url had better be the same install. Best-effort: reports +// ok=false when no cluster is reachable, the configmap is absent, or the +// value doesn't parse — callers fall back to the default. +func clusterVectorEnabled(ctx context.Context, namespace string) (enabled, ok bool) { + restConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}, + ).ClientConfig() + if err != nil { + return false, false + } + k8sClient, err := client.New(restConfig, client.Options{}) + if err != nil { + return false, false + } + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + var cm corev1.ConfigMap + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "kagent-controller"}, &cm); err != nil { + return false, false + } + b, err := strconv.ParseBool(cm.Data[vectorEnabledKey]) + if err != nil { + return false, false + } + // Trailing blank line separates the notice from the command's stdout + // when both land on a terminal; piped stdout is unaffected. + fmt.Fprintf(os.Stderr, "resolved vector track from cluster context %q: configmap %s/kagent-controller has %s=%t (set %s to override)\n\n", + currentKubeContext(), namespace, vectorEnabledKey, b, vectorEnabledKey) + return b, true +} + +// currentKubeContext names the kubeconfig context the CLI's Kubernetes client +// dials, for operator-facing messages. Best-effort. +func currentKubeContext() string { + raw, err := clientcmd.NewDefaultClientConfigLoadingRules().Load() + if err != nil || raw.CurrentContext == "" { + return "(current kubeconfig context)" + } + return raw.CurrentContext +} + +// runInteractive launches the workspace; the TUI reads raw keys, so a redirected stream is an error. +func runInteractive(cmd *cobra.Command, cfg *connection.Options) (err error) { + if !isTerminal(cmd.InOrStdin()) || !isTerminal(cmd.OutOrStdout()) { + return errors.New("kagent requires a terminal; use `kagent get agent-instance` and `kagent invoke` for non-interactive use") + } + + client := cfg.Client() + defer func() { + err = errors.Join(err, client.Close()) + }() + + portForward, connectErr := connection.Connect(cmd.Context(), cfg) + if connectErr != nil { + return fmt.Errorf("connect to kagent: %w", connectErr) + } + if portForward != nil { + defer portForward.Stop() + } + + workspace := tui.Options{Namespace: cfg.Namespace} + if runErr := tui.RunWorkspace(cmd.Context(), workspace, client, cfg.Verbose); runErr != nil { + return fmt.Errorf("run kagent workspace: %w", runErr) + } + return nil +} + +// isTerminal reports whether a stream is backed by a TTY; a non-*os.File never is. +func isTerminal(stream any) bool { + file, ok := stream.(*os.File) + return ok && term.IsTerminal(int(file.Fd())) +} diff --git a/go/core/cli/root_external_test.go b/go/core/cli/root_external_test.go new file mode 100644 index 000000000..662ff2e58 --- /dev/null +++ b/go/core/cli/root_external_test.go @@ -0,0 +1,14 @@ +package cli_test + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/core/cli" +) + +func TestRootIsExternallyImportable(t *testing.T) { + root := cli.Root() + if root == nil { + t.Fatal("Root() returned nil") + } +} diff --git a/go/core/cli/cmd/kagent/main_test.go b/go/core/cli/root_test.go similarity index 56% rename from go/core/cli/cmd/kagent/main_test.go rename to go/core/cli/root_test.go index 1836b4ea5..f8031b522 100644 --- a/go/core/cli/cmd/kagent/main_test.go +++ b/go/core/cli/root_test.go @@ -1,60 +1,30 @@ -package main +package cli import ( "bytes" - "context" "testing" - "time" - "github.com/kagent-dev/kagent/go/core/cli/internal/connection" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestRootCommandUsesOptionValuesAsFlagDefaults(t *testing.T) { - opts := &rootOptions{ - Connection: connection.Options{ - KAgentURL: "http://kagent.example.test", - KAgentGRPCURL: "grpc.kagent.example.test:443", - KAgentGRPCTLS: true, - KAgentGRPCCAFile: "/tmp/kagent-ca.pem", - KAgentGRPCServerName: "grpc.kagent.example.test", - Namespace: "configured-ns", - Verbose: true, - Timeout: 45 * time.Second, - UserID: "configured-user", - }, - OutputFormat: "json", - } - - rootCmd := newRootCommand(context.Background(), opts) - - assert.Equal(t, "http://kagent.example.test", rootCmd.PersistentFlags().Lookup("kagent-url").DefValue) - assert.Equal(t, "grpc.kagent.example.test:443", rootCmd.PersistentFlags().Lookup("grpc-url").DefValue) - assert.Equal(t, "true", rootCmd.PersistentFlags().Lookup("grpc-tls").DefValue) - assert.Equal(t, "/tmp/kagent-ca.pem", rootCmd.PersistentFlags().Lookup("grpc-ca-file").DefValue) - assert.Equal(t, "grpc.kagent.example.test", rootCmd.PersistentFlags().Lookup("grpc-server-name").DefValue) - assert.Equal(t, "configured-ns", rootCmd.PersistentFlags().Lookup("namespace").DefValue) - assert.Equal(t, "json", rootCmd.PersistentFlags().Lookup("output-format").DefValue) - assert.Equal(t, "true", rootCmd.PersistentFlags().Lookup("verbose").DefValue) - assert.Equal(t, "45s", rootCmd.PersistentFlags().Lookup("timeout").DefValue) - assert.Equal(t, "configured-user", rootCmd.PersistentFlags().Lookup("user-id").DefValue) - - assert.Equal(t, "configured-ns", opts.Connection.Namespace) +func TestRootCommandUsesDefaultFlagValues(t *testing.T) { + rootCmd := Root() + + assert.Equal(t, "http://localhost:8083", rootCmd.PersistentFlags().Lookup("kagent-url").DefValue) + assert.Equal(t, "localhost:8084", rootCmd.PersistentFlags().Lookup("kagent-grpc-url").DefValue) + assert.Equal(t, "false", rootCmd.PersistentFlags().Lookup("kagent-grpc-tls").DefValue) + assert.Empty(t, rootCmd.PersistentFlags().Lookup("kagent-grpc-ca-file").DefValue) + assert.Empty(t, rootCmd.PersistentFlags().Lookup("kagent-grpc-server-name").DefValue) + assert.Equal(t, "kagent", rootCmd.PersistentFlags().Lookup("namespace").DefValue) + assert.Equal(t, "table", rootCmd.PersistentFlags().Lookup("output-format").DefValue) + assert.Equal(t, "false", rootCmd.PersistentFlags().Lookup("verbose").DefValue) + assert.Equal(t, "5m0s", rootCmd.PersistentFlags().Lookup("timeout").DefValue) + assert.Equal(t, "admin@kagent.dev", rootCmd.PersistentFlags().Lookup("user-id").DefValue) } func TestRootCommandFlagsOverrideOptionValues(t *testing.T) { - opts := &rootOptions{ - Connection: connection.Options{ - KAgentURL: "http://kagent.example.test", - KAgentGRPCURL: "grpc.kagent.example.test:443", - Namespace: "configured-ns", - Timeout: 45 * time.Second, - }, - OutputFormat: "json", - } - - rootCmd := newRootCommand(context.Background(), opts) + rootCmd := Root() require.NoError(t, rootCmd.ParseFlags([]string{ "--kagent-url", "http://flag.example.test", "--grpc-url", "grpc.flag.example.test:8443", @@ -68,20 +38,42 @@ func TestRootCommandFlagsOverrideOptionValues(t *testing.T) { "--user-id", "flag-user", })) - assert.Equal(t, "http://flag.example.test", opts.Connection.KAgentURL) - assert.Equal(t, "grpc.flag.example.test:8443", opts.Connection.KAgentGRPCURL) - assert.True(t, opts.Connection.KAgentGRPCTLS) - assert.Equal(t, "/tmp/flag-ca.pem", opts.Connection.KAgentGRPCCAFile) - assert.Equal(t, "grpc.flag.example.test", opts.Connection.KAgentGRPCServerName) - assert.Equal(t, "flag-ns", opts.Connection.Namespace) - assert.Equal(t, "yaml", opts.OutputFormat) - assert.True(t, opts.Connection.Verbose) - assert.Equal(t, 10*time.Second, opts.Connection.Timeout) - assert.Equal(t, "flag-user", opts.Connection.UserID) + want := map[string]string{ + "kagent-url": "http://flag.example.test", + "kagent-grpc-url": "grpc.flag.example.test:8443", + "kagent-grpc-tls": "true", + "kagent-grpc-ca-file": "/tmp/flag-ca.pem", + "kagent-grpc-server-name": "grpc.flag.example.test", + "namespace": "flag-ns", + "output-format": "yaml", + "verbose": "true", + "timeout": "10s", + "user-id": "flag-user", + } + for name, value := range want { + assert.Equal(t, value, rootCmd.PersistentFlags().Lookup(name).Value.String()) + } +} + +func TestRootCommandAllowsNoTimeout(t *testing.T) { + rootCmd := Root() + + require.NoError(t, rootCmd.ParseFlags([]string{"--timeout", "0"})) + assert.Equal(t, "0s", rootCmd.PersistentFlags().Lookup("timeout").Value.String()) +} + +func TestRootCommandsOwnIndependentFlagState(t *testing.T) { + first := Root() + second := Root() + + require.NoError(t, first.ParseFlags([]string{"--namespace", "first"})) + + assert.Equal(t, "first", first.PersistentFlags().Lookup("namespace").Value.String()) + assert.Equal(t, "kagent", second.PersistentFlags().Lookup("namespace").Value.String()) } func TestRootCommandDoesNotValidateClientFlagsForIndependentCommand(t *testing.T) { - rootCmd := newRootCommand(t.Context(), defaultRootOptions()) + rootCmd := Root() rootCmd.SetArgs([]string{"--output-format", "yaml", "--user-id", "invalid user", "env"}) rootCmd.SetOut(&bytes.Buffer{}) @@ -89,7 +81,7 @@ func TestRootCommandDoesNotValidateClientFlagsForIndependentCommand(t *testing.T } func TestRootCommandInvokeContract(t *testing.T) { - rootCmd := newRootCommand(t.Context(), defaultRootOptions()) + rootCmd := Root() assert.True(t, rootCmd.SilenceErrors) assert.True(t, rootCmd.SilenceUsage) @@ -111,7 +103,7 @@ func TestRootCommandInvokeContract(t *testing.T) { } func TestRootCommandV2CatalogAndLifecycleContract(t *testing.T) { - rootCmd := newRootCommand(t.Context(), defaultRootOptions()) + rootCmd := Root() getTemplateCmd, _, err := rootCmd.Find([]string{"get", "agent-template"}) require.NoError(t, err) @@ -138,7 +130,7 @@ func TestRootCommandV2CatalogAndLifecycleContract(t *testing.T) { } func TestRootCommandRemovesLegacyPaths(t *testing.T) { - rootCmd := newRootCommand(t.Context(), defaultRootOptions()) + rootCmd := Root() rootCommands := make([]string, 0, len(rootCmd.Commands())) for _, command := range rootCmd.Commands() { @@ -161,7 +153,7 @@ func TestRootCommandRemovesLegacyPaths(t *testing.T) { } func TestRootCommandRequiresTerminalForInteractiveUse(t *testing.T) { - rootCmd := newRootCommand(t.Context(), defaultRootOptions()) + rootCmd := Root() rootCmd.SetArgs(nil) rootCmd.SetIn(&bytes.Buffer{}) rootCmd.SetOut(&bytes.Buffer{}) From 24f4d9e1a3d90fcb6ddafa68dce07c64179dec29 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Wed, 26 Aug 2026 16:01:36 -0700 Subject: [PATCH 4/7] refactor(cli): split command constructors Signed-off-by: Cody Hartsook --- .../commands/agentinstance/commands.go | 88 +++++++++ .../commands/agenttemplate/commands.go | 27 +++ go/core/cli/internal/commands/commands.go | 90 +++++++++ go/core/cli/root.go | 179 ++---------------- 4 files changed, 222 insertions(+), 162 deletions(-) create mode 100644 go/core/cli/internal/commands/agentinstance/commands.go create mode 100644 go/core/cli/internal/commands/agenttemplate/commands.go create mode 100644 go/core/cli/internal/commands/commands.go diff --git a/go/core/cli/internal/commands/agentinstance/commands.go b/go/core/cli/internal/commands/agentinstance/commands.go new file mode 100644 index 000000000..631224600 --- /dev/null +++ b/go/core/cli/internal/commands/agentinstance/commands.go @@ -0,0 +1,88 @@ +package agentinstance + +import ( + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + "github.com/spf13/cobra" +) + +// NewGetCommand constructs the AgentInstance get/list command. +func NewGetCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &GetCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "agent-instance [ID]", + Short: "Get an AgentInstance or list your AgentInstances", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg.OutputFormat = *outputFormat + cfg.InstanceID = "" + if len(args) == 1 { + cfg.InstanceID = args[0] + } + return GetCmd(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + cmd.Flags().Int32Var(&cfg.PageSize, "page-size", 0, "Number of AgentInstances to return (default 50, maximum 100)") + cmd.Flags().StringVar(&cfg.PageToken, "page-token", "", "Token returned by the previous page") + return cmd +} + +// NewInvokeCommand constructs the AgentInstance invoke command. +func NewInvokeCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &InvokeCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "invoke", + Short: "Invoke an AgentInstance", + Long: `Invoke an existing AgentInstance through the A2A API.`, + Args: cobra.NoArgs, + Example: `kagent invoke --agent-instance 8bd650a8-9775-488f-8bc1-0d52bf7bdcab --task "Get all the pods"`, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg.OutputFormat = *outputFormat + return InvokeCmd(cmd.Context(), cfg, cmd.InOrStdin(), cmd.OutOrStdout()) + }, + } + cmd.Flags().StringVar(&cfg.AgentInstance, "agent-instance", "", "AgentInstance ID") + cmd.Flags().StringVarP(&cfg.Task, "task", "t", "", "Task text") + cmd.Flags().StringVarP(&cfg.File, "file", "f", "", "Read task text from a file or - for stdin") + cmd.Flags().BoolVarP(&cfg.Stream, "stream", "S", false, "Stream the response") + cmd.Flags().StringVar(&cfg.Token, "token", "", "Model API key passed through as an A2A Bearer token") + _ = cmd.MarkFlagRequired("agent-instance") + cmd.MarkFlagsOneRequired("task", "file") + cmd.MarkFlagsMutuallyExclusive("task", "file") + return cmd +} + +// NewCreateCommand constructs the AgentInstance create command. +func NewCreateCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &CreateCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "agent-instance", + Short: "Create an AgentInstance", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg.OutputFormat = *outputFormat + return CreateCmd(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + cmd.Flags().StringVar(&cfg.Harness, "harness", "", "Harness name") + cmd.Flags().StringVar(&cfg.AgentTemplate, "agent-template", "", "AgentTemplate name") + cmd.Flags().StringVar(&cfg.RequestID, "request-id", "", "Idempotency key (generated when omitted)") + _ = cmd.MarkFlagRequired("harness") + _ = cmd.MarkFlagRequired("agent-template") + return cmd +} + +// NewDeleteCommand constructs the AgentInstance delete command. +func NewDeleteCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &DeleteCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "agent-instance ID", + Short: "Delete an AgentInstance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg.OutputFormat = *outputFormat + cfg.InstanceID = args[0] + return DeleteCmd(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + return cmd +} diff --git a/go/core/cli/internal/commands/agenttemplate/commands.go b/go/core/cli/internal/commands/agenttemplate/commands.go new file mode 100644 index 000000000..0cf823dc4 --- /dev/null +++ b/go/core/cli/internal/commands/agenttemplate/commands.go @@ -0,0 +1,27 @@ +package agenttemplate + +import ( + "github.com/spf13/cobra" +) + +// NewGetCommand constructs the AgentTemplate get/list command. +func NewGetCommand(namespace *string, outputFormat *string) *cobra.Command { + cfg := &GetCfg{} + cmd := &cobra.Command{ + Use: "agent-template [NAME]", + Short: "Get an AgentTemplate or list AgentTemplates", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg.Namespace = *namespace + cfg.OutputFormat = *outputFormat + cfg.Name = "" + if len(args) == 1 { + cfg.Name = args[0] + } + return GetCmd(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + cmd.Flags().Int64Var(&cfg.PageSize, "page-size", 0, "Number of AgentTemplates per page (0 uses 100; maximum 100)") + cmd.Flags().StringVar(&cfg.PageToken, "page-token", "", "Token returned by the previous page") + return cmd +} diff --git a/go/core/cli/internal/commands/commands.go b/go/core/cli/internal/commands/commands.go new file mode 100644 index 000000000..26bee11d1 --- /dev/null +++ b/go/core/cli/internal/commands/commands.go @@ -0,0 +1,90 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" + "github.com/spf13/cobra" +) + +// NewInstallCommand constructs the kagent install command. +func NewInstallCommand(connectionOptions *connection.Options) *cobra.Command { + cfg := &InstallCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "install", + Short: "Install kagent", + Long: `Install kagent`, + Run: func(cmd *cobra.Command, _ []string) { + InstallCmd(cmd.Context(), cfg) + }, + } + cmd.Flags().StringVar(&cfg.Profile, "profile", "", "Installation profile (minimal|demo)") + _ = cmd.RegisterFlagCompletionFunc("profile", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp + }) + return cmd +} + +// NewUninstallCommand constructs the kagent uninstall command. +func NewUninstallCommand(namespace *string) *cobra.Command { + return &cobra.Command{ + Use: "uninstall", + Short: "Uninstall kagent", + Long: `Uninstall kagent`, + Run: func(cmd *cobra.Command, _ []string) { + UninstallCmd(cmd.Context(), *namespace) + }, + } +} + +// NewBugReportCommand constructs the kagent bug-report command. +func NewBugReportCommand(connectionOptions *connection.Options) *cobra.Command { + return &cobra.Command{ + Use: "bug-report", + Short: "Generate a bug report", + Long: `Generate a bug report`, + Run: func(cmd *cobra.Command, _ []string) { + portForward, err := connection.Connect(cmd.Context(), connectionOptions) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) + return + } + if portForward != nil { + defer portForward.Stop() + } + BugReportCmd(connectionOptions.Namespace, connectionOptions.Verbose) + }, + } +} + +// NewVersionCommand constructs the kagent version command. +func NewVersionCommand(connectionOptions *connection.Options) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the kagent version", + Long: `Print the kagent version`, + Run: func(cmd *cobra.Command, _ []string) { + clientSet := connectionOptions.Client() + defer clientSet.Close() //nolint:errcheck + defer VersionCmd(clientSet) + + if portForward, _ := connection.Connect(cmd.Context(), connectionOptions); portForward != nil { + defer portForward.Stop() + } + }, + } +} + +// NewDashboardCommand constructs the kagent dashboard command. +func NewDashboardCommand(namespace *string) *cobra.Command { + return &cobra.Command{ + Use: "dashboard", + Short: "Open the kagent dashboard", + Long: `Open the kagent dashboard`, + Run: func(cmd *cobra.Command, _ []string) { + DashboardCmd(cmd.Context(), *namespace) + }, + } +} diff --git a/go/core/cli/root.go b/go/core/cli/root.go index e6e7f8370..ca1aaed6e 100644 --- a/go/core/cli/root.go +++ b/go/core/cli/root.go @@ -14,7 +14,6 @@ import ( "github.com/kagent-dev/kagent/go/core/cli/internal/commands/envdoc" "github.com/kagent-dev/kagent/go/core/cli/internal/commands/mcp" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" - "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" "github.com/kagent-dev/kagent/go/core/cli/internal/tui" dbcli "github.com/kagent-dev/kagent/go/core/pkg/cli/db" dbmigrate "github.com/kagent-dev/kagent/go/core/pkg/cli/db/migrate" @@ -51,100 +50,6 @@ func Root() *cobra.Command { rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "Verbose output") rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "Timeout") rootCmd.PersistentFlags().StringVar(&cfg.UserID, "user-id", cfg.UserID, "Caller identity used to select the server-side data partition") - installCfg := &commands.InstallCfg{ - Connection: cfg, - } - - installCmd := &cobra.Command{ - Use: "install", - Short: "Install kagent", - Long: `Install kagent`, - Run: func(cmd *cobra.Command, args []string) { - commands.InstallCmd(cmd.Context(), installCfg) - }, - } - installCmd.Flags().StringVar(&installCfg.Profile, "profile", "", "Installation profile (minimal|demo)") - _ = installCmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp - }) - - uninstallCmd := &cobra.Command{ - Use: "uninstall", - Short: "Uninstall kagent", - Long: `Uninstall kagent`, - Run: func(cmd *cobra.Command, args []string) { - commands.UninstallCmd(cmd.Context(), cfg.Namespace) - }, - } - - invokeCfg := &agentinstancecli.InvokeCfg{ - Connection: cfg, - } - - invokeCmd := &cobra.Command{ - Use: "invoke", - Short: "Invoke an AgentInstance", - Long: `Invoke an existing AgentInstance through the A2A API.`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - invokeCfg.OutputFormat = outputFormat - return agentinstancecli.InvokeCmd(cmd.Context(), invokeCfg, cmd.InOrStdin(), cmd.OutOrStdout()) - }, - Example: `kagent invoke --agent-instance 8bd650a8-9775-488f-8bc1-0d52bf7bdcab --task "Get all the pods"`, - } - - invokeCmd.Flags().StringVar(&invokeCfg.AgentInstance, "agent-instance", "", "AgentInstance ID") - invokeCmd.Flags().StringVarP(&invokeCfg.Task, "task", "t", "", "Task text") - invokeCmd.Flags().StringVarP(&invokeCfg.File, "file", "f", "", "Read task text from a file or - for stdin") - invokeCmd.Flags().BoolVarP(&invokeCfg.Stream, "stream", "S", false, "Stream the response") - invokeCmd.Flags().StringVar(&invokeCfg.Token, "token", "", "Model API key passed through as an A2A Bearer token") - _ = invokeCmd.MarkFlagRequired("agent-instance") - invokeCmd.MarkFlagsOneRequired("task", "file") - invokeCmd.MarkFlagsMutuallyExclusive("task", "file") - - bugReportCmd := &cobra.Command{ - Use: "bug-report", - Short: "Generate a bug report", - Long: `Generate a bug report`, - Run: func(cmd *cobra.Command, args []string) { - pf, err := connection.Connect(cmd.Context(), cfg) - if err != nil { - fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) - return - } - if pf != nil { - defer pf.Stop() - } - commands.BugReportCmd(cfg.Namespace, cfg.Verbose) - }, - } - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Print the kagent version", - Long: `Print the kagent version`, - Run: func(cmd *cobra.Command, args []string) { - // print out kagent CLI version regardless if a port-forward to kagent server succeeds - // versions unable to obtain from the remote kagent will be reported as "unknown" - clientSet := cfg.Client() - defer clientSet.Close() //nolint:errcheck - defer commands.VersionCmd(clientSet) - - if pf, _ := connection.Connect(cmd.Context(), cfg); pf != nil { - defer pf.Stop() - } - }, - } - - dashboardCmd := &cobra.Command{ - Use: "dashboard", - Short: "Open the kagent dashboard", - Long: `Open the kagent dashboard`, - Run: func(cmd *cobra.Command, args []string) { - commands.DashboardCmd(cmd.Context(), cfg.Namespace) - }, - } - getCmd := &cobra.Command{ Use: "get", Short: "Get a kagent resource", @@ -154,43 +59,6 @@ func Root() *cobra.Command { return fmt.Errorf("resource type is required") }, } - agentInstanceGetCfg := &agentinstancecli.GetCfg{Connection: cfg} - getAgentInstanceCmd := &cobra.Command{ - Use: "agent-instance [ID]", - Short: "Get an AgentInstance or list your AgentInstances", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - agentInstanceGetCfg.OutputFormat = outputFormat - agentInstanceGetCfg.InstanceID = "" - if len(args) == 1 { - agentInstanceGetCfg.InstanceID = args[0] - } - return agentinstancecli.GetCmd(cmd.Context(), agentInstanceGetCfg, cmd.OutOrStdout()) - }, - } - getAgentInstanceCmd.Flags().Int32Var(&agentInstanceGetCfg.PageSize, "page-size", 0, "Number of AgentInstances to return (default 50, maximum 100)") - getAgentInstanceCmd.Flags().StringVar(&agentInstanceGetCfg.PageToken, "page-token", "", "Token returned by the previous page") - - agentTemplateGetCfg := &agenttemplatecli.GetCfg{} - getAgentTemplateCmd := &cobra.Command{ - Use: "agent-template [NAME]", - Short: "Get an AgentTemplate or list AgentTemplates", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - agentTemplateGetCfg.Namespace = cfg.Namespace - agentTemplateGetCfg.OutputFormat = outputFormat - agentTemplateGetCfg.Name = "" - if len(args) == 1 { - agentTemplateGetCfg.Name = args[0] - } - return agenttemplatecli.GetCmd(cmd.Context(), agentTemplateGetCfg, cmd.OutOrStdout()) - }, - } - getAgentTemplateCmd.Flags().Int64Var(&agentTemplateGetCfg.PageSize, "page-size", 0, "Number of AgentTemplates per page (0 uses 100; maximum 100)") - getAgentTemplateCmd.Flags().StringVar(&agentTemplateGetCfg.PageToken, "page-token", "", "Token returned by the previous page") - - getCmd.AddCommand(getAgentInstanceCmd, getAgentTemplateCmd) - createCmd := &cobra.Command{ Use: "create", Short: "Create a kagent resource", @@ -199,23 +67,6 @@ func Root() *cobra.Command { return fmt.Errorf("resource type is required") }, } - createAgentInstanceCfg := &agentinstancecli.CreateCfg{Connection: cfg} - createAgentInstanceCmd := &cobra.Command{ - Use: "agent-instance", - Short: "Create an AgentInstance", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - createAgentInstanceCfg.OutputFormat = outputFormat - return agentinstancecli.CreateCmd(cmd.Context(), createAgentInstanceCfg, cmd.OutOrStdout()) - }, - } - createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.Harness, "harness", "", "Harness name") - createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.AgentTemplate, "agent-template", "", "AgentTemplate name") - createAgentInstanceCmd.Flags().StringVar(&createAgentInstanceCfg.RequestID, "request-id", "", "Idempotency key (generated when omitted)") - _ = createAgentInstanceCmd.MarkFlagRequired("harness") - _ = createAgentInstanceCmd.MarkFlagRequired("agent-template") - createCmd.AddCommand(createAgentInstanceCmd) - deleteCmd := &cobra.Command{ Use: "delete", Short: "Delete a kagent resource", @@ -224,20 +75,24 @@ func Root() *cobra.Command { return fmt.Errorf("resource type is required") }, } - deleteAgentInstanceCfg := &agentinstancecli.DeleteCfg{Connection: cfg} - deleteAgentInstanceCmd := &cobra.Command{ - Use: "agent-instance ID", - Short: "Delete an AgentInstance", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - deleteAgentInstanceCfg.OutputFormat = outputFormat - deleteAgentInstanceCfg.InstanceID = args[0] - return agentinstancecli.DeleteCmd(cmd.Context(), deleteAgentInstanceCfg, cmd.OutOrStdout()) - }, - } - deleteCmd.AddCommand(deleteAgentInstanceCmd) - rootCmd.AddCommand(installCmd, uninstallCmd, invokeCmd, bugReportCmd, versionCmd, dashboardCmd, getCmd, createCmd, deleteCmd, mcp.NewMCPCmd(), envdoc.NewEnvCmd(), dbcli.NewCommandFromFunc(migrationSources(cfg))) + // Add subcommands to the respective parent commands + getCmd.AddCommand(agentinstancecli.NewGetCommand(cfg, &outputFormat)) + getCmd.AddCommand(agenttemplatecli.NewGetCommand(&cfg.Namespace, &outputFormat)) + createCmd.AddCommand(agentinstancecli.NewCreateCommand(cfg, &outputFormat)) + deleteCmd.AddCommand(agentinstancecli.NewDeleteCommand(cfg, &outputFormat)) + rootCmd.AddCommand(commands.NewInstallCommand(cfg)) + rootCmd.AddCommand(commands.NewUninstallCommand(&cfg.Namespace)) + rootCmd.AddCommand(agentinstancecli.NewInvokeCommand(cfg, &outputFormat)) + rootCmd.AddCommand(commands.NewBugReportCommand(cfg)) + rootCmd.AddCommand(commands.NewVersionCommand(cfg)) + rootCmd.AddCommand(commands.NewDashboardCommand(&cfg.Namespace)) + rootCmd.AddCommand(getCmd) + rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(deleteCmd) + rootCmd.AddCommand(mcp.NewMCPCmd()) + rootCmd.AddCommand(envdoc.NewEnvCmd()) + rootCmd.AddCommand(dbcli.NewCommandFromFunc(migrationSources(cfg))) return rootCmd } From 751b5299427147b5c0f497c67fc80809b7677ffc Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Thu, 27 Aug 2026 07:50:44 -0700 Subject: [PATCH 5/7] refactor(cli): colocate command constructors Signed-off-by: Cody Hartsook --- go/core/cli/interactive.go | 44 ++++++ .../commands/agentinstance/commands.go | 88 ----------- .../internal/commands/agentinstance/get.go | 26 +++- .../internal/commands/agentinstance/invoke.go | 28 +++- .../commands/agentinstance/lifecycle.go | 45 +++++- .../commands/agenttemplate/commands.go | 27 ---- .../internal/commands/agenttemplate/get.go | 27 +++- go/core/cli/internal/commands/bug_report.go | 24 ++- go/core/cli/internal/commands/commands.go | 90 ----------- go/core/cli/internal/commands/dashboard.go | 21 +-- .../cli/internal/commands/dashboard_darwin.go | 2 +- .../cli/internal/commands/dashboard_other.go | 16 ++ go/core/cli/internal/commands/db/db.go | 102 ++++++++++++ go/core/cli/internal/commands/install.go | 35 ++++- go/core/cli/internal/commands/version.go | 22 ++- go/core/cli/root.go | 146 ++---------------- go/core/cli/root_external_test.go | 14 -- go/core/cli/root_test.go | 23 +-- 18 files changed, 392 insertions(+), 388 deletions(-) create mode 100644 go/core/cli/interactive.go delete mode 100644 go/core/cli/internal/commands/agentinstance/commands.go delete mode 100644 go/core/cli/internal/commands/agenttemplate/commands.go delete mode 100644 go/core/cli/internal/commands/commands.go create mode 100644 go/core/cli/internal/commands/dashboard_other.go create mode 100644 go/core/cli/internal/commands/db/db.go delete mode 100644 go/core/cli/root_external_test.go diff --git a/go/core/cli/interactive.go b/go/core/cli/interactive.go new file mode 100644 index 000000000..c0e54bbc7 --- /dev/null +++ b/go/core/cli/interactive.go @@ -0,0 +1,44 @@ +package cli + +import ( + "errors" + "fmt" + "os" + + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + "github.com/kagent-dev/kagent/go/core/cli/internal/tui" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +// runInteractive launches the workspace; the TUI reads raw keys, so a redirected stream is an error. +func runInteractive(cmd *cobra.Command, cfg *connection.Options) (err error) { + if !isTerminal(cmd.InOrStdin()) || !isTerminal(cmd.OutOrStdout()) { + return errors.New("kagent requires a terminal; use `kagent get agent-instance` and `kagent invoke` for non-interactive use") + } + + client := cfg.Client() + defer func() { + err = errors.Join(err, client.Close()) + }() + + portForward, connectErr := connection.Connect(cmd.Context(), cfg) + if connectErr != nil { + return fmt.Errorf("connect to kagent: %w", connectErr) + } + if portForward != nil { + defer portForward.Stop() + } + + workspace := tui.Options{Namespace: cfg.Namespace} + if runErr := tui.RunWorkspace(cmd.Context(), workspace, client, cfg.Verbose); runErr != nil { + return fmt.Errorf("run kagent workspace: %w", runErr) + } + return nil +} + +// isTerminal reports whether a stream is backed by a TTY; a non-*os.File never is. +func isTerminal(stream any) bool { + file, ok := stream.(*os.File) + return ok && term.IsTerminal(int(file.Fd())) +} diff --git a/go/core/cli/internal/commands/agentinstance/commands.go b/go/core/cli/internal/commands/agentinstance/commands.go deleted file mode 100644 index 631224600..000000000 --- a/go/core/cli/internal/commands/agentinstance/commands.go +++ /dev/null @@ -1,88 +0,0 @@ -package agentinstance - -import ( - "github.com/kagent-dev/kagent/go/core/cli/internal/connection" - "github.com/spf13/cobra" -) - -// NewGetCommand constructs the AgentInstance get/list command. -func NewGetCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &GetCfg{Connection: connectionOptions} - cmd := &cobra.Command{ - Use: "agent-instance [ID]", - Short: "Get an AgentInstance or list your AgentInstances", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - cfg.OutputFormat = *outputFormat - cfg.InstanceID = "" - if len(args) == 1 { - cfg.InstanceID = args[0] - } - return GetCmd(cmd.Context(), cfg, cmd.OutOrStdout()) - }, - } - cmd.Flags().Int32Var(&cfg.PageSize, "page-size", 0, "Number of AgentInstances to return (default 50, maximum 100)") - cmd.Flags().StringVar(&cfg.PageToken, "page-token", "", "Token returned by the previous page") - return cmd -} - -// NewInvokeCommand constructs the AgentInstance invoke command. -func NewInvokeCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &InvokeCfg{Connection: connectionOptions} - cmd := &cobra.Command{ - Use: "invoke", - Short: "Invoke an AgentInstance", - Long: `Invoke an existing AgentInstance through the A2A API.`, - Args: cobra.NoArgs, - Example: `kagent invoke --agent-instance 8bd650a8-9775-488f-8bc1-0d52bf7bdcab --task "Get all the pods"`, - RunE: func(cmd *cobra.Command, _ []string) error { - cfg.OutputFormat = *outputFormat - return InvokeCmd(cmd.Context(), cfg, cmd.InOrStdin(), cmd.OutOrStdout()) - }, - } - cmd.Flags().StringVar(&cfg.AgentInstance, "agent-instance", "", "AgentInstance ID") - cmd.Flags().StringVarP(&cfg.Task, "task", "t", "", "Task text") - cmd.Flags().StringVarP(&cfg.File, "file", "f", "", "Read task text from a file or - for stdin") - cmd.Flags().BoolVarP(&cfg.Stream, "stream", "S", false, "Stream the response") - cmd.Flags().StringVar(&cfg.Token, "token", "", "Model API key passed through as an A2A Bearer token") - _ = cmd.MarkFlagRequired("agent-instance") - cmd.MarkFlagsOneRequired("task", "file") - cmd.MarkFlagsMutuallyExclusive("task", "file") - return cmd -} - -// NewCreateCommand constructs the AgentInstance create command. -func NewCreateCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &CreateCfg{Connection: connectionOptions} - cmd := &cobra.Command{ - Use: "agent-instance", - Short: "Create an AgentInstance", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - cfg.OutputFormat = *outputFormat - return CreateCmd(cmd.Context(), cfg, cmd.OutOrStdout()) - }, - } - cmd.Flags().StringVar(&cfg.Harness, "harness", "", "Harness name") - cmd.Flags().StringVar(&cfg.AgentTemplate, "agent-template", "", "AgentTemplate name") - cmd.Flags().StringVar(&cfg.RequestID, "request-id", "", "Idempotency key (generated when omitted)") - _ = cmd.MarkFlagRequired("harness") - _ = cmd.MarkFlagRequired("agent-template") - return cmd -} - -// NewDeleteCommand constructs the AgentInstance delete command. -func NewDeleteCommand(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &DeleteCfg{Connection: connectionOptions} - cmd := &cobra.Command{ - Use: "agent-instance ID", - Short: "Delete an AgentInstance", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - cfg.OutputFormat = *outputFormat - cfg.InstanceID = args[0] - return DeleteCmd(cmd.Context(), cfg, cmd.OutOrStdout()) - }, - } - return cmd -} diff --git a/go/core/cli/internal/commands/agentinstance/get.go b/go/core/cli/internal/commands/agentinstance/get.go index a88b85bef..2e9757281 100644 --- a/go/core/cli/internal/commands/agentinstance/get.go +++ b/go/core/cli/internal/commands/agentinstance/get.go @@ -14,6 +14,7 @@ import ( apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" + "github.com/spf13/cobra" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -33,8 +34,8 @@ type GetCfg struct { PageToken string } -// GetCmd gets one AgentInstance or lists the caller's AgentInstances. -func GetCmd(ctx context.Context, cfg *GetCfg, out io.Writer) (err error) { +// runGet gets one AgentInstance or lists the caller's AgentInstances. +func runGet(ctx context.Context, cfg *GetCfg, out io.Writer) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err @@ -154,3 +155,24 @@ func formatTimestamp(timestamp *timestamppb.Timestamp) string { } return timestamp.AsTime().UTC().Format(time.RFC3339) } + +// NewGetCmd constructs the AgentInstance get/list command. +func NewGetCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &GetCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "agent-instance [ID]", + Short: "Get an AgentInstance or list your AgentInstances", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg.OutputFormat = *outputFormat + cfg.InstanceID = "" + if len(args) == 1 { + cfg.InstanceID = args[0] + } + return runGet(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + cmd.Flags().Int32Var(&cfg.PageSize, "page-size", 0, "Number of AgentInstances to return (default 50, maximum 100)") + cmd.Flags().StringVar(&cfg.PageToken, "page-token", "", "Token returned by the previous page") + return cmd +} diff --git a/go/core/cli/internal/commands/agentinstance/invoke.go b/go/core/cli/internal/commands/agentinstance/invoke.go index 7d8655284..e05b98f23 100644 --- a/go/core/cli/internal/commands/agentinstance/invoke.go +++ b/go/core/cli/internal/commands/agentinstance/invoke.go @@ -16,6 +16,7 @@ import ( clia2a "github.com/kagent-dev/kagent/go/core/cli/internal/a2a" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" + "github.com/spf13/cobra" ) var errTruncatedA2AStream = errors.New("a2a stream ended before returning a final result") @@ -30,7 +31,7 @@ type InvokeCfg struct { Token string } -func InvokeCmd(ctx context.Context, cfg *InvokeCfg, in io.Reader, out io.Writer) (err error) { +func runInvoke(ctx context.Context, cfg *InvokeCfg, in io.Reader, out io.Writer) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err @@ -327,3 +328,28 @@ func sendResultError(result a2atype.SendMessageResult) error { return fmt.Errorf("AgentInstance task %s returned before reaching a final state: %s", task.ID, task.Status.State) } } + +// NewInvokeCmd constructs the AgentInstance invoke command. +func NewInvokeCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &InvokeCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "invoke", + Short: "Invoke an AgentInstance", + Long: `Invoke an existing AgentInstance through the A2A API.`, + Args: cobra.NoArgs, + Example: `kagent invoke --agent-instance 8bd650a8-9775-488f-8bc1-0d52bf7bdcab --task "Get all the pods"`, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg.OutputFormat = *outputFormat + return runInvoke(cmd.Context(), cfg, cmd.InOrStdin(), cmd.OutOrStdout()) + }, + } + cmd.Flags().StringVar(&cfg.AgentInstance, "agent-instance", "", "AgentInstance ID") + cmd.Flags().StringVarP(&cfg.Task, "task", "t", "", "Task text") + cmd.Flags().StringVarP(&cfg.File, "file", "f", "", "Read task text from a file or - for stdin") + cmd.Flags().BoolVarP(&cfg.Stream, "stream", "S", false, "Stream the response") + cmd.Flags().StringVar(&cfg.Token, "token", "", "Model API key passed through as an A2A Bearer token") + _ = cmd.MarkFlagRequired("agent-instance") + cmd.MarkFlagsOneRequired("task", "file") + cmd.MarkFlagsMutuallyExclusive("task", "file") + return cmd +} diff --git a/go/core/cli/internal/commands/agentinstance/lifecycle.go b/go/core/cli/internal/commands/agentinstance/lifecycle.go index 86f1d065a..2ff79d16b 100644 --- a/go/core/cli/internal/commands/agentinstance/lifecycle.go +++ b/go/core/cli/internal/commands/agentinstance/lifecycle.go @@ -10,6 +10,7 @@ import ( apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" + "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" @@ -36,8 +37,8 @@ type DeleteCfg struct { InstanceID string } -// CreateCmd creates an AgentInstance. -func CreateCmd(ctx context.Context, cfg *CreateCfg, out io.Writer) (err error) { +// runCreate creates an AgentInstance. +func runCreate(ctx context.Context, cfg *CreateCfg, out io.Writer) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err @@ -65,8 +66,8 @@ func ensureRequestID(cfg *CreateCfg) { } } -// DeleteCmd deletes an AgentInstance. -func DeleteCmd(ctx context.Context, cfg *DeleteCfg, out io.Writer) (err error) { +// runDelete deletes an AgentInstance. +func runDelete(ctx context.Context, cfg *DeleteCfg, out io.Writer) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err @@ -141,3 +142,39 @@ func writeLifecycleResult( } return writeInstancesTable(w, []*apiv1alpha1.AgentInstance{instance}, "") } + +// NewCreateCmd constructs the AgentInstance create command. +func NewCreateCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &CreateCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "agent-instance", + Short: "Create an AgentInstance", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg.OutputFormat = *outputFormat + return runCreate(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + cmd.Flags().StringVar(&cfg.Harness, "harness", "", "Harness name") + cmd.Flags().StringVar(&cfg.AgentTemplate, "agent-template", "", "AgentTemplate name") + cmd.Flags().StringVar(&cfg.RequestID, "request-id", "", "Idempotency key (generated when omitted)") + _ = cmd.MarkFlagRequired("harness") + _ = cmd.MarkFlagRequired("agent-template") + return cmd +} + +// NewDeleteCmd constructs the AgentInstance delete command. +func NewDeleteCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { + cfg := &DeleteCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "agent-instance ID", + Short: "Delete an AgentInstance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg.OutputFormat = *outputFormat + cfg.InstanceID = args[0] + return runDelete(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + return cmd +} diff --git a/go/core/cli/internal/commands/agenttemplate/commands.go b/go/core/cli/internal/commands/agenttemplate/commands.go deleted file mode 100644 index 0cf823dc4..000000000 --- a/go/core/cli/internal/commands/agenttemplate/commands.go +++ /dev/null @@ -1,27 +0,0 @@ -package agenttemplate - -import ( - "github.com/spf13/cobra" -) - -// NewGetCommand constructs the AgentTemplate get/list command. -func NewGetCommand(namespace *string, outputFormat *string) *cobra.Command { - cfg := &GetCfg{} - cmd := &cobra.Command{ - Use: "agent-template [NAME]", - Short: "Get an AgentTemplate or list AgentTemplates", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - cfg.Namespace = *namespace - cfg.OutputFormat = *outputFormat - cfg.Name = "" - if len(args) == 1 { - cfg.Name = args[0] - } - return GetCmd(cmd.Context(), cfg, cmd.OutOrStdout()) - }, - } - cmd.Flags().Int64Var(&cfg.PageSize, "page-size", 0, "Number of AgentTemplates per page (0 uses 100; maximum 100)") - cmd.Flags().StringVar(&cfg.PageToken, "page-token", "", "Token returned by the previous page") - return cmd -} diff --git a/go/core/cli/internal/commands/agenttemplate/get.go b/go/core/cli/internal/commands/agenttemplate/get.go index e64e23ae0..c97778c50 100644 --- a/go/core/cli/internal/commands/agenttemplate/get.go +++ b/go/core/cli/internal/commands/agenttemplate/get.go @@ -14,6 +14,7 @@ import ( apiv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" commonk8s "github.com/kagent-dev/kagent/go/core/cli/internal/common/k8s" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" + "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -29,8 +30,8 @@ type GetCfg struct { PageToken string } -// GetCmd gets one AgentTemplate or lists AgentTemplates through Kubernetes. -func GetCmd(ctx context.Context, cfg *GetCfg, out io.Writer) error { +// runGet gets one AgentTemplate or lists AgentTemplates through Kubernetes. +func runGet(ctx context.Context, cfg *GetCfg, out io.Writer) error { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err @@ -122,3 +123,25 @@ func writeTemplatesTable(w io.Writer, templates []apiv1alpha3.AgentTemplate, lis } return nil } + +// NewGetCmd constructs the AgentTemplate get/list command. +func NewGetCmd(namespace *string, outputFormat *string) *cobra.Command { + cfg := &GetCfg{} + cmd := &cobra.Command{ + Use: "agent-template [NAME]", + Short: "Get an AgentTemplate or list AgentTemplates", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg.Namespace = *namespace + cfg.OutputFormat = *outputFormat + cfg.Name = "" + if len(args) == 1 { + cfg.Name = args[0] + } + return runGet(cmd.Context(), cfg, cmd.OutOrStdout()) + }, + } + cmd.Flags().Int64Var(&cfg.PageSize, "page-size", 0, "Number of AgentTemplates per page (0 uses 100; maximum 100)") + cmd.Flags().StringVar(&cfg.PageToken, "page-token", "", "Token returned by the previous page") + return cmd +} diff --git a/go/core/cli/internal/commands/bug_report.go b/go/core/cli/internal/commands/bug_report.go index ef78934ba..9908894cc 100644 --- a/go/core/cli/internal/commands/bug_report.go +++ b/go/core/cli/internal/commands/bug_report.go @@ -8,9 +8,11 @@ import ( "time" commonexec "github.com/kagent-dev/kagent/go/core/cli/internal/common/exec" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + "github.com/spf13/cobra" ) -func BugReportCmd(namespace string, verbose bool) { +func runBugReport(namespace string, verbose bool) { // Create a temporary directory for bug report timestamp := time.Now().Format("20060102-150405") reportDir := fmt.Sprintf("kagent-bug-report-%s", timestamp) @@ -116,3 +118,23 @@ func BugReportCmd(namespace string, verbose bool) { fmt.Printf("Bug report generated in directory: %s\n", reportDir) fmt.Println("WARNING: Please review and scrub any sensitive information from agent.yaml before sharing the bug report.") } + +// NewBugReportCmd constructs the kagent bug-report command. +func NewBugReportCmd(connectionOptions *connection.Options) *cobra.Command { + return &cobra.Command{ + Use: "bug-report", + Short: "Generate a bug report", + Long: `Generate a bug report`, + Run: func(cmd *cobra.Command, _ []string) { + portForward, err := connection.Connect(cmd.Context(), connectionOptions) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) + return + } + if portForward != nil { + defer portForward.Stop() + } + runBugReport(connectionOptions.Namespace, connectionOptions.Verbose) + }, + } +} diff --git a/go/core/cli/internal/commands/commands.go b/go/core/cli/internal/commands/commands.go deleted file mode 100644 index 26bee11d1..000000000 --- a/go/core/cli/internal/commands/commands.go +++ /dev/null @@ -1,90 +0,0 @@ -package commands - -import ( - "fmt" - "os" - - "github.com/kagent-dev/kagent/go/core/cli/internal/connection" - "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" - "github.com/spf13/cobra" -) - -// NewInstallCommand constructs the kagent install command. -func NewInstallCommand(connectionOptions *connection.Options) *cobra.Command { - cfg := &InstallCfg{Connection: connectionOptions} - cmd := &cobra.Command{ - Use: "install", - Short: "Install kagent", - Long: `Install kagent`, - Run: func(cmd *cobra.Command, _ []string) { - InstallCmd(cmd.Context(), cfg) - }, - } - cmd.Flags().StringVar(&cfg.Profile, "profile", "", "Installation profile (minimal|demo)") - _ = cmd.RegisterFlagCompletionFunc("profile", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp - }) - return cmd -} - -// NewUninstallCommand constructs the kagent uninstall command. -func NewUninstallCommand(namespace *string) *cobra.Command { - return &cobra.Command{ - Use: "uninstall", - Short: "Uninstall kagent", - Long: `Uninstall kagent`, - Run: func(cmd *cobra.Command, _ []string) { - UninstallCmd(cmd.Context(), *namespace) - }, - } -} - -// NewBugReportCommand constructs the kagent bug-report command. -func NewBugReportCommand(connectionOptions *connection.Options) *cobra.Command { - return &cobra.Command{ - Use: "bug-report", - Short: "Generate a bug report", - Long: `Generate a bug report`, - Run: func(cmd *cobra.Command, _ []string) { - portForward, err := connection.Connect(cmd.Context(), connectionOptions) - if err != nil { - fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) - return - } - if portForward != nil { - defer portForward.Stop() - } - BugReportCmd(connectionOptions.Namespace, connectionOptions.Verbose) - }, - } -} - -// NewVersionCommand constructs the kagent version command. -func NewVersionCommand(connectionOptions *connection.Options) *cobra.Command { - return &cobra.Command{ - Use: "version", - Short: "Print the kagent version", - Long: `Print the kagent version`, - Run: func(cmd *cobra.Command, _ []string) { - clientSet := connectionOptions.Client() - defer clientSet.Close() //nolint:errcheck - defer VersionCmd(clientSet) - - if portForward, _ := connection.Connect(cmd.Context(), connectionOptions); portForward != nil { - defer portForward.Stop() - } - }, - } -} - -// NewDashboardCommand constructs the kagent dashboard command. -func NewDashboardCommand(namespace *string) *cobra.Command { - return &cobra.Command{ - Use: "dashboard", - Short: "Open the kagent dashboard", - Long: `Open the kagent dashboard`, - Run: func(cmd *cobra.Command, _ []string) { - DashboardCmd(cmd.Context(), *namespace) - }, - } -} diff --git a/go/core/cli/internal/commands/dashboard.go b/go/core/cli/internal/commands/dashboard.go index e42a5c43f..5abcef75b 100644 --- a/go/core/cli/internal/commands/dashboard.go +++ b/go/core/cli/internal/commands/dashboard.go @@ -1,16 +1,17 @@ -//go:build !darwin - package commands import ( - "context" - "fmt" - "os" + "github.com/spf13/cobra" ) -func DashboardCmd(ctx context.Context, namespace string) { - fmt.Fprintln(os.Stderr, "Dashboard is not available on this platform") - fmt.Fprintln(os.Stderr, "You can easily start the dashboard by running:") - fmt.Fprintf(os.Stderr, "kubectl port-forward -n %s service/kagent-ui 8082:8080\n", namespace) - fmt.Fprintln(os.Stderr, "and then opening http://localhost:8082 in your browser") +// NewDashboardCmd constructs the kagent dashboard command. +func NewDashboardCmd(namespace *string) *cobra.Command { + return &cobra.Command{ + Use: "dashboard", + Short: "Open the kagent dashboard", + Long: `Open the kagent dashboard`, + Run: func(cmd *cobra.Command, _ []string) { + runDashboard(cmd.Context(), *namespace) + }, + } } diff --git a/go/core/cli/internal/commands/dashboard_darwin.go b/go/core/cli/internal/commands/dashboard_darwin.go index c679b9f6b..c08ecf143 100644 --- a/go/core/cli/internal/commands/dashboard_darwin.go +++ b/go/core/cli/internal/commands/dashboard_darwin.go @@ -11,7 +11,7 @@ import ( "time" ) -func DashboardCmd(ctx context.Context, namespace string) { +func runDashboard(ctx context.Context, namespace string) { ctx, cancel := context.WithCancel(ctx) cmd := exec.CommandContext(ctx, "kubectl", "-n", namespace, "port-forward", "service/kagent-ui", "8082:8080") diff --git a/go/core/cli/internal/commands/dashboard_other.go b/go/core/cli/internal/commands/dashboard_other.go new file mode 100644 index 000000000..11bbfb349 --- /dev/null +++ b/go/core/cli/internal/commands/dashboard_other.go @@ -0,0 +1,16 @@ +//go:build !darwin + +package commands + +import ( + "context" + "fmt" + "os" +) + +func runDashboard(ctx context.Context, namespace string) { + fmt.Fprintln(os.Stderr, "Dashboard is not available on this platform") + fmt.Fprintln(os.Stderr, "You can easily start the dashboard by running:") + fmt.Fprintf(os.Stderr, "kubectl port-forward -n %s service/kagent-ui 8082:8080\n", namespace) + fmt.Fprintln(os.Stderr, "and then opening http://localhost:8082 in your browser") +} diff --git a/go/core/cli/internal/commands/db/db.go b/go/core/cli/internal/commands/db/db.go new file mode 100644 index 000000000..923e7c70d --- /dev/null +++ b/go/core/cli/internal/commands/db/db.go @@ -0,0 +1,102 @@ +// Package db wires the shared database subcommand to the CLI's migration tracks. +package db + +import ( + "context" + "fmt" + "os" + "strconv" + "time" + + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + dbcli "github.com/kagent-dev/kagent/go/core/pkg/cli/db" + dbmigrate "github.com/kagent-dev/kagent/go/core/pkg/cli/db/migrate" + "github.com/kagent-dev/kagent/go/core/pkg/migrations" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// vectorEnabledKey names two lookups that deliberately share it: the CLI's +// own DATABASE_VECTOR_ENABLED env var (a local operator override), and the +// controller-configmap key the chart renders — the value the controller pod +// itself consumes via envFrom. Same name, two different places. +const vectorEnabledKey = "DATABASE_VECTOR_ENABLED" + +// NewDBCmd constructs the kagent db command. +func NewDBCmd(connectionOptions *connection.Options) *cobra.Command { + return dbcli.NewCommandFromFunc(migrationSources(connectionOptions)) +} + +// migrationSources resolves the built-in migration tracks when a db +// subcommand runs (never during command construction, so unrelated commands +// do no work and print no warnings). The vector track is gated, in order of +// precedence, on: the DATABASE_VECTOR_ENABLED env var in the CLI's own +// environment (explicit operator intent, works without a cluster), the +// controller's configmap on the live cluster (the same value the server +// reads), and finally the controller's default (enabled). +func migrationSources(cfg *connection.Options) dbmigrate.SourcesFunc { + return func(ctx context.Context) ([]migrations.Source, error) { + vectorEnabled := true + if v := os.Getenv(vectorEnabledKey); v != "" { + b, err := strconv.ParseBool(v) + if err != nil { + fmt.Fprintf(os.Stderr, "warning: invalid %s=%q; assuming true\n", vectorEnabledKey, v) + } else { + vectorEnabled = b + } + } else if b, ok := clusterVectorEnabled(ctx, cfg.Namespace); ok { + vectorEnabled = b + } + return migrations.BuiltinSources(vectorEnabled), nil + } +} + +// clusterVectorEnabled reads the vectorEnabledKey entry from the controller +// configmap in the given namespace (the same "kagent-controller" default +// naming the rest of the CLI assumes) — the cluster-side counterpart of the +// env-var override in migrationSources. When the value is used it says so on +// stderr, naming the kubeconfig context it was read from — the lookup follows +// the *current* context, so this is the operator's cue that the cluster and +// their --db-url had better be the same install. Best-effort: reports +// ok=false when no cluster is reachable, the configmap is absent, or the +// value doesn't parse — callers fall back to the default. +func clusterVectorEnabled(ctx context.Context, namespace string) (enabled, ok bool) { + restConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}, + ).ClientConfig() + if err != nil { + return false, false + } + k8sClient, err := client.New(restConfig, client.Options{}) + if err != nil { + return false, false + } + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + var cm corev1.ConfigMap + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "kagent-controller"}, &cm); err != nil { + return false, false + } + b, err := strconv.ParseBool(cm.Data[vectorEnabledKey]) + if err != nil { + return false, false + } + // Trailing blank line separates the notice from the command's stdout + // when both land on a terminal; piped stdout is unaffected. + fmt.Fprintf(os.Stderr, "resolved vector track from cluster context %q: configmap %s/kagent-controller has %s=%t (set %s to override)\n\n", + currentKubeContext(), namespace, vectorEnabledKey, b, vectorEnabledKey) + return b, true +} + +// currentKubeContext names the kubeconfig context the CLI's Kubernetes client +// dials, for operator-facing messages. Best-effort. +func currentKubeContext() string { + raw, err := clientcmd.NewDefaultClientConfigLoadingRules().Load() + if err != nil || raw.CurrentContext == "" { + return "(current kubeconfig context)" + } + return raw.CurrentContext +} diff --git a/go/core/cli/internal/commands/install.go b/go/core/cli/internal/commands/install.go index 5bb9a7f06..6cfaca4e7 100644 --- a/go/core/cli/internal/commands/install.go +++ b/go/core/cli/internal/commands/install.go @@ -12,6 +12,7 @@ import ( "github.com/kagent-dev/kagent/go/api/v1alpha3" "github.com/kagent-dev/kagent/go/core/internal/version" "github.com/kagent-dev/kagent/go/core/pkg/env" + "github.com/spf13/cobra" "github.com/briandowns/spinner" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" @@ -64,7 +65,7 @@ func installChart(ctx context.Context, chartName string, namespace string, regis return "", nil } -func InstallCmd(ctx context.Context, cfg *InstallCfg) *connection.PortForward { +func runInstall(ctx context.Context, cfg *InstallCfg) *connection.PortForward { if version.Version == "dev" { fmt.Fprintln(os.Stderr, "Installation requires released version of kagent") return nil @@ -231,7 +232,7 @@ func deleteCRDs(ctx context.Context) error { return nil } -func UninstallCmd(ctx context.Context, namespace string) { +func runUninstall(ctx context.Context, namespace string) { // Check if helm is available if err := checkHelmAvailable(); err != nil { fmt.Fprintln(os.Stderr, err) @@ -303,3 +304,33 @@ func checkHelmAvailable() error { } return nil } + +// NewInstallCmd constructs the kagent install command. +func NewInstallCmd(connectionOptions *connection.Options) *cobra.Command { + cfg := &InstallCfg{Connection: connectionOptions} + cmd := &cobra.Command{ + Use: "install", + Short: "Install kagent", + Long: `Install kagent`, + Run: func(cmd *cobra.Command, _ []string) { + runInstall(cmd.Context(), cfg) + }, + } + cmd.Flags().StringVar(&cfg.Profile, "profile", "", "Installation profile (minimal|demo)") + _ = cmd.RegisterFlagCompletionFunc("profile", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp + }) + return cmd +} + +// NewUninstallCmd constructs the kagent uninstall command. +func NewUninstallCmd(namespace *string) *cobra.Command { + return &cobra.Command{ + Use: "uninstall", + Short: "Uninstall kagent", + Long: `Uninstall kagent`, + Run: func(cmd *cobra.Command, _ []string) { + runUninstall(cmd.Context(), *namespace) + }, + } +} diff --git a/go/core/cli/internal/commands/version.go b/go/core/cli/internal/commands/version.go index b75d7a24f..e9315271b 100644 --- a/go/core/cli/internal/commands/version.go +++ b/go/core/cli/internal/commands/version.go @@ -7,10 +7,12 @@ import ( "time" "github.com/kagent-dev/kagent/go/api/client" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" "github.com/kagent-dev/kagent/go/core/internal/version" + "github.com/spf13/cobra" ) -func VersionCmd(clientSet *client.ClientSet) { +func runVersion(clientSet *client.ClientSet) { versionInfo := map[string]string{ "kagent_version": version.Version, "git_commit": version.GitCommit, @@ -27,3 +29,21 @@ func VersionCmd(clientSet *client.ClientSet) { json.NewEncoder(os.Stdout).Encode(versionInfo) //nolint:errcheck } + +// NewVersionCmd constructs the kagent version command. +func NewVersionCmd(connectionOptions *connection.Options) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the kagent version", + Long: `Print the kagent version`, + Run: func(cmd *cobra.Command, _ []string) { + clientSet := connectionOptions.Client() + defer clientSet.Close() //nolint:errcheck + defer runVersion(clientSet) + + if portForward, _ := connection.Connect(cmd.Context(), connectionOptions); portForward != nil { + defer portForward.Stop() + } + }, + } +} diff --git a/go/core/cli/root.go b/go/core/cli/root.go index ca1aaed6e..6e92e79d4 100644 --- a/go/core/cli/root.go +++ b/go/core/cli/root.go @@ -1,28 +1,16 @@ package cli import ( - "context" - "errors" "fmt" - "os" - "strconv" - "time" "github.com/kagent-dev/kagent/go/core/cli/internal/commands" agentinstancecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agentinstance" agenttemplatecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agenttemplate" + dbcli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/db" "github.com/kagent-dev/kagent/go/core/cli/internal/commands/envdoc" "github.com/kagent-dev/kagent/go/core/cli/internal/commands/mcp" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" - "github.com/kagent-dev/kagent/go/core/cli/internal/tui" - dbcli "github.com/kagent-dev/kagent/go/core/pkg/cli/db" - dbmigrate "github.com/kagent-dev/kagent/go/core/pkg/cli/db/migrate" - "github.com/kagent-dev/kagent/go/core/pkg/migrations" "github.com/spf13/cobra" - "golang.org/x/term" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/clientcmd" - "sigs.k8s.io/controller-runtime/pkg/client" ) // Root creates a fresh kagent command tree. @@ -77,132 +65,22 @@ func Root() *cobra.Command { } // Add subcommands to the respective parent commands - getCmd.AddCommand(agentinstancecli.NewGetCommand(cfg, &outputFormat)) - getCmd.AddCommand(agenttemplatecli.NewGetCommand(&cfg.Namespace, &outputFormat)) - createCmd.AddCommand(agentinstancecli.NewCreateCommand(cfg, &outputFormat)) - deleteCmd.AddCommand(agentinstancecli.NewDeleteCommand(cfg, &outputFormat)) - rootCmd.AddCommand(commands.NewInstallCommand(cfg)) - rootCmd.AddCommand(commands.NewUninstallCommand(&cfg.Namespace)) - rootCmd.AddCommand(agentinstancecli.NewInvokeCommand(cfg, &outputFormat)) - rootCmd.AddCommand(commands.NewBugReportCommand(cfg)) - rootCmd.AddCommand(commands.NewVersionCommand(cfg)) - rootCmd.AddCommand(commands.NewDashboardCommand(&cfg.Namespace)) + getCmd.AddCommand(agentinstancecli.NewGetCmd(cfg, &outputFormat)) + getCmd.AddCommand(agenttemplatecli.NewGetCmd(&cfg.Namespace, &outputFormat)) + createCmd.AddCommand(agentinstancecli.NewCreateCmd(cfg, &outputFormat)) + deleteCmd.AddCommand(agentinstancecli.NewDeleteCmd(cfg, &outputFormat)) + rootCmd.AddCommand(commands.NewInstallCmd(cfg)) + rootCmd.AddCommand(commands.NewUninstallCmd(&cfg.Namespace)) + rootCmd.AddCommand(agentinstancecli.NewInvokeCmd(cfg, &outputFormat)) + rootCmd.AddCommand(commands.NewBugReportCmd(cfg)) + rootCmd.AddCommand(commands.NewVersionCmd(cfg)) + rootCmd.AddCommand(commands.NewDashboardCmd(&cfg.Namespace)) rootCmd.AddCommand(getCmd) rootCmd.AddCommand(createCmd) rootCmd.AddCommand(deleteCmd) rootCmd.AddCommand(mcp.NewMCPCmd()) rootCmd.AddCommand(envdoc.NewEnvCmd()) - rootCmd.AddCommand(dbcli.NewCommandFromFunc(migrationSources(cfg))) + rootCmd.AddCommand(dbcli.NewDBCmd(cfg)) return rootCmd } - -// vectorEnabledKey names two lookups that deliberately share it: the CLI's -// own DATABASE_VECTOR_ENABLED env var (a local operator override), and the -// controller-configmap key the chart renders — the value the controller pod -// itself consumes via envFrom. Same name, two different places. -const vectorEnabledKey = "DATABASE_VECTOR_ENABLED" - -// migrationSources resolves the built-in migration tracks when a db -// subcommand runs (never during command construction, so unrelated commands -// do no work and print no warnings). The vector track is gated, in order of -// precedence, on: the DATABASE_VECTOR_ENABLED env var in the CLI's own -// environment (explicit operator intent, works without a cluster), the -// controller's configmap on the live cluster (the same value the server -// reads), and finally the controller's default (enabled). -func migrationSources(cfg *connection.Options) dbmigrate.SourcesFunc { - return func(ctx context.Context) ([]migrations.Source, error) { - vectorEnabled := true - if v := os.Getenv(vectorEnabledKey); v != "" { - b, err := strconv.ParseBool(v) - if err != nil { - fmt.Fprintf(os.Stderr, "warning: invalid %s=%q; assuming true\n", vectorEnabledKey, v) - } else { - vectorEnabled = b - } - } else if b, ok := clusterVectorEnabled(ctx, cfg.Namespace); ok { - vectorEnabled = b - } - return migrations.BuiltinSources(vectorEnabled), nil - } -} - -// clusterVectorEnabled reads the vectorEnabledKey entry from the controller -// configmap in the given namespace (the same "kagent-controller" default -// naming the rest of the CLI assumes) — the cluster-side counterpart of the -// env-var override in migrationSources. When the value is used it says so on -// stderr, naming the kubeconfig context it was read from — the lookup follows -// the *current* context, so this is the operator's cue that the cluster and -// their --db-url had better be the same install. Best-effort: reports -// ok=false when no cluster is reachable, the configmap is absent, or the -// value doesn't parse — callers fall back to the default. -func clusterVectorEnabled(ctx context.Context, namespace string) (enabled, ok bool) { - restConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - clientcmd.NewDefaultClientConfigLoadingRules(), - &clientcmd.ConfigOverrides{}, - ).ClientConfig() - if err != nil { - return false, false - } - k8sClient, err := client.New(restConfig, client.Options{}) - if err != nil { - return false, false - } - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - var cm corev1.ConfigMap - if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "kagent-controller"}, &cm); err != nil { - return false, false - } - b, err := strconv.ParseBool(cm.Data[vectorEnabledKey]) - if err != nil { - return false, false - } - // Trailing blank line separates the notice from the command's stdout - // when both land on a terminal; piped stdout is unaffected. - fmt.Fprintf(os.Stderr, "resolved vector track from cluster context %q: configmap %s/kagent-controller has %s=%t (set %s to override)\n\n", - currentKubeContext(), namespace, vectorEnabledKey, b, vectorEnabledKey) - return b, true -} - -// currentKubeContext names the kubeconfig context the CLI's Kubernetes client -// dials, for operator-facing messages. Best-effort. -func currentKubeContext() string { - raw, err := clientcmd.NewDefaultClientConfigLoadingRules().Load() - if err != nil || raw.CurrentContext == "" { - return "(current kubeconfig context)" - } - return raw.CurrentContext -} - -// runInteractive launches the workspace; the TUI reads raw keys, so a redirected stream is an error. -func runInteractive(cmd *cobra.Command, cfg *connection.Options) (err error) { - if !isTerminal(cmd.InOrStdin()) || !isTerminal(cmd.OutOrStdout()) { - return errors.New("kagent requires a terminal; use `kagent get agent-instance` and `kagent invoke` for non-interactive use") - } - - client := cfg.Client() - defer func() { - err = errors.Join(err, client.Close()) - }() - - portForward, connectErr := connection.Connect(cmd.Context(), cfg) - if connectErr != nil { - return fmt.Errorf("connect to kagent: %w", connectErr) - } - if portForward != nil { - defer portForward.Stop() - } - - workspace := tui.Options{Namespace: cfg.Namespace} - if runErr := tui.RunWorkspace(cmd.Context(), workspace, client, cfg.Verbose); runErr != nil { - return fmt.Errorf("run kagent workspace: %w", runErr) - } - return nil -} - -// isTerminal reports whether a stream is backed by a TTY; a non-*os.File never is. -func isTerminal(stream any) bool { - file, ok := stream.(*os.File) - return ok && term.IsTerminal(int(file.Fd())) -} diff --git a/go/core/cli/root_external_test.go b/go/core/cli/root_external_test.go deleted file mode 100644 index 662ff2e58..000000000 --- a/go/core/cli/root_external_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package cli_test - -import ( - "testing" - - "github.com/kagent-dev/kagent/go/core/cli" -) - -func TestRootIsExternallyImportable(t *testing.T) { - root := cli.Root() - if root == nil { - t.Fatal("Root() returned nil") - } -} diff --git a/go/core/cli/root_test.go b/go/core/cli/root_test.go index f8031b522..6064a78e9 100644 --- a/go/core/cli/root_test.go +++ b/go/core/cli/root_test.go @@ -1,15 +1,16 @@ -package cli +package cli_test import ( "bytes" "testing" + "github.com/kagent-dev/kagent/go/core/cli" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRootCommandUsesDefaultFlagValues(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() assert.Equal(t, "http://localhost:8083", rootCmd.PersistentFlags().Lookup("kagent-url").DefValue) assert.Equal(t, "localhost:8084", rootCmd.PersistentFlags().Lookup("kagent-grpc-url").DefValue) @@ -24,7 +25,7 @@ func TestRootCommandUsesDefaultFlagValues(t *testing.T) { } func TestRootCommandFlagsOverrideOptionValues(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() require.NoError(t, rootCmd.ParseFlags([]string{ "--kagent-url", "http://flag.example.test", "--grpc-url", "grpc.flag.example.test:8443", @@ -56,15 +57,15 @@ func TestRootCommandFlagsOverrideOptionValues(t *testing.T) { } func TestRootCommandAllowsNoTimeout(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() require.NoError(t, rootCmd.ParseFlags([]string{"--timeout", "0"})) assert.Equal(t, "0s", rootCmd.PersistentFlags().Lookup("timeout").Value.String()) } func TestRootCommandsOwnIndependentFlagState(t *testing.T) { - first := Root() - second := Root() + first := cli.Root() + second := cli.Root() require.NoError(t, first.ParseFlags([]string{"--namespace", "first"})) @@ -73,7 +74,7 @@ func TestRootCommandsOwnIndependentFlagState(t *testing.T) { } func TestRootCommandDoesNotValidateClientFlagsForIndependentCommand(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() rootCmd.SetArgs([]string{"--output-format", "yaml", "--user-id", "invalid user", "env"}) rootCmd.SetOut(&bytes.Buffer{}) @@ -81,7 +82,7 @@ func TestRootCommandDoesNotValidateClientFlagsForIndependentCommand(t *testing.T } func TestRootCommandInvokeContract(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() assert.True(t, rootCmd.SilenceErrors) assert.True(t, rootCmd.SilenceUsage) @@ -103,7 +104,7 @@ func TestRootCommandInvokeContract(t *testing.T) { } func TestRootCommandV2CatalogAndLifecycleContract(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() getTemplateCmd, _, err := rootCmd.Find([]string{"get", "agent-template"}) require.NoError(t, err) @@ -130,7 +131,7 @@ func TestRootCommandV2CatalogAndLifecycleContract(t *testing.T) { } func TestRootCommandRemovesLegacyPaths(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() rootCommands := make([]string, 0, len(rootCmd.Commands())) for _, command := range rootCmd.Commands() { @@ -153,7 +154,7 @@ func TestRootCommandRemovesLegacyPaths(t *testing.T) { } func TestRootCommandRequiresTerminalForInteractiveUse(t *testing.T) { - rootCmd := Root() + rootCmd := cli.Root() rootCmd.SetArgs(nil) rootCmd.SetIn(&bytes.Buffer{}) rootCmd.SetOut(&bytes.Buffer{}) From a6df7940f8303f1b1ac9629c932f44f91f73c1d1 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Thu, 27 Aug 2026 10:47:23 -0700 Subject: [PATCH 6/7] refactor(cli): make command construction self-contained Signed-off-by: Cody Hartsook --- go/core/cli/interactive.go | 25 +++-- .../internal/commands/agentinstance/get.go | 42 +++++--- .../internal/commands/agentinstance/invoke.go | 38 ++++--- .../commands/agentinstance/lifecycle.go | 74 +++++++------ .../internal/commands/agenttemplate/get.go | 20 +++- go/core/cli/internal/commands/bug_report.go | 19 ++-- go/core/cli/internal/commands/dashboard.go | 12 ++- go/core/cli/internal/commands/db/db.go | 22 +++- .../cli/internal/commands/envdoc/envdoc.go | 6 +- .../internal/commands/envdoc/envdoc_test.go | 23 ++++ go/core/cli/internal/commands/install.go | 31 ++++-- go/core/cli/internal/commands/version.go | 25 +++-- go/core/cli/internal/connection/connection.go | 3 +- go/core/cli/internal/connection/flags.go | 70 ++++++++++++ go/core/cli/internal/connection/flags_test.go | 49 +++++++++ go/core/cli/internal/connection/session.go | 41 +++++++ go/core/cli/internal/output/output.go | 9 ++ go/core/cli/root.go | 102 ++++++++---------- go/core/cli/root_test.go | 70 +++++++++--- 19 files changed, 488 insertions(+), 193 deletions(-) create mode 100644 go/core/cli/internal/commands/envdoc/envdoc_test.go create mode 100644 go/core/cli/internal/connection/flags.go create mode 100644 go/core/cli/internal/connection/flags_test.go create mode 100644 go/core/cli/internal/connection/session.go diff --git a/go/core/cli/interactive.go b/go/core/cli/interactive.go index c0e54bbc7..7e0bd45b2 100644 --- a/go/core/cli/interactive.go +++ b/go/core/cli/interactive.go @@ -12,26 +12,25 @@ import ( ) // runInteractive launches the workspace; the TUI reads raw keys, so a redirected stream is an error. -func runInteractive(cmd *cobra.Command, cfg *connection.Options) (err error) { +func runInteractive(cmd *cobra.Command, _ []string) (err error) { if !isTerminal(cmd.InOrStdin()) || !isTerminal(cmd.OutOrStdout()) { return errors.New("kagent requires a terminal; use `kagent get agent-instance` and `kagent invoke` for non-interactive use") } - client := cfg.Client() - defer func() { - err = errors.Join(err, client.Close()) - }() - - portForward, connectErr := connection.Connect(cmd.Context(), cfg) - if connectErr != nil { - return fmt.Errorf("connect to kagent: %w", connectErr) + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err } - if portForward != nil { - defer portForward.Stop() + session, err := connection.Open(cmd.Context(), options) + if err != nil { + return err } + defer func() { + err = errors.Join(err, session.Close()) + }() - workspace := tui.Options{Namespace: cfg.Namespace} - if runErr := tui.RunWorkspace(cmd.Context(), workspace, client, cfg.Verbose); runErr != nil { + workspace := tui.Options{Namespace: session.Namespace} + if runErr := tui.RunWorkspace(cmd.Context(), workspace, session.Client, options.Verbose); runErr != nil { return fmt.Errorf("run kagent workspace: %w", runErr) } return nil diff --git a/go/core/cli/internal/commands/agentinstance/get.go b/go/core/cli/internal/commands/agentinstance/get.go index 2e9757281..2954250ec 100644 --- a/go/core/cli/internal/commands/agentinstance/get.go +++ b/go/core/cli/internal/commands/agentinstance/get.go @@ -27,7 +27,6 @@ type getClient interface { // GetCfg configures AgentInstance get and list operations. type GetCfg struct { - Connection *connection.Options OutputFormat string InstanceID string PageSize int32 @@ -35,7 +34,12 @@ type GetCfg struct { } // runGet gets one AgentInstance or lists the caller's AgentInstances. -func runGet(ctx context.Context, cfg *GetCfg, out io.Writer) (err error) { +func runGet( + ctx context.Context, + options connection.Options, + cfg *GetCfg, + out io.Writer, +) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err @@ -44,19 +48,14 @@ func runGet(ctx context.Context, cfg *GetCfg, out io.Writer) (err error) { return err } - portForward, err := connection.Connect(ctx, cfg.Connection) + session, err := connection.Open(ctx, options) if err != nil { - return fmt.Errorf("connect to kagent: %w", err) - } - if portForward != nil { - defer portForward.Stop() + return err } - - clientSet := cfg.Connection.Client() defer func() { - err = errors.Join(err, clientSet.Close()) + err = errors.Join(err, session.Close()) }() - return get(ctx, clientSet.AgentInstance, cfg.Connection.Namespace, cfg, format, out) + return get(ctx, session.Client.AgentInstance, session.Namespace, cfg, format, out) } func validateGetCfg(cfg *GetCfg) error { @@ -157,19 +156,28 @@ func formatTimestamp(timestamp *timestamppb.Timestamp) string { } // NewGetCmd constructs the AgentInstance get/list command. -func NewGetCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &GetCfg{Connection: connectionOptions} +func NewGetCmd() *cobra.Command { + cfg := &GetCfg{} cmd := &cobra.Command{ Use: "agent-instance [ID]", Short: "Get an AgentInstance or list your AgentInstances", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg.OutputFormat = *outputFormat - cfg.InstanceID = "" + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + format, err := clioutput.FromCommand(cmd) + if err != nil { + return err + } + var instanceID string if len(args) == 1 { - cfg.InstanceID = args[0] + instanceID = args[0] } - return runGet(cmd.Context(), cfg, cmd.OutOrStdout()) + cfg.OutputFormat = format + cfg.InstanceID = instanceID + return runGet(cmd.Context(), options, cfg, cmd.OutOrStdout()) }, } cmd.Flags().Int32Var(&cfg.PageSize, "page-size", 0, "Number of AgentInstances to return (default 50, maximum 100)") diff --git a/go/core/cli/internal/commands/agentinstance/invoke.go b/go/core/cli/internal/commands/agentinstance/invoke.go index e05b98f23..05c69e249 100644 --- a/go/core/cli/internal/commands/agentinstance/invoke.go +++ b/go/core/cli/internal/commands/agentinstance/invoke.go @@ -22,7 +22,6 @@ import ( var errTruncatedA2AStream = errors.New("a2a stream ended before returning a final result") type InvokeCfg struct { - Connection *connection.Options OutputFormat string Task string File string @@ -31,7 +30,13 @@ type InvokeCfg struct { Token string } -func runInvoke(ctx context.Context, cfg *InvokeCfg, in io.Reader, out io.Writer) (err error) { +func runInvoke( + ctx context.Context, + options connection.Options, + cfg *InvokeCfg, + in io.Reader, + out io.Writer, +) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err @@ -48,19 +53,14 @@ func runInvoke(ctx context.Context, cfg *InvokeCfg, in io.Reader, out io.Writer) return errors.New("model API key must not contain whitespace") } - portForward, err := connection.Connect(ctx, cfg.Connection) + session, err := connection.Open(ctx, options) if err != nil { - return fmt.Errorf("connect to kagent: %w", err) - } - if portForward != nil { - defer portForward.Stop() + return err } - - clientSet := cfg.Connection.Client() defer func() { - err = errors.Join(err, clientSet.Close()) + err = errors.Join(err, session.Close()) }() - a2aClient, err := clientSet.A2A.ForAgentInstance(ctx, cfg.Connection.Namespace, instanceID.String()) + a2aClient, err := session.Client.A2A.ForAgentInstance(ctx, session.Namespace, instanceID.String()) if err != nil { return fmt.Errorf("create AgentInstance A2A client: %w", err) } @@ -330,8 +330,8 @@ func sendResultError(result a2atype.SendMessageResult) error { } // NewInvokeCmd constructs the AgentInstance invoke command. -func NewInvokeCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &InvokeCfg{Connection: connectionOptions} +func NewInvokeCmd() *cobra.Command { + cfg := &InvokeCfg{} cmd := &cobra.Command{ Use: "invoke", Short: "Invoke an AgentInstance", @@ -339,8 +339,16 @@ func NewInvokeCmd(connectionOptions *connection.Options, outputFormat *string) * Args: cobra.NoArgs, Example: `kagent invoke --agent-instance 8bd650a8-9775-488f-8bc1-0d52bf7bdcab --task "Get all the pods"`, RunE: func(cmd *cobra.Command, _ []string) error { - cfg.OutputFormat = *outputFormat - return runInvoke(cmd.Context(), cfg, cmd.InOrStdin(), cmd.OutOrStdout()) + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + format, err := clioutput.FromCommand(cmd) + if err != nil { + return err + } + cfg.OutputFormat = format + return runInvoke(cmd.Context(), options, cfg, cmd.InOrStdin(), cmd.OutOrStdout()) }, } cmd.Flags().StringVar(&cfg.AgentInstance, "agent-instance", "", "AgentInstance ID") diff --git a/go/core/cli/internal/commands/agentinstance/lifecycle.go b/go/core/cli/internal/commands/agentinstance/lifecycle.go index 2ff79d16b..7659f89a9 100644 --- a/go/core/cli/internal/commands/agentinstance/lifecycle.go +++ b/go/core/cli/internal/commands/agentinstance/lifecycle.go @@ -23,7 +23,6 @@ type lifecycleClient interface { // CreateCfg configures AgentInstance creation. type CreateCfg struct { - Connection *connection.Options OutputFormat string Harness string AgentTemplate string @@ -32,32 +31,31 @@ type CreateCfg struct { // DeleteCfg configures AgentInstance deletion. type DeleteCfg struct { - Connection *connection.Options OutputFormat string InstanceID string } // runCreate creates an AgentInstance. -func runCreate(ctx context.Context, cfg *CreateCfg, out io.Writer) (err error) { +func runCreate( + ctx context.Context, + options connection.Options, + cfg *CreateCfg, + out io.Writer, +) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err } ensureRequestID(cfg) - portForward, err := connection.Connect(ctx, cfg.Connection) + session, err := connection.Open(ctx, options) if err != nil { - return fmt.Errorf("connect to kagent: %w", err) - } - if portForward != nil { - defer portForward.Stop() + return err } - - clientSet := cfg.Connection.Client() defer func() { - err = errors.Join(err, clientSet.Close()) + err = errors.Join(err, session.Close()) }() - return create(ctx, clientSet.AgentInstance, cfg.Connection.Namespace, cfg, format, out) + return create(ctx, session.Client.AgentInstance, session.Namespace, cfg, format, out) } func ensureRequestID(cfg *CreateCfg) { @@ -67,24 +65,24 @@ func ensureRequestID(cfg *CreateCfg) { } // runDelete deletes an AgentInstance. -func runDelete(ctx context.Context, cfg *DeleteCfg, out io.Writer) (err error) { +func runDelete( + ctx context.Context, + options connection.Options, + cfg *DeleteCfg, + out io.Writer, +) (err error) { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err } - portForward, err := connection.Connect(ctx, cfg.Connection) + session, err := connection.Open(ctx, options) if err != nil { - return fmt.Errorf("connect to kagent: %w", err) - } - if portForward != nil { - defer portForward.Stop() + return err } - - clientSet := cfg.Connection.Client() defer func() { - err = errors.Join(err, clientSet.Close()) + err = errors.Join(err, session.Close()) }() - return deleteAgentInstance(ctx, clientSet.AgentInstance, cfg.Connection.Namespace, cfg, format, out) + return deleteAgentInstance(ctx, session.Client.AgentInstance, session.Namespace, cfg, format, out) } func create( @@ -144,15 +142,23 @@ func writeLifecycleResult( } // NewCreateCmd constructs the AgentInstance create command. -func NewCreateCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &CreateCfg{Connection: connectionOptions} +func NewCreateCmd() *cobra.Command { + cfg := &CreateCfg{} cmd := &cobra.Command{ Use: "agent-instance", Short: "Create an AgentInstance", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { - cfg.OutputFormat = *outputFormat - return runCreate(cmd.Context(), cfg, cmd.OutOrStdout()) + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + format, err := clioutput.FromCommand(cmd) + if err != nil { + return err + } + cfg.OutputFormat = format + return runCreate(cmd.Context(), options, cfg, cmd.OutOrStdout()) }, } cmd.Flags().StringVar(&cfg.Harness, "harness", "", "Harness name") @@ -164,16 +170,24 @@ func NewCreateCmd(connectionOptions *connection.Options, outputFormat *string) * } // NewDeleteCmd constructs the AgentInstance delete command. -func NewDeleteCmd(connectionOptions *connection.Options, outputFormat *string) *cobra.Command { - cfg := &DeleteCfg{Connection: connectionOptions} +func NewDeleteCmd() *cobra.Command { + cfg := &DeleteCfg{} cmd := &cobra.Command{ Use: "agent-instance ID", Short: "Delete an AgentInstance", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg.OutputFormat = *outputFormat + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + format, err := clioutput.FromCommand(cmd) + if err != nil { + return err + } + cfg.OutputFormat = format cfg.InstanceID = args[0] - return runDelete(cmd.Context(), cfg, cmd.OutOrStdout()) + return runDelete(cmd.Context(), options, cfg, cmd.OutOrStdout()) }, } return cmd diff --git a/go/core/cli/internal/commands/agenttemplate/get.go b/go/core/cli/internal/commands/agenttemplate/get.go index c97778c50..46477c998 100644 --- a/go/core/cli/internal/commands/agenttemplate/get.go +++ b/go/core/cli/internal/commands/agenttemplate/get.go @@ -13,6 +13,7 @@ import ( typedapiv1alpha3 "github.com/kagent-dev/kagent/go/api/clientset/versioned/typed/api/v1alpha3" apiv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" commonk8s "github.com/kagent-dev/kagent/go/core/cli/internal/common/k8s" + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" @@ -125,19 +126,28 @@ func writeTemplatesTable(w io.Writer, templates []apiv1alpha3.AgentTemplate, lis } // NewGetCmd constructs the AgentTemplate get/list command. -func NewGetCmd(namespace *string, outputFormat *string) *cobra.Command { +func NewGetCmd() *cobra.Command { cfg := &GetCfg{} cmd := &cobra.Command{ Use: "agent-template [NAME]", Short: "Get an AgentTemplate or list AgentTemplates", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg.Namespace = *namespace - cfg.OutputFormat = *outputFormat - cfg.Name = "" + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + format, err := clioutput.FromCommand(cmd) + if err != nil { + return err + } + var name string if len(args) == 1 { - cfg.Name = args[0] + name = args[0] } + cfg.Namespace = options.Namespace + cfg.OutputFormat = format + cfg.Name = name return runGet(cmd.Context(), cfg, cmd.OutOrStdout()) }, } diff --git a/go/core/cli/internal/commands/bug_report.go b/go/core/cli/internal/commands/bug_report.go index 9908894cc..73170ddc9 100644 --- a/go/core/cli/internal/commands/bug_report.go +++ b/go/core/cli/internal/commands/bug_report.go @@ -120,21 +120,24 @@ func runBugReport(namespace string, verbose bool) { } // NewBugReportCmd constructs the kagent bug-report command. -func NewBugReportCmd(connectionOptions *connection.Options) *cobra.Command { +func NewBugReportCmd() *cobra.Command { return &cobra.Command{ Use: "bug-report", Short: "Generate a bug report", Long: `Generate a bug report`, - Run: func(cmd *cobra.Command, _ []string) { - portForward, err := connection.Connect(cmd.Context(), connectionOptions) + RunE: func(cmd *cobra.Command, _ []string) error { + options, err := connection.OptionsFromCommand(cmd) if err != nil { - fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) - return + return err } - if portForward != nil { - defer portForward.Stop() + session, err := connection.Open(cmd.Context(), options) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) + return nil } - runBugReport(connectionOptions.Namespace, connectionOptions.Verbose) + defer session.Close() //nolint:errcheck + runBugReport(options.Namespace, options.Verbose) + return nil }, } } diff --git a/go/core/cli/internal/commands/dashboard.go b/go/core/cli/internal/commands/dashboard.go index 5abcef75b..f4bf591a0 100644 --- a/go/core/cli/internal/commands/dashboard.go +++ b/go/core/cli/internal/commands/dashboard.go @@ -1,17 +1,23 @@ package commands import ( + "github.com/kagent-dev/kagent/go/core/cli/internal/connection" "github.com/spf13/cobra" ) // NewDashboardCmd constructs the kagent dashboard command. -func NewDashboardCmd(namespace *string) *cobra.Command { +func NewDashboardCmd() *cobra.Command { return &cobra.Command{ Use: "dashboard", Short: "Open the kagent dashboard", Long: `Open the kagent dashboard`, - Run: func(cmd *cobra.Command, _ []string) { - runDashboard(cmd.Context(), *namespace) + RunE: func(cmd *cobra.Command, _ []string) error { + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + runDashboard(cmd.Context(), options.Namespace) + return nil }, } } diff --git a/go/core/cli/internal/commands/db/db.go b/go/core/cli/internal/commands/db/db.go index 923e7c70d..b7fdb1ada 100644 --- a/go/core/cli/internal/commands/db/db.go +++ b/go/core/cli/internal/commands/db/db.go @@ -24,9 +24,21 @@ import ( // itself consumes via envFrom. Same name, two different places. const vectorEnabledKey = "DATABASE_VECTOR_ENABLED" -// NewDBCmd constructs the kagent db command. -func NewDBCmd(connectionOptions *connection.Options) *cobra.Command { - return dbcli.NewCommandFromFunc(migrationSources(connectionOptions)) +// NewDBCmd constructs the kagent db command. The source callback the shared db +// package takes carries no command, so the namespace is captured just before a +// subcommand runs, when the root's flags have been parsed. +func NewDBCmd() *cobra.Command { + var namespace string + cmd := dbcli.NewCommandFromFunc(migrationSources(&namespace)) + cmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + namespace = options.Namespace + return nil + } + return cmd } // migrationSources resolves the built-in migration tracks when a db @@ -36,7 +48,7 @@ func NewDBCmd(connectionOptions *connection.Options) *cobra.Command { // environment (explicit operator intent, works without a cluster), the // controller's configmap on the live cluster (the same value the server // reads), and finally the controller's default (enabled). -func migrationSources(cfg *connection.Options) dbmigrate.SourcesFunc { +func migrationSources(namespace *string) dbmigrate.SourcesFunc { return func(ctx context.Context) ([]migrations.Source, error) { vectorEnabled := true if v := os.Getenv(vectorEnabledKey); v != "" { @@ -46,7 +58,7 @@ func migrationSources(cfg *connection.Options) dbmigrate.SourcesFunc { } else { vectorEnabled = b } - } else if b, ok := clusterVectorEnabled(ctx, cfg.Namespace); ok { + } else if b, ok := clusterVectorEnabled(ctx, *namespace); ok { vectorEnabled = b } return migrations.BuiltinSources(vectorEnabled), nil diff --git a/go/core/cli/internal/commands/envdoc/envdoc.go b/go/core/cli/internal/commands/envdoc/envdoc.go index a949efa82..0aaef7920 100644 --- a/go/core/cli/internal/commands/envdoc/envdoc.go +++ b/go/core/cli/internal/commands/envdoc/envdoc.go @@ -7,13 +7,9 @@ import ( "github.com/spf13/cobra" ) -var ( - format string - component string -) - // NewEnvCmd returns a cobra command that generates environment variable documentation. func NewEnvCmd() *cobra.Command { + var format, component string cmd := &cobra.Command{ Use: "env", Hidden: true, diff --git a/go/core/cli/internal/commands/envdoc/envdoc_test.go b/go/core/cli/internal/commands/envdoc/envdoc_test.go new file mode 100644 index 000000000..e376d05ee --- /dev/null +++ b/go/core/cli/internal/commands/envdoc/envdoc_test.go @@ -0,0 +1,23 @@ +package envdoc + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEnvCommandsOwnIndependentFlagState(t *testing.T) { + first := NewEnvCmd() + second := NewEnvCmd() + first.SetArgs([]string{"--format", "json", "--component", "cli"}) + first.SetOut(&bytes.Buffer{}) + secondOutput := &bytes.Buffer{} + second.SetOut(secondOutput) + + require.NoError(t, first.ExecuteContext(t.Context())) + require.NoError(t, second.ExecuteContext(t.Context())) + assert.Contains(t, secondOutput.String(), "# Kagent Environment Variables") + assert.Contains(t, secondOutput.String(), "## controller") +} diff --git a/go/core/cli/internal/commands/install.go b/go/core/cli/internal/commands/install.go index 6cfaca4e7..9d7ec8b16 100644 --- a/go/core/cli/internal/commands/install.go +++ b/go/core/cli/internal/commands/install.go @@ -20,8 +20,7 @@ import ( ) type InstallCfg struct { - Connection *connection.Options - Profile string + Profile string } // installChart installs or upgrades a Helm chart with the given parameters @@ -65,7 +64,7 @@ func installChart(ctx context.Context, chartName string, namespace string, regis return "", nil } -func runInstall(ctx context.Context, cfg *InstallCfg) *connection.PortForward { +func runInstall(ctx context.Context, options connection.Options, cfg *InstallCfg) *connection.PortForward { if version.Version == "dev" { fmt.Fprintln(os.Stderr, "Installation requires released version of kagent") return nil @@ -102,7 +101,7 @@ func runInstall(ctx context.Context, cfg *InstallCfg) *connection.PortForward { helmConfig.inlineValues = profiles.GetProfileYaml(cfg.Profile) } - return install(ctx, cfg.Connection, helmConfig, modelProvider) + return install(ctx, &options, helmConfig, modelProvider) } // helmConfig is the config for the kagent chart @@ -306,14 +305,19 @@ func checkHelmAvailable() error { } // NewInstallCmd constructs the kagent install command. -func NewInstallCmd(connectionOptions *connection.Options) *cobra.Command { - cfg := &InstallCfg{Connection: connectionOptions} +func NewInstallCmd() *cobra.Command { + cfg := &InstallCfg{} cmd := &cobra.Command{ Use: "install", Short: "Install kagent", Long: `Install kagent`, - Run: func(cmd *cobra.Command, _ []string) { - runInstall(cmd.Context(), cfg) + RunE: func(cmd *cobra.Command, _ []string) error { + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + runInstall(cmd.Context(), options, cfg) + return nil }, } cmd.Flags().StringVar(&cfg.Profile, "profile", "", "Installation profile (minimal|demo)") @@ -324,13 +328,18 @@ func NewInstallCmd(connectionOptions *connection.Options) *cobra.Command { } // NewUninstallCmd constructs the kagent uninstall command. -func NewUninstallCmd(namespace *string) *cobra.Command { +func NewUninstallCmd() *cobra.Command { return &cobra.Command{ Use: "uninstall", Short: "Uninstall kagent", Long: `Uninstall kagent`, - Run: func(cmd *cobra.Command, _ []string) { - runUninstall(cmd.Context(), *namespace) + RunE: func(cmd *cobra.Command, _ []string) error { + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + runUninstall(cmd.Context(), options.Namespace) + return nil }, } } diff --git a/go/core/cli/internal/commands/version.go b/go/core/cli/internal/commands/version.go index e9315271b..78eedc0b4 100644 --- a/go/core/cli/internal/commands/version.go +++ b/go/core/cli/internal/commands/version.go @@ -31,19 +31,28 @@ func runVersion(clientSet *client.ClientSet) { } // NewVersionCmd constructs the kagent version command. -func NewVersionCmd(connectionOptions *connection.Options) *cobra.Command { +func NewVersionCmd() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print the kagent version", Long: `Print the kagent version`, - Run: func(cmd *cobra.Command, _ []string) { - clientSet := connectionOptions.Client() - defer clientSet.Close() //nolint:errcheck - defer runVersion(clientSet) - - if portForward, _ := connection.Connect(cmd.Context(), connectionOptions); portForward != nil { - defer portForward.Stop() + RunE: func(cmd *cobra.Command, _ []string) error { + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + // The CLI version prints whether or not the server answers; an + // unreachable server reports its version as "unknown". + session, _ := connection.Open(cmd.Context(), options) + if session == nil { + clientSet := options.Client() + defer clientSet.Close() //nolint:errcheck + runVersion(clientSet) + return nil } + defer session.Close() //nolint:errcheck + runVersion(session.Client) + return nil }, } } diff --git a/go/core/cli/internal/connection/connection.go b/go/core/cli/internal/connection/connection.go index 2ce0f049d..70cf3a361 100644 --- a/go/core/cli/internal/connection/connection.go +++ b/go/core/cli/internal/connection/connection.go @@ -30,7 +30,8 @@ const ( kubectlErrorLimit = 8 << 10 ) -// Options contains only the settings needed to connect to kagent. +// Options is how the CLI reaches kagent: where to dial, who to dial as, the +// namespace to port-forward into, and whether to narrate the attempt. type Options struct { KAgentURL string KAgentGRPCURL string diff --git a/go/core/cli/internal/connection/flags.go b/go/core/cli/internal/connection/flags.go new file mode 100644 index 000000000..2d618b0dd --- /dev/null +++ b/go/core/cli/internal/connection/flags.go @@ -0,0 +1,70 @@ +package connection + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Flag names are unexported so RegisterFlags and OptionsFromCommand are the +// only things that can disagree about them, and they cannot. +const ( + flagKAgentURL = "kagent-url" + flagKAgentGRPCURL = "grpc-url" + flagKAgentGRPCTLS = "grpc-tls" + flagKAgentGRPCCAFile = "grpc-ca-file" + flagKAgentGRPCServerName = "grpc-server-name" + flagNamespace = "namespace" + flagVerbose = "verbose" + flagTimeout = "timeout" + flagUserID = "user-id" +) + +// RegisterFlags declares the CLI-wide connection flags, defaulted from DefaultOptions. +func RegisterFlags(flags *pflag.FlagSet) { + defaults := DefaultOptions() + flags.String(flagKAgentURL, defaults.KAgentURL, "KAgent REST URL") + flags.String(flagKAgentGRPCURL, defaults.KAgentGRPCURL, "KAgent gRPC target") + flags.Bool(flagKAgentGRPCTLS, defaults.KAgentGRPCTLS, "Use TLS for KAgent gRPC") + flags.String(flagKAgentGRPCCAFile, defaults.KAgentGRPCCAFile, "CA certificate file for KAgent gRPC") + flags.String(flagKAgentGRPCServerName, defaults.KAgentGRPCServerName, "TLS server name for KAgent gRPC") + flags.StringP(flagNamespace, "n", defaults.Namespace, "Namespace") + flags.BoolP(flagVerbose, "v", defaults.Verbose, "Verbose output") + flags.Duration(flagTimeout, defaults.Timeout, "Timeout") + flags.String(flagUserID, defaults.UserID, "Caller identity used to select the server-side data partition") +} + +// OptionsFromCommand resolves connection options from the flags a command was +// invoked with, which include the root's persistent flags. +func OptionsFromCommand(cmd *cobra.Command) (Options, error) { + flags := cmd.Flags() + var options Options + var err error + if options.KAgentURL, err = flags.GetString(flagKAgentURL); err != nil { + return Options{}, err + } + if options.KAgentGRPCURL, err = flags.GetString(flagKAgentGRPCURL); err != nil { + return Options{}, err + } + if options.KAgentGRPCTLS, err = flags.GetBool(flagKAgentGRPCTLS); err != nil { + return Options{}, err + } + if options.KAgentGRPCCAFile, err = flags.GetString(flagKAgentGRPCCAFile); err != nil { + return Options{}, err + } + if options.KAgentGRPCServerName, err = flags.GetString(flagKAgentGRPCServerName); err != nil { + return Options{}, err + } + if options.Namespace, err = flags.GetString(flagNamespace); err != nil { + return Options{}, err + } + if options.Verbose, err = flags.GetBool(flagVerbose); err != nil { + return Options{}, err + } + if options.Timeout, err = flags.GetDuration(flagTimeout); err != nil { + return Options{}, err + } + if options.UserID, err = flags.GetString(flagUserID); err != nil { + return Options{}, err + } + return options, nil +} diff --git a/go/core/cli/internal/connection/flags_test.go b/go/core/cli/internal/connection/flags_test.go new file mode 100644 index 000000000..79bbfef79 --- /dev/null +++ b/go/core/cli/internal/connection/flags_test.go @@ -0,0 +1,49 @@ +package connection + +import ( + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOptionsFromCommandReadsInheritedFlags(t *testing.T) { + var got Options + root := &cobra.Command{Use: "root"} + RegisterFlags(root.PersistentFlags()) + root.AddCommand(&cobra.Command{ + Use: "child", + RunE: func(cmd *cobra.Command, _ []string) error { + var err error + got, err = OptionsFromCommand(cmd) + return err + }, + }) + root.SetArgs([]string{ + "child", + "--kagent-url", "https://api.example.test", + "--grpc-url", "grpc.example.test:443", + "--grpc-tls", + "--grpc-ca-file", "/tmp/ca.pem", + "--grpc-server-name", "grpc.example.test", + "--namespace", "agents", + "--verbose", + "--timeout", "12s", + "--user-id", "reviewer@example.test", + }) + + require.NoError(t, root.ExecuteContext(t.Context())) + assert.Equal(t, Options{ + KAgentURL: "https://api.example.test", + KAgentGRPCURL: "grpc.example.test:443", + KAgentGRPCTLS: true, + KAgentGRPCCAFile: "/tmp/ca.pem", + KAgentGRPCServerName: "grpc.example.test", + Namespace: "agents", + Verbose: true, + Timeout: 12 * time.Second, + UserID: "reviewer@example.test", + }, got) +} diff --git a/go/core/cli/internal/connection/session.go b/go/core/cli/internal/connection/session.go new file mode 100644 index 000000000..561118156 --- /dev/null +++ b/go/core/cli/internal/connection/session.go @@ -0,0 +1,41 @@ +package connection + +import ( + "context" + "fmt" + + "github.com/kagent-dev/kagent/go/api/client" +) + +// Session is a connected kagent client for one command invocation, together +// with the namespace the command is scoped to. +type Session struct { + Client *client.ClientSet + Namespace string + + portForward *PortForward +} + +// Open reaches the server, starting a port-forward when the default local +// endpoint is unreachable. The caller must Close the returned session. +func Open(ctx context.Context, options Options) (*Session, error) { + portForward, err := Connect(ctx, &options) + if err != nil { + return nil, fmt.Errorf("connect to kagent: %w", err) + } + return &Session{ + Client: options.Client(), + Namespace: options.Namespace, + portForward: portForward, + }, nil +} + +// Close releases the client before tearing down the port-forward it rode on. +func (s *Session) Close() error { + if s == nil { + return nil + } + err := s.Client.Close() + s.portForward.Stop() + return err +} diff --git a/go/core/cli/internal/output/output.go b/go/core/cli/internal/output/output.go index 3b327168a..8f0e5fe7e 100644 --- a/go/core/cli/internal/output/output.go +++ b/go/core/cli/internal/output/output.go @@ -6,10 +6,19 @@ import ( "fmt" "io" + "github.com/spf13/cobra" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) +// FlagName is the root persistent flag that selects the output format. +const FlagName = "output-format" + +// FromCommand reads the output format a command was invoked with. +func FromCommand(cmd *cobra.Command) (string, error) { + return cmd.Flags().GetString(FlagName) +} + // Format selects the CLI payload encoding. type Format string diff --git a/go/core/cli/root.go b/go/core/cli/root.go index 6e92e79d4..c4702192b 100644 --- a/go/core/cli/root.go +++ b/go/core/cli/root.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strings" "github.com/kagent-dev/kagent/go/core/cli/internal/commands" agentinstancecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agentinstance" @@ -10,77 +11,62 @@ import ( "github.com/kagent-dev/kagent/go/core/cli/internal/commands/envdoc" "github.com/kagent-dev/kagent/go/core/cli/internal/commands/mcp" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" + clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/spf13/cobra" ) // Root creates a fresh kagent command tree. func Root() *cobra.Command { - connectionOptions := connection.DefaultOptions() - cfg := &connectionOptions - outputFormat := "table" rootCmd := &cobra.Command{ Use: "kagent", Short: "kagent is a CLI for kagent", Long: "kagent is a CLI for kagent", SilenceErrors: true, SilenceUsage: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return runInteractive(cmd, cfg) - }, - } - rootCmd.PersistentFlags().StringVar(&cfg.KAgentURL, "kagent-url", cfg.KAgentURL, "KAgent REST URL") - rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCURL, "kagent-grpc-url", cfg.KAgentGRPCURL, "KAgent gRPC target") - rootCmd.PersistentFlags().BoolVar(&cfg.KAgentGRPCTLS, "kagent-grpc-tls", cfg.KAgentGRPCTLS, "Use TLS for KAgent gRPC") - rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCCAFile, "kagent-grpc-ca-file", cfg.KAgentGRPCCAFile, "CA certificate file for KAgent gRPC") - rootCmd.PersistentFlags().StringVar(&cfg.KAgentGRPCServerName, "kagent-grpc-server-name", cfg.KAgentGRPCServerName, "TLS server name for KAgent gRPC") - rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", cfg.Namespace, "Namespace") - rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", outputFormat, "Output format") - rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "Verbose output") - rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "Timeout") - rootCmd.PersistentFlags().StringVar(&cfg.UserID, "user-id", cfg.UserID, "Caller identity used to select the server-side data partition") - getCmd := &cobra.Command{ - Use: "get", - Short: "Get a kagent resource", - Long: `Get a kagent resource`, - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - return fmt.Errorf("resource type is required") - }, - } - createCmd := &cobra.Command{ - Use: "create", - Short: "Create a kagent resource", - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - return fmt.Errorf("resource type is required") - }, - } - deleteCmd := &cobra.Command{ - Use: "delete", - Short: "Delete a kagent resource", - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - return fmt.Errorf("resource type is required") - }, + RunE: runInteractive, } + connection.RegisterFlags(rootCmd.PersistentFlags()) + rootCmd.PersistentFlags().StringP(clioutput.FlagName, "o", string(clioutput.FormatTable), "Output format") + + getCmd := newResourceGroupCmd("get", "Get a kagent resource") + createCmd := newResourceGroupCmd("create", "Create a kagent resource") + deleteCmd := newResourceGroupCmd("delete", "Delete a kagent resource") - // Add subcommands to the respective parent commands - getCmd.AddCommand(agentinstancecli.NewGetCmd(cfg, &outputFormat)) - getCmd.AddCommand(agenttemplatecli.NewGetCmd(&cfg.Namespace, &outputFormat)) - createCmd.AddCommand(agentinstancecli.NewCreateCmd(cfg, &outputFormat)) - deleteCmd.AddCommand(agentinstancecli.NewDeleteCmd(cfg, &outputFormat)) - rootCmd.AddCommand(commands.NewInstallCmd(cfg)) - rootCmd.AddCommand(commands.NewUninstallCmd(&cfg.Namespace)) - rootCmd.AddCommand(agentinstancecli.NewInvokeCmd(cfg, &outputFormat)) - rootCmd.AddCommand(commands.NewBugReportCmd(cfg)) - rootCmd.AddCommand(commands.NewVersionCmd(cfg)) - rootCmd.AddCommand(commands.NewDashboardCmd(&cfg.Namespace)) - rootCmd.AddCommand(getCmd) - rootCmd.AddCommand(createCmd) - rootCmd.AddCommand(deleteCmd) - rootCmd.AddCommand(mcp.NewMCPCmd()) - rootCmd.AddCommand(envdoc.NewEnvCmd()) - rootCmd.AddCommand(dbcli.NewDBCmd(cfg)) + getCmd.AddCommand(agentinstancecli.NewGetCmd()) + getCmd.AddCommand(agenttemplatecli.NewGetCmd()) + createCmd.AddCommand(agentinstancecli.NewCreateCmd()) + deleteCmd.AddCommand(agentinstancecli.NewDeleteCmd()) + rootCmd.AddCommand( + getCmd, + createCmd, + deleteCmd, + agentinstancecli.NewInvokeCmd(), + commands.NewInstallCmd(), + commands.NewUninstallCmd(), + commands.NewBugReportCmd(), + commands.NewVersionCmd(), + commands.NewDashboardCmd(), + mcp.NewMCPCmd(), + envdoc.NewEnvCmd(), + dbcli.NewDBCmd(), + ) return rootCmd } + +// newResourceGroupCmd builds a parent command that only routes to resource subcommands. +func newResourceGroupCmd(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Long: short, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + resourceTypes := make([]string, 0, len(cmd.Commands())) + for _, child := range cmd.Commands() { + resourceTypes = append(resourceTypes, child.Name()) + } + return fmt.Errorf("resource type is required; available resource types: %s", strings.Join(resourceTypes, ", ")) + }, + } +} diff --git a/go/core/cli/root_test.go b/go/core/cli/root_test.go index 6064a78e9..d2012bc69 100644 --- a/go/core/cli/root_test.go +++ b/go/core/cli/root_test.go @@ -13,10 +13,10 @@ func TestRootCommandUsesDefaultFlagValues(t *testing.T) { rootCmd := cli.Root() assert.Equal(t, "http://localhost:8083", rootCmd.PersistentFlags().Lookup("kagent-url").DefValue) - assert.Equal(t, "localhost:8084", rootCmd.PersistentFlags().Lookup("kagent-grpc-url").DefValue) - assert.Equal(t, "false", rootCmd.PersistentFlags().Lookup("kagent-grpc-tls").DefValue) - assert.Empty(t, rootCmd.PersistentFlags().Lookup("kagent-grpc-ca-file").DefValue) - assert.Empty(t, rootCmd.PersistentFlags().Lookup("kagent-grpc-server-name").DefValue) + assert.Equal(t, "localhost:8084", rootCmd.PersistentFlags().Lookup("grpc-url").DefValue) + assert.Equal(t, "false", rootCmd.PersistentFlags().Lookup("grpc-tls").DefValue) + assert.Empty(t, rootCmd.PersistentFlags().Lookup("grpc-ca-file").DefValue) + assert.Empty(t, rootCmd.PersistentFlags().Lookup("grpc-server-name").DefValue) assert.Equal(t, "kagent", rootCmd.PersistentFlags().Lookup("namespace").DefValue) assert.Equal(t, "table", rootCmd.PersistentFlags().Lookup("output-format").DefValue) assert.Equal(t, "false", rootCmd.PersistentFlags().Lookup("verbose").DefValue) @@ -40,16 +40,16 @@ func TestRootCommandFlagsOverrideOptionValues(t *testing.T) { })) want := map[string]string{ - "kagent-url": "http://flag.example.test", - "kagent-grpc-url": "grpc.flag.example.test:8443", - "kagent-grpc-tls": "true", - "kagent-grpc-ca-file": "/tmp/flag-ca.pem", - "kagent-grpc-server-name": "grpc.flag.example.test", - "namespace": "flag-ns", - "output-format": "yaml", - "verbose": "true", - "timeout": "10s", - "user-id": "flag-user", + "kagent-url": "http://flag.example.test", + "grpc-url": "grpc.flag.example.test:8443", + "grpc-tls": "true", + "grpc-ca-file": "/tmp/flag-ca.pem", + "grpc-server-name": "grpc.flag.example.test", + "namespace": "flag-ns", + "output-format": "yaml", + "verbose": "true", + "timeout": "10s", + "user-id": "flag-user", } for name, value := range want { assert.Equal(t, value, rootCmd.PersistentFlags().Lookup(name).Value.String()) @@ -165,3 +165,45 @@ func TestRootCommandRequiresTerminalForInteractiveUse(t *testing.T) { assert.Contains(t, err.Error(), "kagent requires a terminal") assert.Contains(t, err.Error(), "kagent invoke") } + +func TestRootCommandOutputFormatReachesResourceCommands(t *testing.T) { + // An unparseable format is rejected before any command connects, so this + // reaches the run function without touching the network or a cluster. + for name, args := range map[string][]string{ + "get agent-instance": {"get", "agent-instance"}, + "get agent-template": {"get", "agent-template"}, + "create agent-instance": {"create", "agent-instance", "--harness", "kagent", "--agent-template", "example"}, + "delete agent-instance": {"delete", "agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab"}, + "invoke": {"invoke", "--agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab", "--task", "hello"}, + } { + t.Run(name, func(t *testing.T) { + rootCmd := cli.Root() + rootCmd.SetArgs(append(args, "--output-format", "bogus")) + rootCmd.SetOut(&bytes.Buffer{}) + rootCmd.SetErr(&bytes.Buffer{}) + + err := rootCmd.ExecuteContext(t.Context()) + + require.Error(t, err) + assert.Contains(t, err.Error(), `unsupported output format "bogus"`) + }) + } +} + +func TestRootResourceGroupsNameAvailableTypes(t *testing.T) { + for name, want := range map[string]string{ + "get": "agent-instance, agent-template", + "create": "agent-instance", + "delete": "agent-instance", + } { + t.Run(name, func(t *testing.T) { + rootCmd := cli.Root() + rootCmd.SetArgs([]string{name}) + + err := rootCmd.ExecuteContext(t.Context()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "available resource types: "+want) + }) + } +} From 13df246a6090121e44dd86bea092812bc6661d5b Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Sun, 30 Aug 2026 21:10:09 -0700 Subject: [PATCH 7/7] refactor(cli): flatten standalone command packages Signed-off-by: Cody Hartsook --- .../get.go => agent_template.go} | 43 +++++++++---------- .../get_test.go => agent_template_test.go} | 24 +++++------ .../commands/{envdoc/envdoc.go => env.go} | 2 +- .../{envdoc/envdoc_test.go => env_test.go} | 2 +- go/core/cli/root.go | 6 +-- 5 files changed, 37 insertions(+), 40 deletions(-) rename go/core/cli/internal/commands/{agenttemplate/get.go => agent_template.go} (73%) rename go/core/cli/internal/commands/{agenttemplate/get_test.go => agent_template_test.go} (81%) rename go/core/cli/internal/commands/{envdoc/envdoc.go => env.go} (98%) rename go/core/cli/internal/commands/{envdoc/envdoc_test.go => env_test.go} (97%) diff --git a/go/core/cli/internal/commands/agenttemplate/get.go b/go/core/cli/internal/commands/agent_template.go similarity index 73% rename from go/core/cli/internal/commands/agenttemplate/get.go rename to go/core/cli/internal/commands/agent_template.go index 46477c998..2d38521b0 100644 --- a/go/core/cli/internal/commands/agenttemplate/get.go +++ b/go/core/cli/internal/commands/agent_template.go @@ -1,5 +1,4 @@ -// Package agenttemplate implements AgentTemplate CLI commands. -package agenttemplate +package commands import ( "context" @@ -20,10 +19,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const maxPageSize = 100 +const agentTemplateMaxPageSize = 100 -// GetCfg configures AgentTemplate get and list operations. -type GetCfg struct { +// AgentTemplateGetCfg configures AgentTemplate get and list operations. +type AgentTemplateGetCfg struct { Namespace string OutputFormat string Name string @@ -31,13 +30,13 @@ type GetCfg struct { PageToken string } -// runGet gets one AgentTemplate or lists AgentTemplates through Kubernetes. -func runGet(ctx context.Context, cfg *GetCfg, out io.Writer) error { +// runGetAgentTemplate gets one AgentTemplate or lists AgentTemplates through Kubernetes. +func runGetAgentTemplate(ctx context.Context, cfg *AgentTemplateGetCfg, out io.Writer) error { format, err := clioutput.Parse(cfg.OutputFormat) if err != nil { return err } - if err := validateGetCfg(cfg); err != nil { + if err := validateAgentTemplateGetCfg(cfg); err != nil { return err } @@ -45,12 +44,12 @@ func runGet(ctx context.Context, cfg *GetCfg, out io.Writer) error { if err != nil { return err } - return get(ctx, clients.ApiV1alpha3().AgentTemplates(cfg.Namespace), cfg, format, out) + return getAgentTemplates(ctx, clients.ApiV1alpha3().AgentTemplates(cfg.Namespace), cfg, format, out) } -func validateGetCfg(cfg *GetCfg) error { - if cfg.PageSize < 0 || cfg.PageSize > maxPageSize { - return fmt.Errorf("page size must be between 1 and %d, or 0 for the default of %d", maxPageSize, maxPageSize) +func validateAgentTemplateGetCfg(cfg *AgentTemplateGetCfg) error { + if cfg.PageSize < 0 || cfg.PageSize > agentTemplateMaxPageSize { + return fmt.Errorf("page size must be between 1 and %d, or 0 for the default of %d", agentTemplateMaxPageSize, agentTemplateMaxPageSize) } if cfg.Name != "" && (cfg.PageSize != 0 || cfg.PageToken != "") { return errors.New("pagination flags cannot be used when getting one AgentTemplate") @@ -58,10 +57,10 @@ func validateGetCfg(cfg *GetCfg) error { return nil } -func get( +func getAgentTemplates( ctx context.Context, client typedapiv1alpha3.AgentTemplateInterface, - cfg *GetCfg, + cfg *AgentTemplateGetCfg, format clioutput.Format, out io.Writer, ) error { @@ -73,12 +72,12 @@ func get( if format == clioutput.FormatJSON { return clioutput.WriteJSON(out, template) } - return writeTemplatesTable(out, []apiv1alpha3.AgentTemplate{*template}, false, "") + return writeAgentTemplatesTable(out, []apiv1alpha3.AgentTemplate{*template}, false, "") } pageSize := cfg.PageSize if pageSize == 0 { - pageSize = maxPageSize + pageSize = agentTemplateMaxPageSize } templates, err := client.List(ctx, metav1.ListOptions{Limit: pageSize, Continue: cfg.PageToken}) if err != nil { @@ -87,10 +86,10 @@ func get( if format == clioutput.FormatJSON { return clioutput.WriteJSON(out, templates) } - return writeTemplatesTable(out, templates.Items, true, templates.Continue) + return writeAgentTemplatesTable(out, templates.Items, true, templates.Continue) } -func writeTemplatesTable(w io.Writer, templates []apiv1alpha3.AgentTemplate, list bool, nextPageToken string) error { +func writeAgentTemplatesTable(w io.Writer, templates []apiv1alpha3.AgentTemplate, list bool, nextPageToken string) error { tw := table.NewWriter() tw.AppendHeader(table.Row{"NAME", "HARNESS", "READY", "CREATED"}) for i := range templates { @@ -125,9 +124,9 @@ func writeTemplatesTable(w io.Writer, templates []apiv1alpha3.AgentTemplate, lis return nil } -// NewGetCmd constructs the AgentTemplate get/list command. -func NewGetCmd() *cobra.Command { - cfg := &GetCfg{} +// NewGetAgentTemplateCmd constructs the AgentTemplate get/list command. +func NewGetAgentTemplateCmd() *cobra.Command { + cfg := &AgentTemplateGetCfg{} cmd := &cobra.Command{ Use: "agent-template [NAME]", Short: "Get an AgentTemplate or list AgentTemplates", @@ -148,7 +147,7 @@ func NewGetCmd() *cobra.Command { cfg.Namespace = options.Namespace cfg.OutputFormat = format cfg.Name = name - return runGet(cmd.Context(), cfg, cmd.OutOrStdout()) + return runGetAgentTemplate(cmd.Context(), cfg, cmd.OutOrStdout()) }, } cmd.Flags().Int64Var(&cfg.PageSize, "page-size", 0, "Number of AgentTemplates per page (0 uses 100; maximum 100)") diff --git a/go/core/cli/internal/commands/agenttemplate/get_test.go b/go/core/cli/internal/commands/agent_template_test.go similarity index 81% rename from go/core/cli/internal/commands/agenttemplate/get_test.go rename to go/core/cli/internal/commands/agent_template_test.go index e3e4c19a9..95a4aec65 100644 --- a/go/core/cli/internal/commands/agenttemplate/get_test.go +++ b/go/core/cli/internal/commands/agent_template_test.go @@ -1,4 +1,4 @@ -package agenttemplate +package commands import ( "bytes" @@ -16,23 +16,23 @@ import ( k8stesting "k8s.io/client-go/testing" ) -func TestValidateGetCfg(t *testing.T) { +func TestValidateAgentTemplateGetCfg(t *testing.T) { tests := []struct { name string - cfg GetCfg + cfg AgentTemplateGetCfg wantErr string }{ {name: "list"}, - {name: "list page", cfg: GetCfg{PageSize: 10, PageToken: "next"}}, - {name: "get", cfg: GetCfg{Name: "template"}}, - {name: "negative page size", cfg: GetCfg{PageSize: -1}, wantErr: "page size"}, - {name: "large page size", cfg: GetCfg{PageSize: 101}, wantErr: "page size"}, - {name: "get with pagination", cfg: GetCfg{Name: "template", PageSize: 10}, wantErr: "pagination"}, + {name: "list page", cfg: AgentTemplateGetCfg{PageSize: 10, PageToken: "next"}}, + {name: "get", cfg: AgentTemplateGetCfg{Name: "template"}}, + {name: "negative page size", cfg: AgentTemplateGetCfg{PageSize: -1}, wantErr: "page size"}, + {name: "large page size", cfg: AgentTemplateGetCfg{PageSize: 101}, wantErr: "page size"}, + {name: "get with pagination", cfg: AgentTemplateGetCfg{Name: "template", PageSize: 10}, wantErr: "pagination"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateGetCfg(&tt.cfg) + err := validateAgentTemplateGetCfg(&tt.cfg) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) @@ -60,7 +60,7 @@ func TestGetAgentTemplatesTableReportsHarnessReadiness(t *testing.T) { }) var output bytes.Buffer - err := get(context.Background(), clientSet.ApiV1alpha3().AgentTemplates("kagent"), &GetCfg{ + err := getAgentTemplates(context.Background(), clientSet.ApiV1alpha3().AgentTemplates("kagent"), &AgentTemplateGetCfg{ Namespace: "kagent", PageSize: 3, PageToken: "previous-page", }, clioutput.FormatTable, &output) require.NoError(t, err) @@ -79,7 +79,7 @@ func TestGetAgentTemplatesJSONPreservesListMetadata(t *testing.T) { clientSet := clientfake.NewSimpleClientset() clientSet.PrependReactor("list", "agenttemplates", func(action k8stesting.Action) (bool, runtime.Object, error) { options := action.(interface{ GetListOptions() metav1.ListOptions }).GetListOptions() - assert.Equal(t, int64(maxPageSize), options.Limit) + assert.Equal(t, int64(agentTemplateMaxPageSize), options.Limit) return true, &apiv1alpha3.AgentTemplateList{ ListMeta: metav1.ListMeta{Continue: "next-page"}, Items: []apiv1alpha3.AgentTemplate{ @@ -91,7 +91,7 @@ func TestGetAgentTemplatesJSONPreservesListMetadata(t *testing.T) { }) var output bytes.Buffer - err := get(context.Background(), clientSet.ApiV1alpha3().AgentTemplates("kagent"), &GetCfg{ + err := getAgentTemplates(context.Background(), clientSet.ApiV1alpha3().AgentTemplates("kagent"), &AgentTemplateGetCfg{ Namespace: "kagent", }, clioutput.FormatJSON, &output) require.NoError(t, err) diff --git a/go/core/cli/internal/commands/envdoc/envdoc.go b/go/core/cli/internal/commands/env.go similarity index 98% rename from go/core/cli/internal/commands/envdoc/envdoc.go rename to go/core/cli/internal/commands/env.go index 0aaef7920..c0a680638 100644 --- a/go/core/cli/internal/commands/envdoc/envdoc.go +++ b/go/core/cli/internal/commands/env.go @@ -1,4 +1,4 @@ -package envdoc +package commands import ( "fmt" diff --git a/go/core/cli/internal/commands/envdoc/envdoc_test.go b/go/core/cli/internal/commands/env_test.go similarity index 97% rename from go/core/cli/internal/commands/envdoc/envdoc_test.go rename to go/core/cli/internal/commands/env_test.go index e376d05ee..95282b5fa 100644 --- a/go/core/cli/internal/commands/envdoc/envdoc_test.go +++ b/go/core/cli/internal/commands/env_test.go @@ -1,4 +1,4 @@ -package envdoc +package commands import ( "bytes" diff --git a/go/core/cli/root.go b/go/core/cli/root.go index c4702192b..8803cdfe1 100644 --- a/go/core/cli/root.go +++ b/go/core/cli/root.go @@ -6,9 +6,7 @@ import ( "github.com/kagent-dev/kagent/go/core/cli/internal/commands" agentinstancecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agentinstance" - agenttemplatecli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/agenttemplate" dbcli "github.com/kagent-dev/kagent/go/core/cli/internal/commands/db" - "github.com/kagent-dev/kagent/go/core/cli/internal/commands/envdoc" "github.com/kagent-dev/kagent/go/core/cli/internal/commands/mcp" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" @@ -33,7 +31,7 @@ func Root() *cobra.Command { deleteCmd := newResourceGroupCmd("delete", "Delete a kagent resource") getCmd.AddCommand(agentinstancecli.NewGetCmd()) - getCmd.AddCommand(agenttemplatecli.NewGetCmd()) + getCmd.AddCommand(commands.NewGetAgentTemplateCmd()) createCmd.AddCommand(agentinstancecli.NewCreateCmd()) deleteCmd.AddCommand(agentinstancecli.NewDeleteCmd()) @@ -48,7 +46,7 @@ func Root() *cobra.Command { commands.NewVersionCmd(), commands.NewDashboardCmd(), mcp.NewMCPCmd(), - envdoc.NewEnvCmd(), + commands.NewEnvCmd(), dbcli.NewDBCmd(), ) return rootCmd