@@ -19,6 +19,9 @@ import (
1919var (
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+
283455func formatShellcheckHTML (output string ) string {
284456 if output == "" {
285457 return `<div class="text-sm text-green-600">✓ No issues found</div>`
0 commit comments