Skip to content
Open
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
140 changes: 140 additions & 0 deletions pkg/cmd/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**********************************************************************
* Copyright (C) 2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

package cmd

import (
"encoding/json"
"fmt"
"path/filepath"
"strings"

"github.com/kortex-hub/kortex-cli/pkg/runtime/podman/config"
"github.com/kortex-hub/kortex-cli/pkg/runtimesetup"
"github.com/kortex-hub/kortex-cli/pkg/version"
"github.com/spf13/cobra"
)

// infoResponse is the JSON output structure for the info command
type infoResponse struct {
Version string `json:"version"`
Agents []string `json:"agents"`
Runtimes []string `json:"runtimes"`
}

// infoCmd contains the configuration for the info command
type infoCmd struct {
output string
}

// preRun validates the parameters and flags
func (i *infoCmd) preRun(cmd *cobra.Command, args []string) error {
// Validate output format if specified
if i.output != "" && i.output != "json" {
return fmt.Errorf("unsupported output format: %s (supported: json)", i.output)
}

// Silence Cobra's error output when JSON mode is enabled
// This prevents "Error: ..." prefix from being printed
if i.output == "json" {
cmd.SilenceErrors = true
}

return nil
}

// run executes the info command logic
func (i *infoCmd) run(cmd *cobra.Command, args []string) error {
runtimes := runtimesetup.ListAvailable()

// Discover agents from podman config directory
storageDir, err := cmd.Flags().GetString("storage")
if err != nil {
return outputErrorIfJSON(cmd, i.output, fmt.Errorf("failed to read --storage flag: %w", err))
}

absStorageDir, err := filepath.Abs(storageDir)
if err != nil {
return outputErrorIfJSON(cmd, i.output, fmt.Errorf("failed to resolve storage directory path: %w", err))
}

configDir := filepath.Join(absStorageDir, "runtimes", "podman", "config")
cfg, err := config.NewConfig(configDir)
if err != nil {
return outputErrorIfJSON(cmd, i.output, fmt.Errorf("failed to create config: %w", err))
}
Comment on lines +76 to +80
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be generic, the command cannot know about specific runtimes, only the instance.Manager knows about them, in a generic way (though interface/implementation). You'll have to add a ListAgents method to the Runtime interfaec, and the manager can collect all the agents from all runtimes. Finally, the Manager will expose a ListAgents() method so the command can get them.

I suppose that we will differentiate agents per runtime, as one agent can be configured for a specific runtime, but not others


agents, err := cfg.ListAgents()
if err != nil {
return outputErrorIfJSON(cmd, i.output, fmt.Errorf("failed to list agents: %w", err))
}

if i.output == "json" {
return i.outputJSON(cmd, agents, runtimes)
}

// Text output
out := cmd.OutOrStdout()
fmt.Fprintf(out, "Version: %s\n", version.Version)
fmt.Fprintf(out, "Agents: %s\n", strings.Join(agents, ", "))
fmt.Fprintf(out, "Runtimes: %s\n", strings.Join(runtimes, ", "))

return nil
}

// outputJSON outputs the info response as JSON
func (i *infoCmd) outputJSON(cmd *cobra.Command, agents, runtimes []string) error {
response := infoResponse{
Version: version.Version,
Agents: agents,
Runtimes: runtimes,
}

jsonData, err := json.MarshalIndent(response, "", " ")
if err != nil {
return outputErrorIfJSON(cmd, i.output, fmt.Errorf("failed to marshal info to JSON: %w", err))
}

fmt.Fprintln(cmd.OutOrStdout(), string(jsonData))
return nil
}

func NewInfoCmd() *cobra.Command {
c := &infoCmd{}

cmd := &cobra.Command{
Use: "info",
Short: "Display information about kortex-cli",
Example: `# Show info
kortex-cli info

# Show info in JSON format
kortex-cli info --output json

# Show info using short flag
kortex-cli info -o json`,
Args: cobra.NoArgs,
PreRunE: c.preRun,
RunE: c.run,
}

cmd.Flags().StringVarP(&c.output, "output", "o", "", "Output format (supported: json)")
cmd.RegisterFlagCompletionFunc("output", newOutputFlagCompletion([]string{"json"}))

return cmd
}
215 changes: 215 additions & 0 deletions pkg/cmd/info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**********************************************************************
* Copyright (C) 2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

package cmd

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/kortex-hub/kortex-cli/pkg/cmd/testutil"
"github.com/kortex-hub/kortex-cli/pkg/version"
"github.com/spf13/cobra"
)

func TestInfoCmd(t *testing.T) {
t.Parallel()

cmd := NewInfoCmd()
if cmd == nil {
t.Fatal("NewInfoCmd() returned nil")
}

if cmd.Use != "info" {
t.Errorf("Expected Use to be 'info', got '%s'", cmd.Use)
}
}

