on main help sign in
Merge branch 'main' into chore/modernize
commit67d5af14c77ac4e634bc864f2ca203c36f8a2099 authorTai Groot <tai@taigrr.com> dateWed 25 Jun 2025, 17:51 (1 year ago) parent829bb7041f97bc
7 files changed+75−43
CRUSH.md+11 README.md+22 internal/config/config.go+3−3 internal/llm/tools/bash.go+2−2 internal/llm/tools/bash_test.go−1 internal/llm/tools/shell/shell.go+19−19 internal/llm/tools/shell/shell_test.go+18−18
CRUSH.md+11
@@ -1,6 +1,7 @@
11# Crush Development Guide
22
33## Build/Test/Lint Commands
4+
45- **Build**: `go build .` or `go run .`
56- **Test**: `task test` or `go test ./...` (run single test: `go test ./internal/llm/prompt -run TestGetContextFromPaths`)
67- **Lint**: `task lint` (golangci-lint run) or `task lint-fix` (with --fix)
@@ -8,6 +9,7 @@
89- **Dev**: `task dev` (runs with profiling enabled)
910
1011## Code Style Guidelines
12+
1113- **Imports**: Use goimports formatting, group stdlib, external, internal packages
1214- **Formatting**: Use gofumpt (stricter than gofmt), enabled in golangci-lint
1315- **Naming**: Standard Go conventions - PascalCase for exported, camelCase for unexported
@@ -21,3 +23,12 @@
2123- **JSON tags**: Use snake_case for JSON field names
2224- **File permissions**: Use octal notation (0o755, 0o644) for file permissions
2325- **Comments**: End comments in periods unless comments are at the end of the line.
26+
27+## Formatting
28+
29+- ALWAYS format any Go code you write.
30+ - First, try `goftumpt -w .`.
31+ - If `gofumpt` is not available, use `goimports`.
32+ - If `goimports` is not available, use `gofmt`.
33+ - You can also use `task fmt` to run `gofumpt -w .` on the entire project,
34+ as long as `gofumpt` is on the `PATH`.
README.md+22
@@ -10,6 +10,28 @@
1010
1111Crush is a tool for building software with AI.
1212
13+## Getting Started
14+
15+For now, the quickest way to get started is to set an environment variable for
16+your preferred provider. Note that you can switch between providers mid-
17+sessions, so you're welcome to set environment variables for multiple
18+providers.
19+
20+| Environment Variable | Provider |
21+| -------------------------- | -------------------------------------------------- |
22+| `ANTHROPIC_API_KEY` | Anthropic |
23+| `OPENAI_API_KEY` | OpenAI |
24+| `GEMINI_API_KEY` | Google Gemini |
25+| `VERTEXAI_PROJECT` | Google Cloud VertexAI (Gemini) |
26+| `VERTEXAI_LOCATION` | Google Cloud VertexAI (Gemini) |
27+| `GROQ_API_KEY` | Groq |
28+| `AWS_ACCESS_KEY_ID` | AWS Bedrock (Claude) |
29+| `AWS_SECRET_ACCESS_KEY` | AWS Bedrock (Claude) |
30+| `AWS_REGION` | AWS Bedrock (Claude) |
31+| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI models |
32+| `AZURE_OPENAI_API_KEY` | Azure OpenAI models (optional when using Entra ID) |
33+| `AZURE_OPENAI_API_VERSION` | Azure OpenAI models |
34+
1335## License
1436
1537[MIT](https://github.com/charmbracelet/crush/raw/main/LICENSE)
internal/config/config.go+3−3
@@ -203,17 +203,17 @@ func Load(workingDir string, debug bool) (*Config, error) {
203203func configureViper() {
204204 viper.SetConfigName(fmt.Sprintf(".%s", appName))
205205 viper.SetConfigType("json")
206−
206+
207207 // Unix-style paths
208208 viper.AddConfigPath("$HOME")
209209 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
210210 viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
211−
211+
212212 // Windows-style paths
213213 viper.AddConfigPath(fmt.Sprintf("$USERPROFILE"))
214214 viper.AddConfigPath(fmt.Sprintf("$APPDATA/%s", appName))
215215 viper.AddConfigPath(fmt.Sprintf("$LOCALAPPDATA/%s", appName))
216−
216+
217217 viper.SetEnvPrefix(strings.ToUpper(appName))
218218 viper.AutomaticEnv()
219219}
internal/llm/tools/bash.go+2−2
@@ -52,7 +52,7 @@ func getSafeReadOnlyCommands() []string {
5252 baseCommands := []string{
5353 // Cross-platform commands
5454 "echo", "hostname", "whoami",
55−
55+
5656 // Git commands (cross-platform)
5757 "git status", "git log", "git diff", "git show", "git branch", "git tag", "git remote", "git ls-files", "git ls-remote",
5858 "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 {
395395 return 0
396396 }
397397 return len(strings.Split(s, "\n"))
398−}
398+}
internal/llm/tools/bash_test.go−1
@@ -94,4 +94,3 @@ func TestPlatformSpecificSafeCommands(t *testing.T) {
9494 }
9595 }
9696}
97−
internal/llm/tools/shell/shell.go+19−19
@@ -1,7 +1,7 @@
11// Package shell provides cross-platform shell execution capabilities.
2−//
2+//
33// 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
55// native Windows shell support (cmd.exe/PowerShell) for optimal compatibility:
66// - On Windows: Uses native cmd.exe or PowerShell for Windows-specific commands
77// - 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
8686
8787 // Determine which shell to use based on platform and command
8888 shellType := s.determineShellType(command)
89−
89+
9090 switch shellType {
9191 case ShellTypeCmd:
9292 return s.execWindows(ctx, command, "cmd")
@@ -110,19 +110,19 @@ func (s *PersistentShell) determineShellType(command string) ShellType {
110110 }
111111
112112 firstCmd := strings.ToLower(parts[0])
113−
113+
114114 // Check if it's a Windows-specific command
115115 if windowsNativeCommands[firstCmd] {
116116 return ShellTypeCmd
117117 }
118−
118+
119119 // Check for PowerShell-specific syntax
120− if strings.Contains(command, "Get-") || strings.Contains(command, "Set-") ||
121− strings.Contains(command, "New-") || strings.Contains(command, "$_") ||
122− strings.Contains(command, "| Where-Object") || strings.Contains(command, "| ForEach-Object") {
120+ if strings.Contains(command, "Get-") || strings.Contains(command, "Set-") ||
121+ strings.Contains(command, "New-") || strings.Contains(command, "$_") ||
122+ strings.Contains(command, "| Where-Object") || strings.Contains(command, "| ForEach-Object") {
123123 return ShellTypePowerShell
124124 }
125−
125+
126126 // Default to POSIX emulation for cross-platform compatibility
127127 return ShellTypePOSIX
128128}
@@ -130,12 +130,12 @@ func (s *PersistentShell) determineShellType(command string) ShellType {
130130// execWindows executes commands using native Windows shells (cmd.exe or PowerShell)
131131func (s *PersistentShell) execWindows(ctx context.Context, command string, shell string) (string, string, error) {
132132 var cmd *exec.Cmd
133−
133+
134134 // Handle directory changes specially to maintain persistent shell behavior
135135 if strings.HasPrefix(strings.TrimSpace(command), "cd ") {
136136 return s.handleWindowsCD(command)
137137 }
138−
138+
139139 switch shell {
140140 case "cmd":
141141 // Use cmd.exe for Windows commands
@@ -150,16 +150,16 @@ func (s *PersistentShell) execWindows(ctx context.Context, command string, shell
150150 default:
151151 return "", "", fmt.Errorf("unsupported Windows shell: %s", shell)
152152 }
153−
153+
154154 // Set environment variables
155155 cmd.Env = s.env
156−
156+
157157 var stdout, stderr bytes.Buffer
158158 cmd.Stdout = &stdout
159159 cmd.Stderr = &stderr
160−
160+
161161 err := cmd.Run()
162−
162+
163163 logging.InfoPersist("Windows command finished", "shell", shell, "command", command, "err", err)
164164 return stdout.String(), stderr.String(), err
165165}
@@ -171,9 +171,9 @@ func (s *PersistentShell) handleWindowsCD(command string) (string, string, error
171171 if len(parts) < 2 {
172172 return "", "cd: missing directory argument", fmt.Errorf("missing directory argument")
173173 }
174−
174+
175175 targetDir := parts[1]
176−
176+
177177 // Handle relative paths
178178 if !strings.Contains(targetDir, ":") && !strings.HasPrefix(targetDir, "\\") {
179179 // Relative path - resolve against current directory
@@ -193,12 +193,12 @@ func (s *PersistentShell) handleWindowsCD(command string) (string, string, error
193193 // Absolute path
194194 s.cwd = targetDir
195195 }
196−
196+
197197 // Verify the directory exists
198198 if _, err := os.Stat(s.cwd); err != nil {
199199 return "", fmt.Sprintf("cd: %s: No such file or directory", targetDir), err
200200 }
201−
201+
202202 return "", "", nil
203203}
204204
internal/llm/tools/shell/shell_test.go+18−18
@@ -87,10 +87,10 @@ func TestRunContinuity(t *testing.T) {
8787
8888func TestShellTypeDetection(t *testing.T) {
8989 shell := &PersistentShell{}
90−
90+
9191 tests := []struct {
92− command string
93− expected ShellType
92+ command string
93+ expected ShellType
9494 windowsOnly bool
9595 }{
9696 // Windows-specific commands
@@ -100,14 +100,14 @@ func TestShellTypeDetection(t *testing.T) {
100100 {"del file.txt", ShellTypeCmd, true},
101101 {"md newdir", ShellTypeCmd, true},
102102 {"tasklist", ShellTypeCmd, true},
103−
103+
104104 // PowerShell commands
105105 {"Get-Process", ShellTypePowerShell, true},
106106 {"Get-ChildItem", ShellTypePowerShell, true},
107107 {"Set-Location C:\\", ShellTypePowerShell, true},
108108 {"Get-Content file.txt | Where-Object {$_ -match 'pattern'}", ShellTypePowerShell, true},
109109 {"$files = Get-ChildItem", ShellTypePowerShell, true},
110−
110+
111111 // Unix/cross-platform commands
112112 {"ls -la", ShellTypePOSIX, false},
113113 {"cat file.txt", ShellTypePOSIX, false},
@@ -116,11 +116,11 @@ func TestShellTypeDetection(t *testing.T) {
116116 {"git status", ShellTypePOSIX, false},
117117 {"go build", ShellTypePOSIX, false},
118118 }
119−
119+
120120 for _, test := range tests {
121121 t.Run(test.command, func(t *testing.T) {
122122 result := shell.determineShellType(test.command)
123−
123+
124124 if test.windowsOnly && runtime.GOOS != "windows" {
125125 // On non-Windows systems, everything should use POSIX
126126 if result != ShellTypePOSIX {
@@ -140,12 +140,12 @@ func TestWindowsCDHandling(t *testing.T) {
140140 if runtime.GOOS != "windows" {
141141 t.Skip("Windows CD handling test only runs on Windows")
142142 }
143−
143+
144144 shell := &PersistentShell{
145145 cwd: "C:\\Users",
146146 env: []string{},
147147 }
148−
148+
149149 tests := []struct {
150150 command string
151151 expectedCwd string
@@ -156,12 +156,12 @@ func TestWindowsCDHandling(t *testing.T) {
156156 {"cd C:\\Windows", "C:\\Windows", false},
157157 {"cd", "", true}, // Missing argument
158158 }
159−
159+
160160 for _, test := range tests {
161161 t.Run(test.command, func(t *testing.T) {
162162 originalCwd := shell.cwd
163163 stdout, stderr, err := shell.handleWindowsCD(test.command)
164−
164+
165165 if test.shouldError {
166166 if err == nil {
167167 t.Errorf("Command %q should have failed", test.command)
@@ -174,7 +174,7 @@ func TestWindowsCDHandling(t *testing.T) {
174174 t.Errorf("Command %q: expected cwd %q, got %q", test.command, test.expectedCwd, shell.cwd)
175175 }
176176 }
177−
177+
178178 // Reset for next test
179179 shell.cwd = originalCwd
180180 _ = stdout
@@ -187,17 +187,17 @@ func TestCrossPlatformExecution(t *testing.T) {
187187 shell := newPersistentShell(".")
188188 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
189189 defer cancel()
190−
190+
191191 // Test a simple command that should work on all platforms
192192 stdout, stderr, err := shell.Exec(ctx, "echo hello")
193193 if err != nil {
194194 t.Fatalf("Echo command failed: %v, stderr: %s", err, stderr)
195195 }
196−
196+
197197 if stdout == "" {
198198 t.Error("Echo command produced no output")
199199 }
200−
200+
201201 // The output should contain "hello" regardless of platform
202202 if !strings.Contains(strings.ToLower(stdout), "hello") {
203203 t.Errorf("Echo output should contain 'hello', got: %q", stdout)
@@ -208,17 +208,17 @@ func TestWindowsNativeCommands(t *testing.T) {
208208 if runtime.GOOS != "windows" {
209209 t.Skip("Windows native command test only runs on Windows")
210210 }
211−
211+
212212 shell := newPersistentShell(".")
213213 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
214214 defer cancel()
215−
215+
216216 // Test Windows dir command
217217 stdout, stderr, err := shell.Exec(ctx, "dir")
218218 if err != nil {
219219 t.Fatalf("Dir command failed: %v, stderr: %s", err, stderr)
220220 }
221−
221+
222222 if stdout == "" {
223223 t.Error("Dir command produced no output")
224224 }