Detailed changes
@@ -203,17 +203,17 @@ func Load(workingDir string, debug bool) (*Config, error) {
func configureViper() {
viper.SetConfigName(fmt.Sprintf(".%s", appName))
viper.SetConfigType("json")
-
+
// Unix-style paths
viper.AddConfigPath("$HOME")
viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
-
+
// Windows-style paths
viper.AddConfigPath(fmt.Sprintf("$USERPROFILE"))
viper.AddConfigPath(fmt.Sprintf("$APPDATA/%s", appName))
viper.AddConfigPath(fmt.Sprintf("$LOCALAPPDATA/%s", appName))
-
+
viper.SetEnvPrefix(strings.ToUpper(appName))
viper.AutomaticEnv()
}
@@ -52,7 +52,7 @@ func getSafeReadOnlyCommands() []string {
baseCommands := []string{
// Cross-platform commands
"echo", "hostname", "whoami",
-
+
// Git commands (cross-platform)
"git status", "git log", "git diff", "git show", "git branch", "git tag", "git remote", "git ls-files", "git ls-remote",
"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 {
return 0
}
return len(strings.Split(s, "\n"))
-}
+}
@@ -7,12 +7,12 @@ import (
func TestGetSafeReadOnlyCommands(t *testing.T) {
commands := getSafeReadOnlyCommands()
-
+
// Check that we have some commands
if len(commands) == 0 {
t.Fatal("Expected some safe commands, got none")
}
-
+
// Check for cross-platform commands that should always be present
crossPlatformCommands := []string{"echo", "hostname", "whoami", "git status", "go version"}
for _, cmd := range crossPlatformCommands {
@@ -27,7 +27,7 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
t.Errorf("Expected cross-platform command %q to be in safe commands", cmd)
}
}
-
+
if runtime.GOOS == "windows" {
// Check for Windows-specific commands
windowsCommands := []string{"dir", "type", "Get-Process"}
@@ -43,7 +43,7 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
t.Errorf("Expected Windows command %q to be in safe commands on Windows", cmd)
}
}
-
+
// Check that Unix commands are NOT present on Windows
unixCommands := []string{"ls", "pwd", "ps"}
for _, cmd := range unixCommands {
@@ -73,7 +73,7 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
t.Errorf("Expected Unix command %q to be in safe commands on Unix", cmd)
}
}
-
+
// Check that Windows-specific commands are NOT present on Unix
windowsOnlyCommands := []string{"dir", "Get-Process", "systeminfo"}
for _, cmd := range windowsOnlyCommands {
@@ -94,10 +94,10 @@ func TestGetSafeReadOnlyCommands(t *testing.T) {
func TestPlatformSpecificSafeCommands(t *testing.T) {
// Test that the function returns different results on different platforms
commands := getSafeReadOnlyCommands()
-
+
hasWindowsCommands := false
hasUnixCommands := false
-
+
for _, cmd := range commands {
if cmd == "dir" || cmd == "Get-Process" || cmd == "systeminfo" {
hasWindowsCommands = true
@@ -106,7 +106,7 @@ func TestPlatformSpecificSafeCommands(t *testing.T) {
hasUnixCommands = true
}
}
-
+
if runtime.GOOS == "windows" {
if !hasWindowsCommands {
t.Error("Expected Windows commands on Windows platform")
@@ -122,4 +122,4 @@ func TestPlatformSpecificSafeCommands(t *testing.T) {
t.Error("Expected Unix commands on Unix platform")
}
}
-}
+}
@@ -1,7 +1,7 @@
// Package shell provides cross-platform shell execution capabilities.
-//
+//
// WINDOWS COMPATIBILITY:
-// This implementation provides both POSIX shell emulation (mvdan.cc/sh/v3) and
+// This implementation provides both POSIX shell emulation (mvdan.cc/sh/v3) and
// native Windows shell support (cmd.exe/PowerShell) for optimal compatibility:
// - On Windows: Uses native cmd.exe or PowerShell for Windows-specific commands
// - 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
// Determine which shell to use based on platform and command
shellType := s.determineShellType(command)
-
+
switch shellType {
case ShellTypeCmd:
return s.execWindows(ctx, command, "cmd")
@@ -110,19 +110,19 @@ func (s *PersistentShell) determineShellType(command string) ShellType {
}
firstCmd := strings.ToLower(parts[0])
-
+
// Check if it's a Windows-specific command
if windowsNativeCommands[firstCmd] {
return ShellTypeCmd
}
-
+
// Check for PowerShell-specific syntax
- if strings.Contains(command, "Get-") || strings.Contains(command, "Set-") ||
- strings.Contains(command, "New-") || strings.Contains(command, "$_") ||
- strings.Contains(command, "| Where-Object") || strings.Contains(command, "| ForEach-Object") {
+ if strings.Contains(command, "Get-") || strings.Contains(command, "Set-") ||
+ strings.Contains(command, "New-") || strings.Contains(command, "$_") ||
+ strings.Contains(command, "| Where-Object") || strings.Contains(command, "| ForEach-Object") {
return ShellTypePowerShell
}
-
+
// Default to POSIX emulation for cross-platform compatibility
return ShellTypePOSIX
}
@@ -130,12 +130,12 @@ func (s *PersistentShell) determineShellType(command string) ShellType {
// execWindows executes commands using native Windows shells (cmd.exe or PowerShell)
func (s *PersistentShell) execWindows(ctx context.Context, command string, shell string) (string, string, error) {
var cmd *exec.Cmd
-
+
// Handle directory changes specially to maintain persistent shell behavior
if strings.HasPrefix(strings.TrimSpace(command), "cd ") {
return s.handleWindowsCD(command)
}
-
+
switch shell {
case "cmd":
// Use cmd.exe for Windows commands
@@ -150,16 +150,16 @@ func (s *PersistentShell) execWindows(ctx context.Context, command string, shell
default:
return "", "", fmt.Errorf("unsupported Windows shell: %s", shell)
}
-
+
// Set environment variables
cmd.Env = s.env
-
+
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
-
+
err := cmd.Run()
-
+
logging.InfoPersist("Windows command finished", "shell", shell, "command", command, "err", err)
return stdout.String(), stderr.String(), err
}
@@ -171,9 +171,9 @@ func (s *PersistentShell) handleWindowsCD(command string) (string, string, error
if len(parts) < 2 {
return "", "cd: missing directory argument", fmt.Errorf("missing directory argument")
}
-
+
targetDir := parts[1]
-
+
// Handle relative paths
if !strings.Contains(targetDir, ":") && !strings.HasPrefix(targetDir, "\\") {
// Relative path - resolve against current directory
@@ -193,12 +193,12 @@ func (s *PersistentShell) handleWindowsCD(command string) (string, string, error
// Absolute path
s.cwd = targetDir
}
-
+
// Verify the directory exists
if _, err := os.Stat(s.cwd); err != nil {
return "", fmt.Sprintf("cd: %s: No such file or directory", targetDir), err
}
-
+
return "", "", nil
}
@@ -87,10 +87,10 @@ func TestRunContinuity(t *testing.T) {
func TestShellTypeDetection(t *testing.T) {
shell := &PersistentShell{}
-
+
tests := []struct {
- command string
- expected ShellType
+ command string
+ expected ShellType
windowsOnly bool
}{
// Windows-specific commands
@@ -100,14 +100,14 @@ func TestShellTypeDetection(t *testing.T) {
{"del file.txt", ShellTypeCmd, true},
{"md newdir", ShellTypeCmd, true},
{"tasklist", ShellTypeCmd, true},
-
+
// PowerShell commands
{"Get-Process", ShellTypePowerShell, true},
{"Get-ChildItem", ShellTypePowerShell, true},
{"Set-Location C:\\", ShellTypePowerShell, true},
{"Get-Content file.txt | Where-Object {$_ -match 'pattern'}", ShellTypePowerShell, true},
{"$files = Get-ChildItem", ShellTypePowerShell, true},
-
+
// Unix/cross-platform commands
{"ls -la", ShellTypePOSIX, false},
{"cat file.txt", ShellTypePOSIX, false},
@@ -116,11 +116,11 @@ func TestShellTypeDetection(t *testing.T) {
{"git status", ShellTypePOSIX, false},
{"go build", ShellTypePOSIX, false},
}
-
+
for _, test := range tests {
t.Run(test.command, func(t *testing.T) {
result := shell.determineShellType(test.command)
-
+
if test.windowsOnly && runtime.GOOS != "windows" {
// On non-Windows systems, everything should use POSIX
if result != ShellTypePOSIX {
@@ -140,12 +140,12 @@ func TestWindowsCDHandling(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows CD handling test only runs on Windows")
}
-
+
shell := &PersistentShell{
cwd: "C:\\Users",
env: []string{},
}
-
+
tests := []struct {
command string
expectedCwd string
@@ -156,12 +156,12 @@ func TestWindowsCDHandling(t *testing.T) {
{"cd C:\\Windows", "C:\\Windows", false},
{"cd", "", true}, // Missing argument
}
-
+
for _, test := range tests {
t.Run(test.command, func(t *testing.T) {
originalCwd := shell.cwd
stdout, stderr, err := shell.handleWindowsCD(test.command)
-
+
if test.shouldError {
if err == nil {
t.Errorf("Command %q should have failed", test.command)
@@ -174,7 +174,7 @@ func TestWindowsCDHandling(t *testing.T) {
t.Errorf("Command %q: expected cwd %q, got %q", test.command, test.expectedCwd, shell.cwd)
}
}
-
+
// Reset for next test
shell.cwd = originalCwd
_ = stdout
@@ -187,17 +187,17 @@ func TestCrossPlatformExecution(t *testing.T) {
shell := newPersistentShell(".")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
-
+
// Test a simple command that should work on all platforms
stdout, stderr, err := shell.Exec(ctx, "echo hello")
if err != nil {
t.Fatalf("Echo command failed: %v, stderr: %s", err, stderr)
}
-
+
if stdout == "" {
t.Error("Echo command produced no output")
}
-
+
// The output should contain "hello" regardless of platform
if !strings.Contains(strings.ToLower(stdout), "hello") {
t.Errorf("Echo output should contain 'hello', got: %q", stdout)
@@ -208,17 +208,17 @@ func TestWindowsNativeCommands(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows native command test only runs on Windows")
}
-
+
shell := newPersistentShell(".")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
-
+
// Test Windows dir command
stdout, stderr, err := shell.Exec(ctx, "dir")
if err != nil {
t.Fatalf("Dir command failed: %v, stderr: %s", err, stderr)
}
-
+
if stdout == "" {
t.Error("Dir command produced no output")
}