func TestInfoCmd_Examples(t *testing.T) {
t.Parallel()

cmd := NewInfoCmd()

if cmd.Example == "" {
t.Fatal("Example field should not be empty")
}

commands, err := testutil.ParseExampleCommands(cmd.Example)
if err != nil {
t.Fatalf("Failed to parse examples: %v", err)
}

expectedCount := 3
if len(commands) != expectedCount {
t.Errorf("Expected %d example commands, got %d", expectedCount, len(commands))
}

rootCmd := NewRootCmd()
err = testutil.ValidateCommandExamples(rootCmd, cmd.Example)
if err != nil {
t.Errorf("Example validation failed: %v", err)
}
}

func TestInfoCmd_PreRun(t *testing.T) {
t.Parallel()

t.Run("accepts empty output flag", func(t *testing.T) {
t.Parallel()

c := &infoCmd{}
cmd := &cobra.Command{}

err := c.preRun(cmd, []string{})
if err != nil {
t.Fatalf("preRun() failed: %v", err)
}
})

t.Run("accepts json output format", func(t *testing.T) {
t.Parallel()

c := &infoCmd{output: "json"}
cmd := &cobra.Command{}

err := c.preRun(cmd, []string{})
if err != nil {
t.Fatalf("preRun() failed: %v", err)
}
})

t.Run("rejects invalid output format", func(t *testing.T) {
t.Parallel()

c := &infoCmd{output: "xml"}
cmd := &cobra.Command{}

err := c.preRun(cmd, []string{})
if err == nil {
t.Fatal("Expected error for invalid output format")
}

if !strings.Contains(err.Error(), "unsupported output format") {
t.Errorf("Expected 'unsupported output format' error, got: %v", err)
}
})
}

func TestInfoCmd_E2E(t *testing.T) {
t.Parallel()

t.Run("text output contains version and runtimes", func(t *testing.T) {
t.Parallel()

storageDir := t.TempDir()

rootCmd := NewRootCmd()
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"info", "--storage", storageDir})

err := rootCmd.Execute()
if err != nil {
t.Fatalf("Execute() failed: %v", err)
}

output := buf.String()
if !strings.Contains(output, "Version: "+version.Version) {
t.Errorf("Expected output to contain version, got: %s", output)
}
if !strings.Contains(output, "Runtimes:") {
t.Errorf("Expected output to contain Runtimes, got: %s", output)
}
if !strings.Contains(output, "Agents:") {
t.Errorf("Expected output to contain Agents, got: %s", output)
}
})

t.Run("json output has expected fields", func(t *testing.T) {
t.Parallel()

storageDir := t.TempDir()

rootCmd := NewRootCmd()
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"info", "--storage", storageDir, "-o", "json"})

err := rootCmd.Execute()
if err != nil {
t.Fatalf("Execute() failed: %v", err)
}

var response infoResponse
if err := json.Unmarshal(buf.Bytes(), &response); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}

if response.Version != version.Version {
t.Errorf("Expected version %s, got: %s", version.Version, response.Version)
}

if response.Runtimes == nil {
t.Error("Expected runtimes to be present")
}

if response.Agents == nil {
t.Error("Expected agents to be present")
}
})

t.Run("json output with agents from config", func(t *testing.T) {
t.Parallel()

storageDir := t.TempDir()

// Create podman config directory with a claude.json
configDir := filepath.Join(storageDir, "runtimes", "podman", "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("Failed to create config dir: %v", err)
}

claudeConfig := `{"packages": [], "run_commands": [], "terminal_command": ["claude"]}`
if err := os.WriteFile(filepath.Join(configDir, "claude.json"), []byte(claudeConfig), 0644); err != nil {
t.Fatalf("Failed to write claude config: %v", err)
}

rootCmd := NewRootCmd()
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"info", "--storage", storageDir, "-o", "json"})

err := rootCmd.Execute()
if err != nil {
t.Fatalf("Execute() failed: %v", err)
}

var response infoResponse
if err := json.Unmarshal(buf.Bytes(), &response); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}

if len(response.Agents) != 1 || response.Agents[0] != "claude" {
t.Errorf("Expected agents [claude], got: %v", response.Agents)
}
})
}
1 change: 1 addition & 0 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func NewRootCmd() *cobra.Command {

// Add subcommands
rootCmd.AddCommand(NewVersionCmd())
rootCmd.AddCommand(NewInfoCmd())
rootCmd.AddCommand(NewInitCmd())
rootCmd.AddCommand(NewWorkspaceCmd())
rootCmd.AddCommand(NewListCmd())
Expand Down
Loading
Loading