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
21 changes: 18 additions & 3 deletions cmd/entire/cli/agent/external/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log/slog"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/entireio/cli/cmd/entire/cli/agent"
Expand Down Expand Up @@ -49,7 +50,10 @@ func DiscoverAndRegister(ctx context.Context) {
}
seen[name] = true

agentName := types.AgentName(strings.TrimPrefix(name, binaryPrefix))
// Strip Windows executable extensions (.exe, .bat) before deriving agent name.
// On Unix, binaries have no extension, so this is a no-op.
cleanName := stripExeExt(name)
agentName := types.AgentName(strings.TrimPrefix(cleanName, binaryPrefix))
if registered[agentName] {
logging.Debug(ctx, "skipping external agent (name conflict with built-in)",
slog.String("binary", name),
Expand All @@ -61,8 +65,8 @@ func DiscoverAndRegister(ctx context.Context) {
if err != nil || finfo.IsDir() {
continue
}
// Check executable bit (on Unix)
if finfo.Mode()&0o111 == 0 {
// Check executable bit (on Unix; Windows doesn't set execute bits)
if runtime.GOOS != "windows" && finfo.Mode()&0o111 == 0 {
continue
}

Expand Down Expand Up @@ -94,3 +98,14 @@ func DiscoverAndRegister(ctx context.Context) {
}
}
}

// stripExeExt removes Windows executable extensions (.exe, .bat, .cmd) from a
// file name so that the agent name derived from the binary matches on all platforms.
// On Unix this is effectively a no-op because binaries have no extension.
func stripExeExt(name string) string {
switch strings.ToLower(filepath.Ext(name)) {
case ".exe", ".bat", ".cmd":
return strings.TrimSuffix(name, filepath.Ext(name))
}
return name
}
33 changes: 33 additions & 0 deletions cmd/entire/cli/agent/external/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"

"github.com/entireio/cli/cmd/entire/cli/agent"
Expand Down Expand Up @@ -279,3 +280,35 @@
t.Error("expected agent with bad info to be skipped, but it was registered")
}
}

// TestDiscoverAndRegister_RegistersBatOnWindows verifies that a .bat agent
// binary is discovered and registered on Windows, with the file extension
// stripped from the agent name. .cmd and .exe follow the same code path.
func TestDiscoverAndRegister_RegistersBatOnWindows(t *testing.T) {
if runtime.GOOS != "windows" {

Check failure on line 288 in cmd/entire/cli/agent/external/discovery_test.go

View workflow job for this annotation

GitHub Actions / lint

string `windows` has 3 occurrences, make it a constant (goconst)
t.Skip("this test only applies on Windows")
}

enableExternalAgents(t)

name := "disc-bat"
infoJSON := `{"protocol_version":1,"name":"` + name + `","type":"` + name + ` Agent","description":"Agent ` + name + `","is_preview":false,"protected_dirs":[],"hook_names":[],"capabilities":{}}`
script := "@echo off\r\nif not \"%1\"==\"info\" goto :notinfo\r\necho " + infoJSON + "\r\ngoto :eof\r\n:notinfo\r\necho unknown subcommand: %1 1>&2\r\nexit /b 1\r\n"

dir := t.TempDir()
binPath := filepath.Join(dir, binaryPrefix+name+".bat")
if err := os.WriteFile(binPath, []byte(script), 0o755); err != nil {
t.Fatalf("write mock binary: %v", err)
}
t.Setenv("PATH", dir)

DiscoverAndRegister(context.Background())

ag, err := agent.Get(types.AgentName(name))
if err != nil {
t.Fatalf("expected agent %q to be registered after stripping .bat, got error: %v", name, err)
}
if string(ag.Name()) != name {
t.Errorf("agent Name() = %q, want %q", ag.Name(), name)
}
}
32 changes: 32 additions & 0 deletions cmd/entire/cli/agent/external/external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,35 @@ func TestWrap_HooksAnalyzerPreparer(t *testing.T) {
t.Error("Wrap() should not return TokenCalculator when token_calculator=false")
}
}

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

tests := []struct {
name string
in string
want string
}{
{name: "exe lowercase", in: "entire-agent-test.exe", want: "entire-agent-test"},
{name: "bat lowercase", in: "entire-agent-test.bat", want: "entire-agent-test"},
{name: "cmd lowercase", in: "entire-agent-test.cmd", want: "entire-agent-test"},
{name: "exe uppercase", in: "entire-agent-test.EXE", want: "entire-agent-test"},
{name: "bat mixed case", in: "entire-agent-test.Bat", want: "entire-agent-test"},
{name: "cmd mixed case", in: "entire-agent-test.CmD", want: "entire-agent-test"},
{name: "no extension", in: "entire-agent-test", want: "entire-agent-test"},
{name: "unrelated extension", in: "entire-agent-test.sh", want: "entire-agent-test.sh"},
{name: "dot only", in: "entire-agent-test.", want: "entire-agent-test."},
{name: "empty string", in: "", want: ""},
{name: "exe in middle", in: "entire-agent-exe-test", want: "entire-agent-exe-test"},
{name: "double extension", in: "entire-agent-test.tar.exe", want: "entire-agent-test.tar"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := stripExeExt(tt.in); got != tt.want {
t.Errorf("stripExeExt(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
Loading