-
Notifications
You must be signed in to change notification settings - Fork 6
feat(cmd): add info command to list supported agents and runtimes #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bmahabirbu
wants to merge
1
commit into
kortex-hub:main
Choose a base branch
from
bmahabirbu:feat/info-command
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+485
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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