Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 33 additions & 27 deletions go/core/cli/cmd/kagent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"os/signal"
"syscall"
"time"

cli "github.com/kagent-dev/kagent/go/core/cli/internal/cli/agent"
"github.com/kagent-dev/kagent/go/core/cli/internal/cli/envdoc"
Expand Down Expand Up @@ -34,20 +33,43 @@ func main() {

cancel()
}()
cfg := &config.Config{}
cfg, err := loadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error initializing config: %v\n", err)
os.Exit(1)
}

rootCmd := newRootCommand(ctx, cfg)
if err := rootCmd.ExecuteContext(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

os.Exit(1)
}
}

func loadConfig() (*config.Config, error) {
if err := config.Init(); err != nil {
return nil, err
}
return config.Get()
}

func newRootCommand(ctx context.Context, cfg *config.Config) *cobra.Command {
rootCmd := &cobra.Command{
Use: "kagent",
Short: "kagent is a CLI and TUI for kagent",
Long: "kagent is a CLI and TUI for kagent",
Run: runInteractive,
Run: func(cmd *cobra.Command, args []string) {
runInteractive(cmd, args, cfg)
},
}
rootCmd.SetContext(ctx)

rootCmd.PersistentFlags().StringVar(&cfg.KAgentURL, "kagent-url", "http://localhost:8083", "KAgent URL")
rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", "kagent", "Namespace")
rootCmd.PersistentFlags().StringVarP(&cfg.OutputFormat, "output-format", "o", "table", "Output format")
rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", false, "Verbose output")
rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", 300*time.Second, "Timeout")
rootCmd.PersistentFlags().StringVar(&cfg.KAgentURL, "kagent-url", cfg.KAgentURL, "KAgent URL")
rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", cfg.Namespace, "Namespace")
rootCmd.PersistentFlags().StringVarP(&cfg.OutputFormat, "output-format", "o", cfg.OutputFormat, "Output format")
rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "Verbose output")
rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "Timeout")
installCfg := &cli.InstallCfg{
Config: cfg,
}
Expand Down Expand Up @@ -348,7 +370,7 @@ Examples:
// Add flags for deploy command
deployCmd.Flags().StringVarP(&deployCfg.Image, "image", "i", "", "Image to use (defaults to localhost:5001/{agentName}:latest)")
deployCmd.Flags().StringVar(&deployCfg.EnvFile, "env-file", "", "Path to .env file containing environment variables (including API keys)")
deployCmd.Flags().StringVar(&deployCfg.Config.Namespace, "namespace", "kagent", "Kubernetes namespace to deploy to")
deployCmd.Flags().StringVar(&deployCfg.Config.Namespace, "namespace", cfg.Namespace, "Kubernetes namespace to deploy to")
deployCmd.Flags().BoolVar(&deployCfg.DryRun, "dry-run", false, "Output YAML manifests without applying them to the cluster")
deployCmd.Flags().StringVar(&deployCfg.Platform, "platform", "", "Target platform for Docker build (e.g., linux/amd64, linux/arm64)")

Expand Down Expand Up @@ -429,26 +451,10 @@ Examples:

rootCmd.AddCommand(installCmd, uninstallCmd, invokeCmd, bugReportCmd, versionCmd, dashboardCmd, getCmd, initCmd, buildCmd, deployCmd, addMcpCmd, runCmd, mcp.NewMCPCmd(), envdoc.NewEnvCmd())

// Initialize config
if err := config.Init(); err != nil {
fmt.Fprintf(os.Stderr, "Error initializing config: %v\n", err)
os.Exit(1)
}

if err := rootCmd.ExecuteContext(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

os.Exit(1)
}
return rootCmd
}

func runInteractive(cmd *cobra.Command, args []string) {
cfg, err := config.Get()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting config: %v\n", err)
os.Exit(1)
}

func runInteractive(cmd *cobra.Command, args []string, cfg *config.Config) {
client := cfg.Client()

// Start port forward and ensure it is healthy.
Expand Down
104 changes: 104 additions & 0 deletions go/core/cli/cmd/kagent/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"context"
"os"
"path/filepath"
"testing"
"time"

"github.com/kagent-dev/kagent/go/core/cli/internal/config"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLoadConfigReadsConfigFileValues(t *testing.T) {
resetConfigState(t)

homeDir := t.TempDir()
t.Setenv("HOME", homeDir)

configDir := filepath.Join(homeDir, ".kagent")
require.NoError(t, os.MkdirAll(configDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(`
kagent_url: http://kagent.example.test
namespace: configured-ns
output_format: json
verbose: true
timeout: 45s
`), 0600))

cfg, err := loadConfig()
require.NoError(t, err)

assert.Equal(t, "http://kagent.example.test", cfg.KAgentURL)
assert.Equal(t, "configured-ns", cfg.Namespace)
assert.Equal(t, "json", cfg.OutputFormat)
assert.True(t, cfg.Verbose)
assert.Equal(t, 45*time.Second, cfg.Timeout)
}

func TestRootCommandUsesConfigValuesAsFlagDefaults(t *testing.T) {
cfg := &config.Config{
KAgentURL: "http://kagent.example.test",
Namespace: "configured-ns",
OutputFormat: "json",
Verbose: true,
Timeout: 45 * time.Second,
}

rootCmd := newRootCommand(context.Background(), cfg)

assert.Equal(t, "http://kagent.example.test", rootCmd.PersistentFlags().Lookup("kagent-url").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)

deployCmd, _, err := rootCmd.Find([]string{"deploy"})
require.NoError(t, err)
require.NotNil(t, deployCmd)

assert.Equal(t, "configured-ns", deployCmd.Flags().Lookup("namespace").DefValue)
assert.Equal(t, "configured-ns", cfg.Namespace)
}

func TestRootCommandFlagsOverrideConfigValues(t *testing.T) {
cfg := &config.Config{
KAgentURL: "http://kagent.example.test",
Namespace: "configured-ns",
OutputFormat: "json",
Verbose: false,
Timeout: 45 * time.Second,
}

rootCmd := newRootCommand(context.Background(), cfg)
require.NoError(t, rootCmd.ParseFlags([]string{
"--kagent-url", "http://flag.example.test",
"--namespace", "flag-ns",
"--output-format", "yaml",
"--verbose",
"--timeout", "10s",
}))

assert.Equal(t, "http://flag.example.test", cfg.KAgentURL)
assert.Equal(t, "flag-ns", cfg.Namespace)
assert.Equal(t, "yaml", cfg.OutputFormat)
assert.True(t, cfg.Verbose)
assert.Equal(t, 10*time.Second, cfg.Timeout)
}

func resetConfigState(t *testing.T) {
t.Helper()

oldCommandLine := pflag.CommandLine
viper.Reset()
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)

t.Cleanup(func() {
viper.Reset()
pflag.CommandLine = oldCommandLine
})
}
Loading