on main help sign in
chore(lint): gofmt
af72204Christian Rocha
internal/config/config.go+3−3
@@ -203,17 +203,17 @@ func Load(workingDir string, debug bool) (*Config, error) {
203func configureViper() {
203func configureViper() {
204 viper.SetConfigName(fmt.Sprintf(".%s", appName))
204 viper.SetConfigName(fmt.Sprintf(".%s", appName))
205 viper.SetConfigType("json")
205 viper.SetConfigType("json")
206−
206+
207 // Unix-style paths
207 // Unix-style paths
208 viper.AddConfigPath("$HOME")
208 viper.AddConfigPath("$HOME")
209 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
209 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
210 viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
210 viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
211−
211+
212 // Windows-style paths
212 // Windows-style paths
213 viper.AddConfigPath(fmt.Sprintf("$USERPROFILE"))
213 viper.AddConfigPath(fmt.Sprintf("$USERPROFILE"))
214 viper.AddConfigPath(fmt.Sprintf("$APPDATA/%s", appName))
214 viper.AddConfigPath(fmt.Sprintf("$APPDATA/%s", appName))
215 viper.AddConfigPath(fmt.Sprintf("$LOCALAPPDATA/%s", appName))
215 viper.AddConfigPath(fmt.Sprintf("$LOCALAPPDATA/%s", appName))
216−
216+
217 viper.SetEnvPrefix(strings.ToUpper(appName))
217 viper.SetEnvPrefix(strings.ToUpper(appName))
218 viper.AutomaticEnv()
218 viper.AutomaticEnv()
219}
219}
internal/llm/tools/bash.go+2−2
@@ -52,7 +52,7 @@ func getSafeReadOnlyCommands() []string {
52 baseCommands := []string{
52 baseCommands := []string{
53 // Cross-platform commands
53 // Cross-platform commands
54 "echo", "hostname", "whoami",
54 "echo", "hostname", "whoami",
55−
55+
56 // Git commands (cross-platform)
56 // Git commands (cross-platform)
57 "git status", "git log", "git diff", "git show", "git branch", "git tag", "git remote", "git ls-files", "git ls-remote",
57 "git status", "git log", "git diff", "git show", "git branch", "git tag", "git remote", "git ls-files", "git ls-remote",
58 "git rev-parse", "git config --get", "git config --list", "git describe", "git blame", "git grep", "git shortlog",
58 "git rev-parse", "git config --get", "git config --list", "git describe", "git blame", "git grep", "git shortlog",
@@ -395,4 +395,4 @@ func countLines(s string) int {
395 return 0
395 return 0
396 }
396 }
397 return len(strings.Split(s, "\n"))
397 return len(strings.Split(s, "\n"))
398−}
398+}
internal/llm/tools/bash_test.go+9−9
@@ -7,12 +7,12 @@ import (
7
7
8func TestGetSafeReadOnlyCommands(t *testing.T) {
8func TestGetSafeReadOnlyCommands(t *testing.T) {
9 commands := getSafeReadOnlyCommands()
9 commands := getSafeReadOnlyCommands()
10−
10+
11 // Check that we have some commands
11 // Check that we have some commands
12 if len(commands) == 0 {
12 if len(commands) == 0 {
13 t.Fatal("Expected some safe commands, got none")
13 t.Fatal("Expected some safe commands, got none")
14 }
14 }
15−
15+
16 // Check for cross-platform commands that should always be present
16 // Check for cross-platform commands that should always be present
17 crossPlatformCommands := []string{"echo", "hostname", "whoami", "git status", "go version"}
17 crossPlatformCommands := []string{"echo", "hostname", "whoami", "git status", "go version"}
18 for _, cmd := range crossPlatformCommands {
18 for _, cmd := range crossPlatformCommands {
@@ -27,7 +27,7 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
27 t.Errorf("Expected cross-platform command %q to be in safe commands", cmd)
27 t.Errorf("Expected cross-platform command %q to be in safe commands", cmd)
28 }
28 }
29 }
29 }
30−
30+
31 if runtime.GOOS == "windows" {
31 if runtime.GOOS == "windows" {
32 // Check for Windows-specific commands
32 // Check for Windows-specific commands
33 windowsCommands := []string{"dir", "type", "Get-Process"}
33 windowsCommands := []string{"dir", "type", "Get-Process"}
@@ -43,7 +43,7 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
43 t.Errorf("Expected Windows command %q to be in safe commands on Windows", cmd)
43 t.Errorf("Expected Windows command %q to be in safe commands on Windows", cmd)
44 }
44 }
45 }
45 }
46−
46+
47 // Check that Unix commands are NOT present on Windows
47 // Check that Unix commands are NOT present on Windows
48 unixCommands := []string{"ls", "pwd", "ps"}
48 unixCommands := []string{"ls", "pwd", "ps"}
49 for _, cmd := range unixCommands {
49 for _, cmd := range unixCommands {
@@ -73,7 +73,7 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
73 t.Errorf("Expected Unix command %q to be in safe commands on Unix", cmd)
73 t.Errorf("Expected Unix command %q to be in safe commands on Unix", cmd)
74 }
74 }
75 }
75 }
76−
76+
77 // Check that Windows-specific commands are NOT present on Unix
77 // Check that Windows-specific commands are NOT present on Unix
78 windowsOnlyCommands := []string{"dir", "Get-Process", "systeminfo"}
78 windowsOnlyCommands := []string{"dir", "Get-Process", "systeminfo"}
79 for _, cmd := range windowsOnlyCommands {
79 for _, cmd := range windowsOnlyCommands {
@@ -94,10 +94,10 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
94func TestPlatformSpecificSafeCommands(t *testing.T) {
94func TestPlatformSpecificSafeCommands(t *testing.T) {
95 // Test that the function returns different results on different platforms
95 // Test that the function returns different results on different platforms
96 commands := getSafeReadOnlyCommands()
96 commands := getSafeReadOnlyCommands()
97−
97+
98 hasWindowsCommands := false
98 hasWindowsCommands := false
99 hasUnixCommands := false
99 hasUnixCommands := false
100−
100+
101 for _, cmd := range commands {
101 for _, cmd := range commands {
102 if cmd == "dir" || cmd == "Get-Process" || cmd == "systeminfo" {
102 if cmd == "dir" || cmd == "Get-Process" || cmd == "systeminfo" {
103 hasWindowsCommands = true
103 hasWindowsCommands = true
@@ -106,7 +106,7 @@ func TestPlatformSpecificSafeCommands(t *testing.T) {
106 hasUnixCommands = true
106 hasUnixCommands = true
107 }
107 }
108 }
108 }
109−
109+
110 if runtime.GOOS == "windows" {
110 if runtime.GOOS == "windows" {
111 if !hasWindowsCommands {
111 if !hasWindowsCommands {
112 t.Error("Expected Windows commands on Windows platform")
112 t.Error("Expected Windows commands on Windows platform")
@@ -122,4 +122,4 @@ func TestPlatformSpecificSafeCommands(t *testing.T) {
122 t.Error("Expected Unix commands on Unix platform")
122 t.Error("Expected Unix commands on Unix platform")
123 }
123 }
124 }
124 }
125−}
125+}
internal/llm/tools/shell/shell.go+19−19
@@ -1,7 +1,7 @@
1// Package shell provides cross-platform shell execution capabilities.
1// Package shell provides cross-platform shell execution capabilities.
2−//
2+//
3// WINDOWS COMPATIBILITY:
3// WINDOWS COMPATIBILITY:
4−// This implementation provides both POSIX shell emulation (mvdan.cc/sh/v3) and
4+// This implementation provides both POSIX shell emulation (mvdan.cc/sh/v3) and
5// native Windows shell support (cmd.exe/PowerShell) for optimal compatibility:
5// native Windows shell support (cmd.exe/PowerShell) for optimal compatibility:
6// - On Windows: Uses native cmd.exe or PowerShell for Windows-specific commands
6// - On Windows: Uses native cmd.exe or PowerShell for Windows-specific commands
7// - Cross-platform: Falls back to POSIX emulation for Unix-style commands
7// - Cross-platform: Falls back to POSIX emulation for Unix-style commands
@@ -86,7 +86,7 @@ func (s *PersistentShell) Exec(ctx context.Context, command string) (string, str
86
86
87 // Determine which shell to use based on platform and command
87 // Determine which shell to use based on platform and command
88 shellType := s.determineShellType(command)
88 shellType := s.determineShellType(command)
89−
89+
90 switch shellType {
90 switch shellType {
91 case ShellTypeCmd:
91 case ShellTypeCmd:
92 return s.execWindows(ctx, command, "cmd")
92 return s.execWindows(ctx, command, "cmd")
@@ -110,19 +110,19 @@ func (s *PersistentShell) determineShellType(command string) ShellType {
110 }
110 }
111
111
112 firstCmd := strings.ToLower(parts[0])
112 firstCmd := strings.ToLower(parts[0])
113−
113+
114 // Check if it's a Windows-specific command
114 // Check if it's a Windows-specific command
115 if windowsNativeCommands[firstCmd] {
115 if windowsNativeCommands[firstCmd] {
116 return ShellTypeCmd
116 return ShellTypeCmd
117 }
117 }
118−
118+
119 // Check for PowerShell-specific syntax
119 // Check for PowerShell-specific syntax
120− if strings.Contains(command, "Get-") || strings.Contains(command, "Set-") ||
120+ if strings.Contains(command, "Get-") || strings.Contains(command, "Set-") ||
121− strings.Contains(command, "New-") || strings.Contains(command, "$_") ||
121+ strings.Contains(command, "New-") || strings.Contains(command, "$_") ||
122− strings.Contains(command, "| Where-Object") || strings.Contains(command, "| ForEach-Object") {
122+ strings.Contains(command, "| Where-Object") || strings.Contains(command, "| ForEach-Object") {
123 return ShellTypePowerShell
123 return ShellTypePowerShell
124 }
124 }
125−
125+
126 // Default to POSIX emulation for cross-platform compatibility
126 // Default to POSIX emulation for cross-platform compatibility
127 return ShellTypePOSIX
127 return ShellTypePOSIX
128}
128}
@@ -130,12 +130,12 @@ func (s *PersistentShell) determineShellType(command string) ShellType {
130// execWindows executes commands using native Windows shells (cmd.exe or PowerShell)
130// execWindows executes commands using native Windows shells (cmd.exe or PowerShell)
131func (s *PersistentShell) execWindows(ctx context.Context, command string, shell string) (string, string, error) {
131func (s *PersistentShell) execWindows(ctx context.Context, command string, shell string) (string, string, error) {
132 var cmd *exec.Cmd
132 var cmd *exec.Cmd
133−
133+
134 // Handle directory changes specially to maintain persistent shell behavior
134 // Handle directory changes specially to maintain persistent shell behavior
135 if strings.HasPrefix(strings.TrimSpace(command), "cd ") {
135 if strings.HasPrefix(strings.TrimSpace(command), "cd ") {
136 return s.handleWindowsCD(command)
136 return s.handleWindowsCD(command)
137 }
137 }
138−
138+
139 switch shell {
139 switch shell {
140 case "cmd":
140 case "cmd":
141 // Use cmd.exe for Windows commands
141 // Use cmd.exe for Windows commands
@@ -150,16 +150,16 @@ func (s *PersistentShell) execWindows(ctx context.Context, command string, shell
150 default:
150 default:
151 return "", "", fmt.Errorf("unsupported Windows shell: %s", shell)
151 return "", "", fmt.Errorf("unsupported Windows shell: %s", shell)
152 }
152 }
153−
153+
154 // Set environment variables
154 // Set environment variables
155 cmd.Env = s.env
155 cmd.Env = s.env
156−
156+
157 var stdout, stderr bytes.Buffer
157 var stdout, stderr bytes.Buffer
158 cmd.Stdout = &stdout
158 cmd.Stdout = &stdout
159 cmd.Stderr = &stderr
159 cmd.Stderr = &stderr
160−
160+
161 err := cmd.Run()
161 err := cmd.Run()
162−
162+
163 logging.InfoPersist("Windows command finished", "shell", shell, "command", command, "err", err)
163 logging.InfoPersist("Windows command finished", "shell", shell, "command", command, "err", err)
164 return stdout.String(), stderr.String(), err
164 return stdout.String(), stderr.String(), err
165}
165}
@@ -171,9 +171,9 @@ func (s *PersistentShell) handleWindowsCD(command string) (string, string, error
171 if len(parts) < 2 {
171 if len(parts) < 2 {
172 return "", "cd: missing directory argument", fmt.Errorf("missing directory argument")
172 return "", "cd: missing directory argument", fmt.Errorf("missing directory argument")
173 }
173 }
174−
174+
175 targetDir := parts[1]
175 targetDir := parts[1]
176−
176+
177 // Handle relative paths
177 // Handle relative paths
178 if !strings.Contains(targetDir, ":") && !strings.HasPrefix(targetDir, "\\") {
178 if !strings.Contains(targetDir, ":") && !strings.HasPrefix(targetDir, "\\") {
179 // Relative path - resolve against current directory
179 // Relative path - resolve against current directory
@@ -193,12 +193,12 @@ func (s *PersistentShell) handleWindowsCD(command string) (string, string, error
193 // Absolute path
193 // Absolute path
194 s.cwd = targetDir
194 s.cwd = targetDir
195 }
195 }
196−
196+
197 // Verify the directory exists
197 // Verify the directory exists
198 if _, err := os.Stat(s.cwd); err != nil {
198 if _, err := os.Stat(s.cwd); err != nil {
199 return "", fmt.Sprintf("cd: %s: No such file or directory", targetDir), err
199 return "", fmt.Sprintf("cd: %s: No such file or directory", targetDir), err
200 }
200 }
201−
201+
202 return "", "", nil
202 return "", "", nil
203}
203}
204
204
internal/llm/tools/shell/shell_test.go+18−18
@@ -87,10 +87,10 @@ func TestRunContinuity(t *testing.T) {
87
87
88func TestShellTypeDetection(t *testing.T) {
88func TestShellTypeDetection(t *testing.T) {
89 shell := &PersistentShell{}
89 shell := &PersistentShell{}
90−
90+
91 tests := []struct {
91 tests := []struct {
92− command string
92+ command string
93− expected ShellType
93+ expected ShellType
94 windowsOnly bool
94 windowsOnly bool
95 }{
95 }{
96 // Windows-specific commands
96 // Windows-specific commands
@@ -100,14 +100,14 @@ func TestShellTypeDetection(t *testing.T) {
100 {"del file.txt", ShellTypeCmd, true},
100 {"del file.txt", ShellTypeCmd, true},
101 {"md newdir", ShellTypeCmd, true},
101 {"md newdir", ShellTypeCmd, true},
102 {"tasklist", ShellTypeCmd, true},
102 {"tasklist", ShellTypeCmd, true},
103−
103+
104 // PowerShell commands
104 // PowerShell commands
105 {"Get-Process", ShellTypePowerShell, true},
105 {"Get-Process", ShellTypePowerShell, true},
106 {"Get-ChildItem", ShellTypePowerShell, true},
106 {"Get-ChildItem", ShellTypePowerShell, true},
107 {"Set-Location C:\\", ShellTypePowerShell, true},
107 {"Set-Location C:\\", ShellTypePowerShell, true},
108 {"Get-Content file.txt | Where-Object {$_ -match 'pattern'}", ShellTypePowerShell, true},
108 {"Get-Content file.txt | Where-Object {$_ -match 'pattern'}", ShellTypePowerShell, true},
109 {"$files = Get-ChildItem", ShellTypePowerShell, true},
109 {"$files = Get-ChildItem", ShellTypePowerShell, true},
110−
110+
111 // Unix/cross-platform commands
111 // Unix/cross-platform commands
112 {"ls -la", ShellTypePOSIX, false},
112 {"ls -la", ShellTypePOSIX, false},
113 {"cat file.txt", ShellTypePOSIX, false},
113 {"cat file.txt", ShellTypePOSIX, false},
@@ -116,11 +116,11 @@ func TestShellTypeDetection(t *testing.T) {
116 {"git status", ShellTypePOSIX, false},
116 {"git status", ShellTypePOSIX, false},
117 {"go build", ShellTypePOSIX, false},
117 {"go build", ShellTypePOSIX, false},
118 }
118 }
119−
119+
120 for _, test := range tests {
120 for _, test := range tests {
121 t.Run(test.command, func(t *testing.T) {
121 t.Run(test.command, func(t *testing.T) {
122 result := shell.determineShellType(test.command)
122 result := shell.determineShellType(test.command)
123−
123+
124 if test.windowsOnly && runtime.GOOS != "windows" {
124 if test.windowsOnly && runtime.GOOS != "windows" {
125 // On non-Windows systems, everything should use POSIX
125 // On non-Windows systems, everything should use POSIX
126 if result != ShellTypePOSIX {
126 if result != ShellTypePOSIX {
@@ -140,12 +140,12 @@ func TestWindowsCDHandling(t *testing.T) {
140 if runtime.GOOS != "windows" {
140 if runtime.GOOS != "windows" {
141 t.Skip("Windows CD handling test only runs on Windows")
141 t.Skip("Windows CD handling test only runs on Windows")
142 }
142 }
143−
143+
144 shell := &PersistentShell{
144 shell := &PersistentShell{
145 cwd: "C:\\Users",
145 cwd: "C:\\Users",
146 env: []string{},
146 env: []string{},
147 }
147 }
148−
148+
149 tests := []struct {
149 tests := []struct {
150 command string
150 command string
151 expectedCwd string
151 expectedCwd string
@@ -156,12 +156,12 @@ func TestWindowsCDHandling(t *testing.T) {
156 {"cd C:\\Windows", "C:\\Windows", false},
156 {"cd C:\\Windows", "C:\\Windows", false},
157 {"cd", "", true}, // Missing argument
157 {"cd", "", true}, // Missing argument
158 }
158 }
159−
159+
160 for _, test := range tests {
160 for _, test := range tests {
161 t.Run(test.command, func(t *testing.T) {
161 t.Run(test.command, func(t *testing.T) {
162 originalCwd := shell.cwd
162 originalCwd := shell.cwd
163 stdout, stderr, err := shell.handleWindowsCD(test.command)
163 stdout, stderr, err := shell.handleWindowsCD(test.command)
164−
164+
165 if test.shouldError {
165 if test.shouldError {
166 if err == nil {
166 if err == nil {
167 t.Errorf("Command %q should have failed", test.command)
167 t.Errorf("Command %q should have failed", test.command)
@@ -174,7 +174,7 @@ func TestWindowsCDHandling(t *testing.T) {
174 t.Errorf("Command %q: expected cwd %q, got %q", test.command, test.expectedCwd, shell.cwd)
174 t.Errorf("Command %q: expected cwd %q, got %q", test.command, test.expectedCwd, shell.cwd)
175 }
175 }
176 }
176 }
177−
177+
178 // Reset for next test
178 // Reset for next test
179 shell.cwd = originalCwd
179 shell.cwd = originalCwd
180 _ = stdout
180 _ = stdout
@@ -187,17 +187,17 @@ func TestCrossPlatformExecution(t *testing.T) {
187 shell := newPersistentShell(".")
187 shell := newPersistentShell(".")
188 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
188 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
189 defer cancel()
189 defer cancel()
190−
190+
191 // Test a simple command that should work on all platforms
191 // Test a simple command that should work on all platforms
192 stdout, stderr, err := shell.Exec(ctx, "echo hello")
192 stdout, stderr, err := shell.Exec(ctx, "echo hello")
193 if err != nil {
193 if err != nil {
194 t.Fatalf("Echo command failed: %v, stderr: %s", err, stderr)
194 t.Fatalf("Echo command failed: %v, stderr: %s", err, stderr)
195 }
195 }
196−
196+
197 if stdout == "" {
197 if stdout == "" {
198 t.Error("Echo command produced no output")
198 t.Error("Echo command produced no output")
199 }
199 }
200−
200+
201 // The output should contain "hello" regardless of platform
201 // The output should contain "hello" regardless of platform
202 if !strings.Contains(strings.ToLower(stdout), "hello") {
202 if !strings.Contains(strings.ToLower(stdout), "hello") {
203 t.Errorf("Echo output should contain 'hello', got: %q", stdout)
203 t.Errorf("Echo output should contain 'hello', got: %q", stdout)
@@ -208,17 +208,17 @@ func TestWindowsNativeCommands(t *testing.T) {
208 if runtime.GOOS != "windows" {
208 if runtime.GOOS != "windows" {
209 t.Skip("Windows native command test only runs on Windows")
209 t.Skip("Windows native command test only runs on Windows")
210 }
210 }
211−
211+
212 shell := newPersistentShell(".")
212 shell := newPersistentShell(".")
213 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
213 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
214 defer cancel()
214 defer cancel()
215−
215+
216 // Test Windows dir command
216 // Test Windows dir command
217 stdout, stderr, err := shell.Exec(ctx, "dir")
217 stdout, stderr, err := shell.Exec(ctx, "dir")
218 if err != nil {
218 if err != nil {
219 t.Fatalf("Dir command failed: %v, stderr: %s", err, stderr)
219 t.Fatalf("Dir command failed: %v, stderr: %s", err, stderr)
220 }
220 }
221−
221+
222 if stdout == "" {
222 if stdout == "" {
223 t.Error("Dir command produced no output")
223 t.Error("Dir command produced no output")
224 }
224 }