Skip to content

Commit e8f62eb

Browse files
committed
Add autofix with AI
1 parent 4749af6 commit e8f62eb

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

index.html

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ <h1 class="text-3xl font-semibold mb-2">Bash Script Tools</h1>
6565
>
6666
<span>Autofix</span>
6767
</button>
68+
69+
<button
70+
onclick="autofixAICode()"
71+
class="inline-flex items-center px-4 py-2 bg-zinc-900 text-zinc-100 rounded-md hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-zinc-700 transition-colors font-medium text-sm border border-zinc-800"
72+
>
73+
<span>Autofix (AI)</span>
74+
</button>
6875
</div>
6976

7077
<!-- Output Box -->
@@ -175,6 +182,33 @@ <h1 class="text-3xl font-semibold mb-2">Bash Script Tools</h1>
175182
console.error("Autofix error:", error);
176183
}
177184
}
185+
186+
async function autofixAICode() {
187+
const code = editor.getValue();
188+
189+
try {
190+
const response = await fetch("/autofix-ai", {
191+
method: "POST",
192+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
193+
body: "code=" + encodeURIComponent(code),
194+
});
195+
196+
if (!response.ok) {
197+
console.error("AI Autofix error:", response.status);
198+
return;
199+
}
200+
201+
const fixedCode = await response.text();
202+
const cursor = editor.getCursorPosition();
203+
editor.setValue(fixedCode, -1);
204+
editor.moveCursorToPosition(cursor);
205+
206+
// Run shellcheck again to update annotations
207+
await checkCode();
208+
} catch (error) {
209+
console.error("AI Autofix error:", error);
210+
}
211+
}
178212
</script>
179213
</body>
180214
</html>

