From fb0f1728e406eb41423911c99bd73aaf87ff3ccc Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Wed, 25 Jun 2025 16:43:14 -0400 Subject: [PATCH 1/3] docs(readme): add (very) basic getting started instuctions --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index d94fb2690b7ad370d3096f92504cd203268d08b5..d31a932ab0be4f5f6e50adddc33e0ef1f833d369 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,28 @@ Crush is a tool for building software with AI. +## Getting Started + +For now, the quickest way to get started is to set an environment variable for +your preferred provider. Note that you can switch between providers mid- +sessions, so you're welcome to set environment variables for multiple +providers. + +| Environment Variable | Provider | +| -------------------------- | -------------------------------------------------- | +| `ANTHROPIC_API_KEY` | Anthropic | +| `OPENAI_API_KEY` | OpenAI | +| `GEMINI_API_KEY` | Google Gemini | +| `VERTEXAI_PROJECT` | Google Cloud VertexAI (Gemini) | +| `VERTEXAI_LOCATION` | Google Cloud VertexAI (Gemini) | +| `GROQ_API_KEY` | Groq | +| `AWS_ACCESS_KEY_ID` | AWS Bedrock (Claude) | +| `AWS_SECRET_ACCESS_KEY` | AWS Bedrock (Claude) | +| `AWS_REGION` | AWS Bedrock (Claude) | +| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI models | +| `AZURE_OPENAI_API_KEY` | Azure OpenAI models (optional when using Entra ID) | +| `AZURE_OPENAI_API_VERSION` | Azure OpenAI models | + ## License [MIT](https://github.com/charmbracelet/crush/raw/main/LICENSE) From af72204ab19104137d9af34c57fd8b479931fcf7 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Wed, 25 Jun 2025 17:12:31 -0400 Subject: [PATCH 2/3] chore(lint): gofmt --- internal/config/config.go | 6 ++-- internal/llm/tools/bash.go | 4 +-- internal/llm/tools/bash_test.go | 18 ++++++------ internal/llm/tools/shell/shell.go | 38 +++++++++++++------------- internal/llm/tools/shell/shell_test.go | 36 ++++++++++++------------ 5 files changed, 51 insertions(+), 51 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index fef8fd43b60a183ad0e161ef3d1cf6ff17156c05..3f932221c5b5ecdd21164a3efe8ed85f60cc1cce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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() } diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index d9c19b808d487641f06529036c918e73225be7c8..194632742d1f9916172cb0933c37d1e7a42adbf3 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -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")) -} \ No newline at end of file +} diff --git a/internal/llm/tools/bash_test.go b/internal/llm/tools/bash_test.go index b26c96097d25414567f0c456b88dbf54e6503e12..870cc9f0c05dffa232cbcb00d74a9c88dd5f3a42 100644 --- a/internal/llm/tools/bash_test.go +++ b/internal/llm/tools/bash_test.go @@ -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") } } -} \ No newline at end of file +} diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index 475746f7a4758a14d44dd59348fb264a2d5461f0..81fa33085a40bf2bccd8814a5b179d3ca4453a8c 100644 --- a/internal/llm/tools/shell/shell.go +++ b/internal/llm/tools/shell/shell.go @@ -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 } diff --git a/internal/llm/tools/shell/shell_test.go b/internal/llm/tools/shell/shell_test.go index d54ab8a051d6aa8123de38f2bfe5be217565e29c..e4273a7a60a2ea96069b97b9d111e4a5b7a4c73a 100644 --- a/internal/llm/tools/shell/shell_test.go +++ b/internal/llm/tools/shell/shell_test.go @@ -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") } From 41f97bc80e04a89ac84cc2db5f4391ba12d92aaf Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Wed, 25 Jun 2025 17:17:55 -0400 Subject: [PATCH 3/3] docs(crush.md): add more specific Go formatting instructions --- CRUSH.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CRUSH.md b/CRUSH.md index ecd37043c0f8638ccb64bee680675dc19e00ffb2..e2f6053c110f317ee85113a2f08359673342e645 100644 --- a/CRUSH.md +++ b/CRUSH.md @@ -1,6 +1,7 @@ # Crush Development Guide ## Build/Test/Lint Commands + - **Build**: `go build .` or `go run .` - **Test**: `task test` or `go test ./...` (run single test: `go test ./internal/llm/prompt -run TestGetContextFromPaths`) - **Lint**: `task lint` (golangci-lint run) or `task lint-fix` (with --fix) @@ -8,6 +9,7 @@ - **Dev**: `task dev` (runs with profiling enabled) ## Code Style Guidelines + - **Imports**: Use goimports formatting, group stdlib, external, internal packages - **Formatting**: Use gofumpt (stricter than gofmt), enabled in golangci-lint - **Naming**: Standard Go conventions - PascalCase for exported, camelCase for unexported @@ -21,3 +23,12 @@ - **JSON tags**: Use snake_case for JSON field names - **File permissions**: Use octal notation (0o755, 0o644) for file permissions - **Comments**: End comments in periods unless comments are at the end of the line. + +## Formatting + +- ALWAYS format any Go code you write. + - First, try `goftumpt -w .`. + - If `gofumpt` is not available, use `goimports`. + - If `goimports` is not available, use `gofmt`. + - You can also use `task fmt` to run `gofumpt -w .` on the entire project, + as long as `gofumpt` is on the `PATH`.