Skip to content

Commit e0feda4

Browse files
authored
Sort clients in CLI commands (#1228)
Signed-off-by: Dan Barr <[email protected]> Co-authored-by: Dan Barr <[email protected]>
1 parent 343692d commit e0feda4

File tree

4 files changed

+102
-3
lines changed

4 files changed

+102
-3
lines changed

cmd/thv/app/client.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package app
22

33
import (
44
"fmt"
5+
"sort"
56

67
"github.com/spf13/cobra"
78

@@ -217,8 +218,14 @@ func listRegisteredClientsCmdFunc(_ *cobra.Command, _ []string) error {
217218
fmt.Println("No clients are currently registered.")
218219
return nil
219220
}
221+
222+
// Create a copy of the registered clients and sort it alphabetically
223+
registeredClients := make([]string, len(cfg.Clients.RegisteredClients))
224+
copy(registeredClients, cfg.Clients.RegisteredClients)
225+
sort.Strings(registeredClients)
226+
220227
fmt.Println("Registered clients:")
221-
for _, clientName := range cfg.Clients.RegisteredClients {
228+
for _, clientName := range registeredClients {
222229
fmt.Printf(" - %s\n", clientName)
223230
}
224231
return nil

cmd/thv/app/client_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package app
22

33
import (
44
"bytes"
5+
"io"
56
"os"
67
"strings"
78
"testing"
@@ -79,3 +80,56 @@ func TestClientRemoveCmd_InvalidClient(t *testing.T) { //nolint:paralleltest //
7980
assert.Error(t, err)
8081
assert.True(t, strings.Contains(err.Error(), "invalid client type"))
8182
}
83+
84+
func TestListRegisteredClientsCmd_Sorting(t *testing.T) { //nolint:paralleltest // Uses environment variables
85+
tempDir := t.TempDir()
86+
os.Setenv("XDG_CONFIG_HOME", tempDir)
87+
88+
// Pre-populate config with multiple registered clients in non-alphabetical order
89+
testClients := []string{"vscode", "cursor", "roo-code", "cline", "claude-code"}
90+
err := config.UpdateConfig(func(c *config.Config) {
91+
c.Clients.RegisteredClients = testClients
92+
})
93+
assert.NoError(t, err)
94+
95+
// Temporarily redirect stdout to capture the output
96+
oldStdout := os.Stdout
97+
r, w, _ := os.Pipe()
98+
os.Stdout = w
99+
100+
// Call the function directly
101+
err = listRegisteredClientsCmdFunc(nil, nil)
102+
assert.NoError(t, err)
103+
104+
// Restore stdout and read the captured output
105+
w.Close()
106+
os.Stdout = oldStdout
107+
108+
outputBytes, err := io.ReadAll(r)
109+
assert.NoError(t, err)
110+
outputStr := string(outputBytes)
111+
112+
// Extract client names from output (they appear after "- ")
113+
lines := strings.Split(outputStr, "\n")
114+
var foundClients []string
115+
for _, line := range lines {
116+
line = strings.TrimSpace(line)
117+
if strings.HasPrefix(line, "- ") {
118+
client := strings.TrimPrefix(line, "- ")
119+
foundClients = append(foundClients, client)
120+
}
121+
}
122+
123+
// Verify all clients are present
124+
assert.Len(t, foundClients, len(testClients), "Should find all registered clients")
125+
for _, expectedClient := range testClients {
126+
assert.Contains(t, foundClients, expectedClient, "Should contain client: %s", expectedClient)
127+
}
128+
129+
// Verify alphabetical order
130+
for i := 1; i < len(foundClients); i++ {
131+
assert.True(t, foundClients[i-1] < foundClients[i],
132+
"Clients should be sorted alphabetically: %s should come before %s",
133+
foundClients[i-1], foundClients[i])
134+
}
135+
}

pkg/client/discovery.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path/filepath"
77
"runtime"
8+
"sort"
89

910
"github.com/stacklok/toolhive/pkg/config"
1011
)
@@ -65,6 +66,11 @@ func GetClientStatus() ([]MCPClientStatus, error) {
6566
statuses = append(statuses, status)
6667
}
6768

69+
// Sort statuses alphabetically by ClientType
70+
sort.Slice(statuses, func(i, j int) bool {
71+
return string(statuses[i].ClientType) < string(statuses[j].ClientType)
72+
})
73+
6874
return statuses, nil
6975
}
7076

pkg/client/discovery_test.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@ import (
1313

1414
func TestGetClientStatus(t *testing.T) {
1515
// Setup a temporary home directory for testing
16-
origHome := os.Getenv("HOME")
1716
tempHome, err := os.MkdirTemp("", "toolhive-test-home")
1817
require.NoError(t, err)
1918
defer os.RemoveAll(tempHome)
2019

2120
t.Setenv("HOME", tempHome)
22-
defer t.Setenv("HOME", origHome)
2321

2422
// Create mock config with registered clients
2523
mockConfig := &config.Config{
@@ -63,3 +61,37 @@ func TestGetClientStatus(t *testing.T) {
6361
assert.False(t, vscodeStatus.Installed)
6462
assert.False(t, vscodeStatus.Registered)
6563
}
64+
65+
func TestGetClientStatus_Sorting(t *testing.T) {
66+
// Setup a temporary home directory for testing
67+
origHome := os.Getenv("HOME")
68+
tempHome, err := os.MkdirTemp("", "toolhive-test-home")
69+
require.NoError(t, err)
70+
defer os.RemoveAll(tempHome)
71+
72+
t.Setenv("HOME", tempHome)
73+
defer t.Setenv("HOME", origHome)
74+
75+
// Create mock config with no registered clients
76+
mockConfig := &config.Config{
77+
Clients: config.Clients{
78+
RegisteredClients: []string{},
79+
},
80+
}
81+
cleanup := MockConfig(t, mockConfig)
82+
defer cleanup()
83+
84+
statuses, err := GetClientStatus()
85+
require.NoError(t, err)
86+
require.NotNil(t, statuses)
87+
require.Greater(t, len(statuses), 1, "Need at least 2 clients to test sorting")
88+
89+
// Verify that the statuses are sorted alphabetically by ClientType
90+
for i := 1; i < len(statuses); i++ {
91+
prevClient := string(statuses[i-1].ClientType)
92+
currClient := string(statuses[i].ClientType)
93+
assert.True(t, prevClient < currClient,
94+
"Client statuses should be sorted alphabetically: %s should come before %s",
95+
prevClient, currClient)
96+
}
97+
}

0 commit comments

Comments
 (0)