main.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import (
1919
var (
2020
shfmtPath = getEnvOrDefault("SHFMT_PATH", "shfmt")
2121
shellcheckPath = getEnvOrDefault("SHELLCHECK_PATH", "shellcheck")
22+
groqAPIKey = os.Getenv("GROQ_API_KEY")
23+
groqModelID = getEnvOrDefault("GROQ_MODEL_ID", "openai/gpt-oss-120b")
24+
groqAPIURL = getEnvOrDefault("GROQ_API_URL", "https://api.groq.com/openai/v1/chat/completions")
2225
)
2326

2427
//go:embed index.html
@@ -55,11 +58,15 @@ func main() {
5558
http.HandleFunc("/format", handleFormat)
5659
http.HandleFunc("/shellcheck", handleShellcheck)
5760
http.HandleFunc("/autofix", handleAutofix)
61+
http.HandleFunc("/autofix-ai", handleAutofixAI)
5862

5963
port := getEnvOrDefault("PORT", "8080")
6064
log.Printf("Server starting on http://localhost:%s", port)
6165
log.Printf("Using shfmt: %s", shfmtPath)
6266
log.Printf("Using shellcheck: %s", shellcheckPath)
67+
if groqAPIKey != "" {
68+
log.Printf("AI autofix enabled with model: %s", groqModelID)
69+
}
6370
log.Fatal(http.ListenAndServe(":"+port, nil))
6471
}
6572

@@ -280,6 +287,171 @@ func handleAutofix(w http.ResponseWriter, r *http.Request) {
280287
w.Write(fixed)
281288
}
282289

290+
type GroqRequest struct {
291+
Model string `json:"model"`
292+
Temperature float64 `json:"temperature"`
293+
Messages []GroqMessage `json:"messages"`
294+
ResponseFormat map[string]interface{} `json:"response_format"`
295+
}
296+
297+
type GroqMessage struct {
298+
Role string `json:"role"`
299+
Content string `json:"content"`
300+
}
301+
302+
type GroqResponse struct {
303+
Choices []struct {
304+
Message struct {
305+
Content string `json:"content"`
306+
} `json:"message"`
307+
} `json:"choices"`
308+
}
309+
310+
type FixedCodeResponse struct {
311+
FixedCode string `json:"fixed_code"`
312+
}
313+
314+
func handleAutofixAI(w http.ResponseWriter, r *http.Request) {
315+
if r.Method != http.MethodPost {
316+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
317+
return
318+
}
319+
320+
if groqAPIKey == "" {
321+
log.Printf("GROQ_API_KEY not set")
322+
http.Error(w, "AI autofix not configured", http.StatusInternalServerError)
323+
return
324+
}
325+
326+
code := r.FormValue("code")
327+
if code == "" {
328+
w.Write([]byte(code))
329+
return
330+
}
331+
332+
// Create temporary file for shellcheck
333+
tmpFile := filepath.Join(os.TempDir(), "script.sh")
334+
if err := os.WriteFile(tmpFile, []byte(code), 0644); err != nil {
335+
log.Printf("autofix-ai error: %v", err)
336+
w.Write([]byte(code))
337+
return
338+
}
339+
defer os.Remove(tmpFile)
340+
341+
// Run shellcheck to get issues
342+
cmd := exec.Command(shellcheckPath, "-f", "tty", tmpFile)
343+
var out, stderr bytes.Buffer
344+
cmd.Stdout = &out
345+
cmd.Stderr = &stderr
346+
cmd.Run()
347+
348+
shellcheckOutput := out.String()
349+
if shellcheckOutput == "" {
350+
shellcheckOutput = stderr.String()
351+
}
352+
353+
if shellcheckOutput == "" {
354+
// No issues to fix
355+
w.Write([]byte(code))
356+
return
357+
}
358+
359+
// Build prompt for AI
360+
prompt := fmt.Sprintf(`Fix all ShellCheck issues in the following bash script. Return ONLY the fixed code without any explanations, markdown formatting, or code blocks.
361+
362+
ShellCheck Issues:
363+
%s
364+
365+
Original Script:
366+
%s`, shellcheckOutput, code)
367+
368+
// Prepare Groq API request
369+
reqBody := GroqRequest{
370+
Model: groqModelID,
371+
Temperature: 0,
372+
Messages: []GroqMessage{
373+
{
374+
Role: "system",
375+
Content: "You are a bash script fixing assistant. Return only the fixed code without any markdown formatting or explanations.",
376+
},
377+
{
378+
Role: "user",
379+
Content: prompt,
380+
},
381+
},
382+
ResponseFormat: map[string]interface{}{
383+
"type": "json_schema",
384+
"json_schema": map[string]interface{}{
385+
"name": "fixed_script",
386+
"schema": map[string]interface{}{
387+
"type": "object",
388+
"properties": map[string]interface{}{
389+
"fixed_code": map[string]interface{}{
390+
"type": "string",
391+
},
392+
},
393+
"required": []string{"fixed_code"},
394+
},
395+
},
396+
},
397+
}
398+
399+
jsonData, err := json.Marshal(reqBody)
400+
if err != nil {
401+
log.Printf("JSON marshal error: %v", err)
402+
w.Write([]byte(code))
403+
return
404+
}
405+
406+
// Call Groq API
407+
req, err := http.NewRequest("POST", groqAPIURL, bytes.NewBuffer(jsonData))
408+
if err != nil {
409+
log.Printf("Request creation error: %v", err)
410+
w.Write([]byte(code))
411+
return
412+
}
413+
414+
req.Header.Set("Content-Type", "application/json")
415+
req.Header.Set("Authorization", "Bearer "+groqAPIKey)
416+
417+
client := &http.Client{}
418+
resp, err := client.Do(req)
419+
if err != nil {
420+
log.Printf("API request error: %v", err)
421+
w.Write([]byte(code))
422+
return
423+
}
424+
defer resp.Body.Close()
425+
426+
if resp.StatusCode != http.StatusOK {
427+
log.Printf("API error: %d", resp.StatusCode)
428+
w.Write([]byte(code))
429+
return
430+
}
431+
432+
var groqResp GroqResponse
433+
if err := json.NewDecoder(resp.Body).Decode(&groqResp); err != nil {
434+
log.Printf("JSON decode error: %v", err)
435+
w.Write([]byte(code))
436+
return
437+
}
438+
439+
if len(groqResp.Choices) == 0 {
440+
log.Printf("No choices in response")
441+
w.Write([]byte(code))
442+
return
443+
}
444+
445+
var fixedResp FixedCodeResponse
446+
if err := json.Unmarshal([]byte(groqResp.Choices[0].Message.Content), &fixedResp); err != nil {
447+
log.Printf("Fixed code parse error: %v", err)
448+
w.Write([]byte(code))
449+
return
450+
}
451+
452+
w.Write([]byte(fixedResp.FixedCode))
453+
}
454+
283455
func formatShellcheckHTML(output string) string {
284456
if output == "" {
285457
return `<div class="text-sm text-green-600">✓ No issues found</div>`

0 commit comments

Comments
 (0)